Linux シグナル入門

2013-11-5 (鈴)

5. SIGCHLD

   SIGCHLD (= 17, 20 など実装により異なる, シグチャイルド, child)

SIGCHLD は Unix の初期の文献(※1) には現れない歴史的には後付けのシグナルであり,Unix 類の実装により整数値が異なる。 子 (child) プロセスが終了したり休止すると,親プロセスに SIGCHLD が送られる。

※1: S. R. Bourne: The UNIX System, Addison-Wesley Publishing, 1982, ISBN 0-201-13791-7
Bourne Shell のオリジナルの作者が直々に著した Unix の広範なガイドブックです。 §6.6.1 Signals (p.138) の "a complete list of signals" には第1章で言及した 15 個のシグナルしか挙げられていません。 /usr が当時は名前のとおり本当に各ユーザのホーム・ディレクトリ (/usr/srb など) を置いた場所だったことなど Unix の初期の姿を知ることができる貴重な資料……といいますか史料です。
プロセスを「休止」つまり stop ないし suspend させる典型的な方法は Control-Z の打鍵です。
01:~/tmp$ cat
^Z
[1]+  Stopped                 cat
1481:~/tmp$  
Control-Z の打鍵で cat を実行中のプロセスに
   SIGTSTP (= 20, 18 など実装により異なる,Terminal SToP)
が送られて子プロセスが休止します。 この子プロセスの休止により親プロセスである bashSIGCHLD が送られます。 bash は休止の原因となった SIGTSTP の値 (このカーネルでは 20) を 8 ビット目を立てて変数 $? にセットします。
   Conrol-Z 打鍵 → SIGTSTP (=20)
   → cat 休止   → SIGCHLD (=17)
   → bash が Stopped cat と表示し $? を 148 (=20+128) にセット

デフォルトの動作としてプロセスは SIGCHLD を単に無視する。

初期の Unix には無かったシグナルですから,互換性のためにも,かかわらない限りは始めから無かったと同じにできるようにしたのでしょう。

このシグナルを利用して親プロセスは非同期に子プロセスの状態の変化を知ることができる。 典型的には親プロセスは SIGCHLD のハンドラで wait(2) や waitpid(2) を実行してシグナルの原因となった子プロセスのプロセス ID と終了ステータスを取得する。

システム・コール wait(2), waitpid(2) は名前のとおり子プロセスの状態が変化するまで待ちます。 シグナルを利用しなくても親はこれらのシステム・コールで直接,同期待ちをすることもできます。 さらに別解として waitpid(2) の第3引数に WNOHANG を指定すると状態変化がないとき待ちませんから,ループの中でときどき子プロセスの状態を調べるポーリングを実現できます。

子プロセスが死んだとき,デフォルトの設定では,親プロセスが終了ステータスを取得するか,あるいは親プロセス自身が死ぬまで子プロセスはゾンビ (zombie) プロセスと化していつまでも消えることができない。

プロセス ID と終了ステータスを親プロセスに伝えるために亡き子が情報だけ残していることを指して「ゾンビ」プロセスと呼んでいるわけです。 これをなきがらの体が動く「ゾンビ」と形容することは強烈な印象を与えますが,実際はより儚げな,亡き子の言葉だけ伝えて消える形見のようなものです。
子プロセスが死んだとき,ゾンビと化すことなくそのまま消えるように設定するにはシステム・コール sigaction(2) の第1引数に SIGCHLD を与え,第2引数の sa_flags メンバに SA_NOCLDWAIT を,sa_handler メンバに SIG_DFL またはシグナル・ハンドラを与えます。 この場合 sa_handler メンバにシグナル・ハンドラを与えて子プロセスが死んだときの動作は Unix 類の実装に依存しますが,Linux ではシグナル・ハンドラが期待どおりに起動されます。 ただし,wait(2) や waitpid(2) で亡き子プロセスの情報を取得することはできません。 子が親に言葉を伝えるつて (=ゾンビ・プロセス) が無いのですから仕方ありません。

プログラム例を示す。 fork(2) 後,子プロセスは 3 秒待って main 関数から return 4 して死ぬ。 親プロセスに SIGCHLD が送られ,handler(SIGCHLD) が非同期に起動される。 handler 内では wait(2) でプロセス ID handled_pid とプロセスの状態 handled_status を取得する。 for ループの中の if 文で handled_pid ≠ 0 であることを検出し,状態から情報を取り出すマクロ WIFEXITEDWEXITSTATUShandled_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 はスレッドごとにローカルであると保証されているが,シグナル・ハンドラはそうではない。 ハンドラ内でローカル変数 savederrno の値を退避しているのは,こうした不具合を防ぐためである。

伝統的な Unix では errno は intro(2) の man ページで説明されていますが,Linux では errno(3) で説明されています。 このプログラムでは今のところ errno も perror(3) も使っていませんから saved への退避をしなくてもただちには破綻しませんが,関数呼び出しを伴うハンドラの「お手本」として退避処理を書きました。

コンパイル&実行例を示す。

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 システム・コール へ続く


Copyright (c) 2013 OKI Software Co., Ltd.