web-dev-qa-db-ja.com

SIGINTが親プロセスに送信されたときに子プロセスに伝播されないのはなぜですか?

シェルプロセス(例:sh)とその子プロセス(例:cat)を前提として、 Ctrl+C シェルのプロセスIDを使用していますか?


これは私が試したものです:

shを実行してからcatを実行:

[user@Host ~]$ sh
sh-4.3$ cat
test
test

別の端末からSIGINTcatに送信:

[user@Host ~]$ kill -SIGINT $PID_OF_CAT

catはシグナルを受信して​​終了しました(期待どおり)。

親プロセスへのシグナルの送信が機能していないようです。 信号が親プロセスに送信されたときにcatに伝搬されないのはなぜですかsh

これは動作しません:

[user@Host ~]$ kill -SIGINT $PID_OF_SH
67
rob87

どうやって CTRL+C 働く

最初に、方法を理解することです CTRL+C 動作します。

押すと CTRL+C、ターミナルエミュレータがETX文字(テキストの終わり/ 0x03)を送信します。
TTYは、この文字を受信するとSIGINTを端末のフォアグラウンドプロセスグループに送信するように構成されています。この構成は、stty -aを実行してintr = ^C;を確認することで表示できます。 POSIX仕様 は、INTRを受信すると、その端末のフォアグラウンドプロセスグループにSIGINTを送信する必要があることを示しています。

フォアグラウンドプロセスグループとは何ですか?

では、問題は、どのようにしてフォアグラウンドプロセスグループを特定するのかということです。フォアグラウンドプロセスグループは、キーボードによって生成された信号(SIGTSTP、SIGINTなど)を受信するプロセスのグループです。

プロセスグループIDを決定する最も簡単な方法は、psを使用することです。

ps ax -O tpgid

2番目の列はプロセスグループIDです。

プロセスグループにシグナルを送信するにはどうすればよいですか?

プロセスグループIDがわかったので、グループ全体にシグナルを送信するというPOSIXの動作をシミュレートする必要があります。

これは、グループIDの前に-を置くことにより、killで実行できます。
たとえば、プロセスグループIDが1234の場合、次を使用します。

kill -INT -1234

シミュレート CTRL+C 端子番号を使用します。

したがって、上記はシミュレーション方法をカバーしています CTRL+C 手動プロセスとして。しかし、TTY番号がわかっていて、シミュレーションしたい場合 CTRL+C そのターミナルのために?

これは非常に簡単になります。

$ttyがターゲットにするターミナルであると仮定しましょう(ターミナルでtty | sed 's#^/dev/##'を実行すると、これを取得できます)。

kill -INT -$(ps h -t $tty -o tpgid | uniq)

これにより、$ttyのフォアグラウンドプロセスグループが何であれ、SIGINTが送信されます。

92
Patrick

vinc17が言う のように、これが発生する理由はありません。信号生成キーシーケンスを入力すると(例: Ctrl+C)、シグナルは端末に接続されている(関連付けられている)すべてのプロセスに送信されます。 killによって生成される信号には、そのようなメカニズムはありません。

ただし、次のようなコマンド

_kill -SIGINT -12345
_

process group12345内のすべてのプロセスにシグナルを送信します。 kill(1) および kill(2) を参照してください。シェルの子は通常、シェルのプロセスグループにあります(少なくとも非同期でない場合)。したがって、シェルのPIDの負の値に信号を送信すると、必要な処理を行うことができます。


Oops

vinc17が指摘 のように、これはインタラクティブシェルでは機能しません。 mightが機能する別の方法を次に示します。

kill -SIGINT-$(echo $(ps -pPID_of_Shell o tpgid =))

_ps -pPID_of_Shell_は、シェルのプロセス情報を取得します。 _o tpgid=_は、ヘッダーなしで端末プロセスグループIDのみを出力するようにpsに指示します。これが10000未満の場合、psは先行スペースを付けて表示します。 $(echo …)は、先頭(および末尾)のスペースを取り除く簡単なトリックです。

私はこれをDebianマシンでの大まかなテストで機能させました。

質問には独自の答えが含まれています。 SIGINTcatプロセスにkillで送信すると、 Ctrl+C

より正確には、割り込み文字(デフォルトでは^C)がSIGINTを端末のフォアグラウンドプロセスグループのすべてのプロセスに送信します。 catの代わりに、複数のプロセスを含むより複雑なコマンドを実行している場合、^Cと同じ効果を得るには、プロセスグループを強制終了する必要があります。

&バックグラウンドオペレーターなしで外部コマンドを実行すると、シェルはコマンドの新しいプロセスグループを作成し、このプロセスグループがフォアグラウンドにあることをターミナルに通知します。シェルはまだ独自のプロセスグループにあり、もはやフォアグラウンドにはありません。次に、シェルはコマンドが終了するのを待ちます。

これは、よくある誤解、つまりシェルが子プロセスとターミナル間の相互作用を促進するためにシェルが何かをしているという考えによって被害者になったように見える場所です。それは真実ではありません。セットアップ作業(プロセスの作成、ターミナルモードの設定、パイプの作成と他のファイル記述子のリダイレクト、およびターゲットプログラムの実行)が完了すると、シェルは待つだけですcatに入力した内容は、通常の入力であっても、^Cのような信号を生成する特殊文字であっても、シェルを通過しません。 catプロセスは、独自のファイル記述子を介してターミナルに直接アクセスできます。ターミナルは、フォアグラウンドプロセスグループであるため、catプロセスにシグナルを直接送信できます。シェルは邪魔になりません。

