シェルプロセス(例:sh
)とその子プロセス(例:cat
)を前提として、 Ctrl+C シェルのプロセスIDを使用していますか?
これは私が試したものです:
sh
を実行してからcat
を実行:
[user@Host ~]$ sh
sh-4.3$ cat
test
test
別の端末からSIGINT
をcat
に送信:
[user@Host ~]$ kill -SIGINT $PID_OF_CAT
cat
はシグナルを受信して終了しました(期待どおり)。
親プロセスへのシグナルの送信が機能していないようです。 信号が親プロセスに送信されたときにcat
に伝搬されないのはなぜですかsh
?
これは動作しません:
[user@Host ~]$ kill -SIGINT $PID_OF_SH
最初に、方法を理解することです 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 手動プロセスとして。しかし、TTY番号がわかっていて、シミュレーションしたい場合 CTRL+C そのターミナルのために?
これは非常に簡単になります。
$tty
がターゲットにするターミナルであると仮定しましょう(ターミナルでtty | sed 's#^/dev/##'
を実行すると、これを取得できます)。
kill -INT -$(ps h -t $tty -o tpgid | uniq)
これにより、$tty
のフォアグラウンドプロセスグループが何であれ、SIGINTが送信されます。
vinc17が言う のように、これが発生する理由はありません。信号生成キーシーケンスを入力すると(例: Ctrl+C)、シグナルは端末に接続されている(関連付けられている)すべてのプロセスに送信されます。 kill
によって生成される信号には、そのようなメカニズムはありません。
ただし、次のようなコマンド
_kill -SIGINT -12345
_
process group12345内のすべてのプロセスにシグナルを送信します。 kill(1) および kill(2) を参照してください。シェルの子は通常、シェルのプロセスグループにあります(少なくとも非同期でない場合)。したがって、シェルのPIDの負の値に信号を送信すると、必要な処理を行うことができます。
vinc17が指摘 のように、これはインタラクティブシェルでは機能しません。 mightが機能する別の方法を次に示します。
kill -SIGINT-$(echo $(ps -pPID_of_Shell o tpgid =))
_ps -pPID_of_Shell
_は、シェルのプロセス情報を取得します。 _o tpgid=
_は、ヘッダーなしで端末プロセスグループIDのみを出力するようにps
に指示します。これが10000未満の場合、ps
は先行スペースを付けて表示します。 $(echo …)
は、先頭(および末尾)のスペースを取り除く簡単なトリックです。
私はこれをDebianマシンでの大まかなテストで機能させました。
質問には独自の答えが含まれています。 SIGINT
をcat
プロセスに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プロセスに配信されます。これはターミナルでの唯一のプロセスであるため、シェルプロンプトで「終了」と言ったかのように、セッションは終了します。実際には、しばらくの間、猫がシェルだった.
SIGINT
を子に伝達する理由はありません。さらに、 system()
POSIX仕様は、次のように述べています。
シェルが受信したSIGINT
を伝播した場合。本当の Ctrl+C、これは、子プロセスがSIGINT
シグナルを2回受信することを意味し、これは望ましくない動作をする可能性があります。
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);
}
_
コンパイル:
_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 + \
_を使用した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でテスト済み。