Linux シグナル入門
2013-11-5 (鈴)- 1. シグナルとは?
- 2. シグナル・ハンドラと signal システム・コール
- 3. シグナルの用途
- 4. SIGHUP, SIGINT, SIGTERM
- 5. SIGCHLD
- 6. SIGALRM と sigaction システム・コール
- 7. siglongjmp による大域脱出
- 8. popen/pclose と SIGCHLD
5. SIGCHLD
SIGCHLD (= 17, 20 など実装により異なる, シグチャイルド, child)
SIGCHLD は Unix の初期の文献(※1) には現れない歴史的には後付けのシグナルであり,Unix 類の実装により整数値が異なる。 子 (child) プロセスが終了したり休止すると,親プロセスに SIGCHLD が送られる。
Bourne Shell のオリジナルの作者が直々に著した Unix の広範なガイドブックです。 §6.6.1 Signals (p.138) の "a complete list of signals" には第1章で言及した 15 個のシグナルしか挙げられていません。 /usr が当時は名前のとおり本当に各ユーザのホーム・ディレクトリ (/usr/srb など) を置いた場所だったことなど Unix の初期の姿を知ることができる貴重な資料……といいますか史料です。
01:~/tmp$ cat ^Z [1]+ Stopped cat 1481:~/tmp$Control-Z の打鍵で cat を実行中のプロセスに
SIGTSTP (= 20, 18 など実装により異なる,Terminal SToP)が送られて子プロセスが休止します。 この子プロセスの休止により親プロセスである bash に SIGCHLD が送られます。 bash は休止の原因となった SIGTSTP の値 (このカーネルでは 20) を 8 ビット目を立てて変数 $? にセットします。
Conrol-Z 打鍵 → SIGTSTP (=20) → cat 休止 → SIGCHLD (=17) → bash が Stopped cat と表示し $? を 148 (=20+128) にセット
デフォルトの動作としてプロセスは SIGCHLD を単に無視する。
このシグナルを利用して親プロセスは非同期に子プロセスの状態の変化を知ることができる。 典型的には親プロセスは SIGCHLD のハンドラで wait(2) や waitpid(2) を実行してシグナルの原因となった子プロセスのプロセス ID と終了ステータスを取得する。
子プロセスが死んだとき,デフォルトの設定では,親プロセスが終了ステータスを取得するか,あるいは親プロセス自身が死ぬまで子プロセスはゾンビ (zombie) プロセスと化していつまでも消えることができない。
プログラム例を示す。 fork(2) 後,子プロセスは 3 秒待って main 関数から return 4 して死ぬ。 親プロセスに SIGCHLD が送られ,handler(SIGCHLD) が非同期に起動される。 handler 内では wait(2) でプロセス ID handled_pid とプロセスの状態 handled_status を取得する。 for ループの中の if 文で handled_pid ≠ 0 であることを検出し,状態から情報を取り出すマクロ WIFEXITED と WEXITSTATUS を handled_status に適用して子プロセスが残した 4 を取り出し,"child exited: 4" を表示する。
#include <assert.h> #include <errno.h> #include <signal.h> #include <stdio.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> static volatile int handled_pid = 0; static volatile int handled_status = 0; static void handler(int sig) { int saved = errno; assert(sig == SIGCHLD); handled_pid = wait((int*) &handled_status); errno = saved; } int main(void) { if (signal(SIGCHLD, handler) == SIG_ERR) return 1; pid_t pid = fork(); if (pid < 0) return 2; if (pid == 0) { /* child */ printf("\tCHILD\n"); sleep(3); printf("\tRETURN 4\n"); return 4; } else { /* parent */ printf("PARENT\n"); int i; for (i = 0; i < 5; i++) { sleep(1); printf("%i\n", i); if (handled_pid != 0) { assert(pid == handled_pid); handled_pid = 0; int st = handled_status; if (WIFEXITED(st)) { printf("child exited: %d\n", WEXITSTATUS(st)); } else if (WIFSIGNALED(st)) { printf("child signaled: %d\n", WTERMSIG(st)); } else if (WIFSTOPPED(st)) { printf("child signaled: %d\n", WSTOPSIG(st)); } } } } return 0; }
ハンドラ内で呼び出している wait(2) は 第3章で言及した「非同期シグナルで安全な関数」の一つである。 ただし,wait(2) の呼び出しにより errno が変更されるかもしれない。 シグナル・ハンドラは1機械語命令ごとの粒度で処理に割り込むから,そのままではプログラムが信頼して errno の値を参照できなくなる。なんらかの関数呼び出しの失敗の直後に errno 値を取得したつもりでも,そのはざまの瞬間にシグナル・ハンドラが割り込んで書き換えているかもしれない。 なぜ関数呼び出しが失敗したのかの判断を誤ることになる。 マルチスレッドならば errno はスレッドごとにローカルであると保証されているが,シグナル・ハンドラはそうではない。 ハンドラ内でローカル変数 saved に errno の値を退避しているのは,こうした不具合を防ぐためである。
コンパイル&実行例を示す。
01:~/tmp$ gcc -Wall handle-sigchld.c 01:~/tmp$ ./a.out PARENT CHILD 0 1 RETURN 4 2 child exited: 4 3 4 01:~/tmp$
これまでシェルの $? 変数に残された終了ステータスの 8 ビット目が立っているかどうかで,直前のコマンドがシグナルで終了したかどうか判定できると説明してきました。 実は,このように終了ステータスを組み立てているのは Linux カーネルではなくシェルです。
wait(2) で取得した状態値に対して WIFEXITED, WIFSIGNALED, WIFSTOPPED のどれが真になるかで,普通に終了したのか,シグナルで終了したのか,それともシグナルで休止したのかを判定できます。 さらにそれぞれの場合についてそれぞれ WEXITSTATUS, WTERMSIG, WSTOPSIG を使って exit 値や終了・休止の原因となったシグナルを取得できます。 このとき WEXITSTATUS で取得できる exit 値は 8 ビットです。 子プロセスで exit(-1) としたとき,-1 の下位 8 ビットから 255 (== 0xFF) が取得されます。
ただし,実用上はシェルから見てシグナルを受けた場合と混同しないように 7 ビットで表現できる小さな非負整数に exit 値の範囲を限ります。
ライブラリ関数 popen(3) を使うときは SIGCHLD の扱いに注意する必要があります。 第8章で説明します。
6. SIGALRM と sigaction システム・コール へ続く