catプロセスが終了すると、catプロセスの親であるため、シェルに通知されます。その後、シェルがアクティブになり、再びフォアグラウンドになります。

理解を深めるための演習です。

新しいターミナルのシェルプロンプトで、次のコマンドを実行します。

exec cat

execキーワードを指定すると、シェルは子プロセスを作成せずにcatを実行します。シェルはcatに置き換えられました。以前はシェルに属していたPIDは、catのPIDになりました。別の端末でpsを使用してこれを確認します。いくつかのランダムな行を入力して、catがそれらを繰り返していることを確認します。Shellプロセスが親としていないにもかかわらず、正常に動作していることを証明します。 押すとどうなりますか Ctrl+C 今?

回答:

SIGINTは、死ぬcatプロセスに配信されます。これはターミナルでの唯一のプロセスであるため、シェルプロンプトで「終了」と言ったかのように、セッションは終了します。実際には、しばらくの間、猫がシェルだった.

12
user41515

SIGINTを子に伝達する理由はありません。さらに、 system() POSIX仕様は、次のように述べています。

シェルが受信したSIGINTを伝播した場合。本当の Ctrl+C、これは、子プロセスがSIGINTシグナルを2回受信することを意味し、これは望ましくない動作をする可能性があります。

4
vinc17

setpgid POSIX Cプロセスグループの最小限の例

基礎となるAPIの最小限の実行可能な例を使用すると、理解が容易になる可能性があります。

これは、子がsetpgidでプロセスグループを変更しなかった場合に、信号が子に送信される方法を示しています。

main.c

_#define _XOPEN_SOURCE 700
#include <assert.h>
#include <signal.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

volatile sig_atomic_t is_child = 0;

void signal_handler(int sig) {
    char parent_str[] = "sigint parent\n";
    char child_str[] = "sigint child\n";
    signal(sig, signal_handler);
    if (sig == SIGINT) {
        if (is_child) {
            write(STDOUT_FILENO, child_str, sizeof(child_str) - 1);
        } else {
            write(STDOUT_FILENO, parent_str, sizeof(parent_str) - 1);
        }
    }
}

int main(int argc, char **argv) {
    pid_t pid, pgid;

    (void)argv;
    signal(SIGINT, signal_handler);
    signal(SIGUSR1, signal_handler);
    pid = fork();
    assert(pid != -1);
    if (pid == 0) {
        is_child = 1;
        if (argc > 1) {
            /* Change the pgid.
             * The new one is guaranteed to be different than the previous, which was equal to the parent's,
             * because `man setpgid` says:
             * > the child has its own unique process ID, and this PID does not match
             * > the ID of any existing process group (setpgid(2)) or session.
             */
            setpgid(0, 0);
        }
        printf("child pid, pgid = %ju, %ju\n", (uintmax_t)getpid(), (uintmax_t)getpgid(0));
        assert(kill(getppid(), SIGUSR1) == 0);
        while (1);
        exit(EXIT_SUCCESS);
    }
    /* Wait until the child sends a SIGUSR1. */
    pause();
    pgid = getpgid(0);
    printf("parent pid, pgid = %ju, %ju\n", (uintmax_t)getpid(), (uintmax_t)pgid);
    /* man kill explains that negative first argument means to send a signal to a process group. */
    kill(-pgid, SIGINT);
    while (1);
}
_

GitHubアップストリーム

コンパイル:

_gcc -ggdb3 -O0 -std=c99 -Wall -Wextra -Wpedantic -o setpgid setpgid.c
_

setpgidなしで実行

CLI引数がない場合、setpgidは実行されません。

_./setpgid
_

考えられる結果:

_child pid, pgid = 28250, 28249
parent pid, pgid = 28249, 28249
sigint parent
sigint child
_

そして、プログラムがハングします。

ご覧のとおり、fork全体で継承されるため、両方のプロセスのpgidは同じです。

その後、ヒットするたびに:

_Ctrl + C
_

それは再び出力します:

_sigint parent
sigint child
_

これは次の方法を示しています。

  • kill(-pgid, SIGINT)を使用してプロセスグループ全体にシグナルを送信する
  • 端末のCtrl + Cは、デフォルトでプロセスグループ全体にkillを送信します

両方のプロセスに異なるシグナルを送信して、プログラムを終了します。 _Ctrl + \_を使用したSIGQUIT。

setpgidで実行

引数を指定して実行した場合、例:

_./setpgid 1
_

次に、子はそのpgidを変更し、今度は親からのみ1つのsigintのみが出力されます。

_child pid, pgid = 16470, 16470
parent pid, pgid = 16469, 16469
sigint parent
_

そして今、あなたが打つたびに:

_Ctrl + C
_

親のみが信号も受信します。

_sigint parent
_

以前と同じように、SIGQUITを使用して親を殺すことができます。

_Ctrl + \
_

ただし、子は別のPGIDを持っているため、その信号を受信しません。これは以下から見ることができます:

_ps aux | grep setpgid
_

あなたはそれを明示的に殺す必要があります:

_kill -9 16470
_

これにより、信号グループが存在する理由が明らかになります。それ以外の場合は、残っているプロセスの束が常に手動でクリーンアップされることになります。

Ubuntu 18.04でテスト済み。