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

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

シェルプロセス(例sh)とそのサブプロセス(例cat)が与えられた場合、シェルのプロセスIDを使用して+アクションをシミュレートする方法はCtrlC


私が試したことは次のとおりです。

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

答え1

CTRL+C仕組み

最初に理解する必要があるのは、CTRL+がどのようにC機能するかです。

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

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

今問題は、フォアグラウンドプロセスグループが何であるかを判断する方法です。フォアグラウンドプロセスグループは、単にキーボードから生成されたすべての信号(SIGTSTP、SIGINTなど)を受信するプロセスグループです。

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

ps ax -O tpgid

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

プロセスグループにシグナルを送信するには?

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

killグループIDを前に追加するだけです。たとえば、プロセスグループ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)

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

答え2

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

しかし、このようなコマンドは

kill -SIGINT -12345

すべてのプロセスに信号を送信します。プロセスグループ12345;見てください殺す(1) そして殺す(2)。シェルの子プロセスは通常シェルのプロセスグループにあるため(少なくとも非同期でない場合)、負のシェルPIDにシグナルを送信すると、必要に応じて機能できます。


こんな

〜のようにvinc17が指摘しました。、これは対話型シェルでは機能しません。これは代替です可能働く:

kill -SIGINT -$(echo $(ps -p)シェルPIDtpgid=))

ps -pPID_of_shellシェルのプロセス情報を取得します。ヘッダーなしで端末プロセスグループIDのみを出力する必要があることo tpgid=を示します。ps10000未満の場合、ps先行スペースが表示されます。これは$(echo …)先行(および末尾)スペースを削除する簡単な方法です。

私はDebianマシンでおおよそのテストをしました。

答え3

この質問には独自の回答が含まれています。 Send SIGINTto catprocess はkill+ を押すと何が起こるかを完全にシミュレートします。CtrlC

より正確には、割り込み文字は^C(デフォルトでは)SIGINT端末の前景プロセスグループ内のすべてのプロセスに送信されます。cat複数のプロセスを含むより複雑なコマンドを実行する場合^C

バックグラウンド演算子なしで外部コマンドを実行すると、&シェルはコマンドの新しいプロセスグループを作成し、プロセスグループがフォアグラウンドにあることを端末に通知します。シェルは独自のプロセスグループに残り、フォアグラウンドにはもうありません。その後、シェルはコマンドが終了するのを待ちます。

ここで一般的な誤解、つまりシェルが子プロセスとターミナル間の相互作用を促進するために何かをしているという考えの犠牲になるようです。それは本当ではありません。設定(プロセス生成、ターミナルモード設定、パイプ生成、その他のファイル記述子リダイレクト、ターゲットプログラムの実行)が完了したら、シェルただ待っています。cat通常の入力であるか信号を生成する特殊文字(たとえば)であれ、入力した内容はシェルを介して渡されません^C。プロセスは独自のファイル記述子を介して端末に直接アクセスでき、フォアグラウンドプロセスグループであるため、cat端末はプロセスに直接信号を送信できます。cat皮が剥がれた。

プロセスが終了すると、catそのプロセスの親プロセスであるシェルに通知が送信されますcat。これにより、シェルがアクティブになり、前景として再び表示されます。

理解度を高める練習です。

新しい端末のシェルプロンプトで、次のコマンドを実行します。

exec cat

このキーワードを使用すると、子プロセスを作成せずにexecシェルが実行されます。catシェルはで置き換えられますcat。以前シェルに属していたPIDは、現在のPIDですcatps他の端末でこれを確認してください。任意の行を入力してcat繰り返し表示されることを確認すると、親プロセスであるシェルプロセスがなくてもまだ正常に機能することがわかります。今+を押すとCtrlどうなりますかC

答え:

SIGINT は終了する cat プロセスに渡されます。端末の唯一のプロセスなので、シェルプロンプトで「exit」と言ったようにセッションが終了します。実際に猫以前はしばらくの間あなたの殻。

答え4

setpgidPOSIX C プロセスグループの最小例

基本 API の実行可能な最小の例を理解する方が簡単です。

これは、子プロセスがプロセスグループを変更していない場合に子プロセスにシグナルを送信する方法を示していますsetpgid

メインプログラム

#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

プログラムが中断されます。

ご覧のとおり、2つのプロセスのpgidは横にあるので同じですfork

次に、クリックするたびに次の操作を行います。

Ctrl + C

再出力されます:

sigint parent
sigint child

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

  • プロセスグループ全体にシグナルを送信するkill(-pgid, SIGINT)
  • ターミナルでは、Ctrl + Cはデフォルトでプロセスグループ全体にkillコマンドを送信します。

SIGQUITなど、2つのプロセスに異なる信号を送信してプログラムを終了しますCtrl + \

次に実行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でテストされました。

関連情報