昨日の問題をきっかけに、いままで曖昧なままにしていた
SIGCHLD とゾンビ・プロセスについて整理してみました。
恐らくツッコミどころ満載な説明だと思いますが、
間違いをそのまま覚えているのはつらいですので、
あえて無知を晒け出そうと思います。
尚、ゾンビは BSD よりの表現らしく、SYSV では defunct と呼ぶようです。
自分はとりあえずゾンビで呼び方を統一しますので、
その辺りは適当に読み替えて下さい。
▼仕組み:
一般に UNIX では、親が子プロセス(の終了)と同期を取る為に、
- exit や C-c interrupt で終了した時等
*1
に、親プロセスに送られる SIGCHLD
- 子プロセスの処理終了まで待つ wait システムコール
といった仕組みが用意されているのですが、
- プロセスが死ぬと親プロセスに CHLD シグナルを送って自分はゾンビになる。 (処理系に応じてゾンビを作らない設定をする事で避ける事も可能)
- ゾンビの子がいる時に親プロセスが wait を発行すると子プロセスは成仏する。
- 親プロセスが死んだ場合は、init が代わりに処理をしてくれる。 (実は、自分はここの理解が曖昧です… daemon ライブラリを使っているせいでしょうか…)
という挙動をします。つまり、
親に先立ち子プロセスが死んだ場合は、子プロセスは水子(ゾンビ)として
自縛霊のようにプロセステーブルを占有して、親に (wait という)
お経を唱えてもらうまで成仏できません。
お経 (wait) を忘れて、短命な子プロセスを生成し続ける無責任な親がいると、
プロセステーブルがゾンビで溢れて新たなプロセスが生成できなくなり、
(一般ユーザでは)ログインさえ出来なくなります。
一つのプロセスがシステム全体に悪影響を及ぼしますので、
fork を利用する際には、子プロセスを作り過ぎないというだけでなく、
ゾンビの処理にも気をつけなければなりません。
▼対策:
自分は、親子で同期を取る必要のない
放任主義的なプログラムを作成する時には、
CHLD シグナルの割り込みハンドラで wait を実行するルーチンを
設定しています。
先の例では、
$SIG{'CHLD'} = sub { wait(); };
といったコードを追加していますが、
それだけでは問題が生じる事があります。なぜなら、
- 複数の子プロセスが同時に終了すると、 一つの SIGCHLD しか伝えられない。
つまり、一つの SIGCHLD の割り込みハンドラで一度 wait を実行したとしても、
取りこぼしが生じて、ゾンビが生じる可能性が残るからです。
この辺を care しないデーモンは DoS で簡単にサービス不能になります…
の「シグナルの取りこぼしとwaitpid()システムコール」に実例が載っていますので
参考になると思います。
これは解決しないといけない問題ですが、
wait(2) は、単純に子プロセスが死ぬまで待ちますので、
死んだ子プロセスがいる時に呼ばないとブロックします。
*2
無条件で呼ぶとブロックする可能性があります。
その為、非同期(
ブロックしない)指定が可能な wait である
waitpid(2) や
wait3(2) 、 wait4(2) 等を使うのが定石のようです。
$SIG{'CHLD'} = sub { while(wait3() > 0) { } };
use POSIX ":sys_wait_h";
$SIG{'CHLD'} = sub { shift; while(waitpid(-1,&WNOHANG)> 0) { } };
このようにすれば、
wait3
*3
waitpid は、
- ゾンビの子プロセスが残っている間は 処理したプロセスの pid を返す
- ゾンビを処理しきった後はブロックせずに -1 を返すので while を抜ける
という挙動をしますので、残っているゾンビプロセスを一掃できます。
▼まとめ:
そういう訳で、自分は fork をして、
かつ
親子で同期を取る必要のないプログラムを書く時には、
呪文のように、先の
wait3waitpid のコードを SIGCHLD ハンドラに設定する事にしています。
尚、
にあるように、SIG_IGN を設定すると、
システムが良きに計らってくれる処理系もありますし、
もしかしたら、それが主流派かもしれません。
ですが、(いささか古い FAQ ではありますが)
を読むと、
POSIX では SIG_IGN に SIGCLD をセットした時の振舞いは規定されていないので、
POSIX 系をサポー トするようなプログラムには使うことができません。
とありますので、今のところ、SIGCHLD でSIG_IGN を利用せず、
自前でハンドラを用意する事にしています。
▼備考:
以上の話はゾンビ対策に限った場合の話でして、
BSD のマニュアルにある通り
SIGCHLD はプロセスのステータス変化を親に通知するシグナルですので、suspend した時にも SIGCHLD が発生するようです。
# まだ確かめていません。
また、プロセスが生きていたとしても、上記のような状態変化がある場合は、
wait でブロックしないそうです。
とはいえ…
自分としては子プロセスに resume させたりといったプログラムと
しばらく縁がなさそう
…でもなかったりします… (汗……ですので、子プロセスの終了に限定した話をするのも意味があると思います。
▼追記 (2009/4/22):
google 経由で来られる方が多いので、少しまとめました。
*1: signal(7) のマニュアルを読むと、
Linux2.4 は「Child stopped or terminated」、
NetBSD1.6 ではより抽象的に「child status has changed」と記載されていて、
処理系によって説明が異なるようですので、
「〜等」と表現しました…
*2: ブロックしない条件が曖昧ですので、断言は避けます。
*3: Solaris2.x や Linux2.x、FreeBSD4.x で wait3 を使って来たのですが、
今回、NetBSD1.6 では wait3 が使えなかったので、より一般的な waitpid に
変更しました…