4. シグナルの実装(ゾンビプロセス)
4.5 ゾンビプロセス
4.5.1 ゾンビプロセスとは
UNIX系OSでは、主にfork(2)を行う事でプロセス内からプロセスを起動することができます。普通、fork(2)を行ったプロセスを親プロセス、fork(2)によって生まれたプロセスを子プロセスと言います。しかし、親プロセスが子プロセスを生成した場合、親プロセスは一般的にwait(2)によって子プロセスの終了を待たなくてはいけません。
wait(2)を行う事で、wait(2)する事で子プロセスがどの様な状態で終了したのかを知る事ができます。この辺りはwait(2)の引数であるstatusを提供されたマクロに通す事で判定可能です。詳細はman wait/waitpidを参照してください。
親プロセスは子プロセスを生成したら、子プロセスが死ぬのをwait(2)で待なくてはいけませんが、では待たないとどうなるのでしょうか。
子プロセスの終了から親プロセスのwait(2)呼び出しまでの間、子プロセスは「ゾンビプロセス」という状態になっています。ゾンビプロセスはプロセスとして動いてはいませんが、OS上ではプロセス管理対象(プロセステーブル上のエントリを占めている状態)です。
プロセステーブルには一定数分しか容量がなく、OSがプロセス数不足を来たし、新たにプロセスの起動が出来なくなる可能性があるので、これは少々まずい状態です。仮にシステムがリソース不足を来たさないとしても、各ユーザーが同時に起動できるプロセス数には制限があります。
親プロセスがデーモンで、クライアントとのセッションを子プロセスに対応させる場合、セッション終了する度に子プロセスがゾンビ化していく。
ゾンビ対策を行わないと、最終的にはプロセスの起動が出来なくなってしまうでしょう。
なお、親プロセスが子プロセスよりも先に亡くなると、子プロセスはinitプロセスに引き取られます。initは子プロセス終了後のクリーンアップに必要な作業を肩代わりします。これはこれでまずい事ではないですが、自分で生成した子プロセスを他プロセスに処理させるのは、行儀のよいプロセスとは言えないでしょう。
/*
gcc signal_test_zonbie.c -o signal_test_zonbie -W -Wall -g
*/ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> int main( int argc, char ** argv ) { if( argc < 3 ) { exit( 1 ); } int max_cnt = atoi( argv[1] ); int slep_sec = atoi( argv[2] ); int status; int i = 0; pid_t pid; for( i = 0; i < max_cnt; i ++ ) { pid = fork( ); if( pid == 0 ) { fprintf( stdout, "child:[%d] sleep:[%d] Start!! \n", i, slep_sec ); sleep( slep_sec ); fprintf( stdout, "child:[%d] sleep:[%d] End!! \n", i, slep_sec ); exit( 0 ); } } sleep( slep_sec * 2 ); while( max_cnt ) { pid = wait( &status ); if( pid < 0 ) { break; } max_cnt --; if( WIFEXITED( status )) { fprintf( stdout, "worker %i normally end...\n", pid ); } else { fprintf( stdout, "worker %i abnormally end...[%d][[%s]\n", pid, errno, strerror( errno )); } } fprintf( stdout, "parent process [%d][[%s]\n", errno, strerror( errno )); return 0; }
上記プログラムは引数を2個とります。1個目は子プロセスの数、2個目は子プロセスの処理時間(sleep秒)で、親プロセスは子プロセスの処理時間の2倍sleepします。つまり、子プロセスが先に終了し、親プロセスがwait(2)を行うまで時間がある状態になります。
この時、別ターミナルで下記コマンドを実行してみてください。
$ ps asx | grep signal_test | grep -v grep
子プロセスが動いている時は状態フィールドが「S」になっていますが、子プロセスが終了し親プロセスがwait(2)するまでの間は、状態フィールドが「Z」になっています。なお、このプログラムはゾンビプロセスを大量に長期間生成できるので、扱いには十分気をつけ、個人の責任の範疇で行うようにしてください。
子プロセスが終了するまで親プロセスはひたすら待つはナンセンスです。ネットワークを扱うプロセスの場合、通常は親プロセスはselect(2)やaccept(2)等でセッション待機していて、その間に子プロセスはクライアントと処理を行っています。
つまりそのような状況でも子プロセスが終了した事を検知し、適切な処理を行えるようにするべきです。そこで、シグナルを用いる事になります。主にその方法は3パターンあります。
4.5.2 SIGCHLDにSIG_IGNをセット
子プロセスが終了すると、SIGCHLDシグナルが発生します。親プロセスはこのシグナルに対して無視を設定する事で、子プロセスのゾンビ化は避けられます。
/*
gcc signal_test_nowait.c -o signal_test_nowait -W -Wall -g
*/ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <signal.h> #include <errno.h> #include <unistd.h> int main( int argc, char ** argv ) { if( argc < 3 ) { exit( 1 ); } int max_cnt = atoi( argv[1] ); int slep_sec = atoi( argv[2] ); int main_slep_sec = slep_sec * 2; int i = 0; pid_t pid = 0; struct sigaction sa; sa.sa_handler = SIG_IGN; sigemptyset( &sa.sa_mask ); sigaction( SIGCHLD, &sa, 0 ); for( i = 0; i < max_cnt; i ++ ) { pid = fork( ); if( pid == 0 ) { fprintf( stdout, "child:[%d] sleep:[%d] pid:[%d] Start!!\n", i, slep_sec, getpid( )); sleep( slep_sec ); fprintf( stdout, "child:[%d] sleep:[%d] pid:[%d] End!!\n", i, slep_sec, getpid( )); exit( 0 ); } } while( main_slep_sec ) { main_slep_sec = sleep( main_slep_sec ); } fprintf( stdout, "parent process [%d][[%s]\n", errno, strerror( errno )); return 0; }
子プロセスの終了時判定などを行わない場合は、コレが一番シンプルで良いのかも知れません。
4.5.3 ハンドラで対処
子プロセスが終了すると、SIGCHLDシグナルが発生します。親プロセスはこのイベントコードを捕捉するハンドラを用意して、そのハンドラ内でwait(2)すればよいことになります。
/*
gcc signal_test_wait.c -o signal_test_wait -W -Wall -g
*/ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <signal.h> #include <errno.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> #include <time.h> void signal_handler( int num ); int main( int argc, char ** argv ) { if( argc < 3 ) { exit( 1 ); } int max_cnt = atoi( argv[1] ); int slep_sec = atoi( argv[2] ); int ret = 0; int i = 0; pid_t pid = 0; struct sigaction sa; struct timespec req, rem; sa.sa_handler = signal_handler; sa.sa_flags = 0; sigemptyset( &sa.sa_mask ); sigaction( SIGCHLD, &sa, 0 ); for( i = 0; i < max_cnt; i ++ ) { pid = fork( ); if( pid == 0 ) { fprintf( stdout, "child:[%d] sleep:[%d] Start!! \n", i, slep_sec ); sleep( slep_sec ); fprintf( stdout, "child:[%d] sleep:[%d] End!! \n", i, slep_sec ); exit( 0 ); } } req.tv_sec = slep_sec * 2; req.tv_nsec = 0; rem.tv_sec = 0; rem.tv_nsec = 0; while( 1 ) { ret = nanosleep( &req, &rem ); if( ret == 0 ) { break; } if( errno == EINTR ) { req = rem; rem.tv_sec = 0; rem.tv_nsec = 0; continue; } break; } fprintf( stdout, "parent process end.\n" ); return 0; } void signal_handler( int num ) { ( void )num; pid_t pid; int status; pid = wait( &status ); if( WIFEXITED( status )) { fprintf( stdout, "worker %i normally end...\n", pid ); } else { fprintf( stdout, "worker %i abnormally end...[%d][[%s]\n", pid, errno, strerror( errno )); } return; }
上記プログラムを試し、別ターミナルで下記コマンドを実行してみてください。
$ ps asx | grep signal_test | grep -v grep
生成する子プロセス数にもよりますが、私の環境では5個のプロセスを起動したら3個のプロセスがゾンビ化してしまいました。
それはなぜでしょうか。実はシグナルハンドラでwait(2)を呼ぶだけでは不十分なのです。子プロセスは確かにSIGCHLDシグナルを送っていますが、今回の場合はすべての子プロセスがほぼ同時に終了しており、ハンドラは同時に複数のシグナルを受信する事になります。しかしハンドラは同時に複数のシグナルを受信した場合、1回しかwait(2)を発行しません。
つまりwait(2)の取りこぼしが発生してしまい、結果、ゾンビプロセスが発生してしまう事になります。しかしwait(2)をループで囲み、全ゾンビプロセスに対してwait(2)を発行しようとすると、今度はゾンビプロセスが生まれるまでハンドラ内でサスペンドしてしまい、親プロセスの処理が滞ってしまいます。
この問題に対処するためには、wait(2)ではなくwaitpid(2)を使用します。waitpid(2)はwait(2)の改良版で、オプション次第ではwait(2)のようにサスペンドしません。各オプションはmanなどを参照して欲しいですが、今回やりたい事をするには、下記の記述で対処できます。
waitpid( -1, &status, WNOHANG );
第1引数が「-1」なのは、全子プロセスを対象にするという意味です。第2引数は子プロセスの状態情報、第3引数はwaitpidの動作で、WNOHANGはゾンビプロセスが居ない場合は「0」を返してすぐに復帰するという意味です。戻り値としては成功すればゾンビプロセスのpidを、ゾンビプロセスが居ない時は「0」を、エラーのときは「-1」を返します。
/*
gcc signal_test_waitpid.c -o signal_test_waitpid -W -Wall -g
*/ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <signal.h> #include <errno.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> #include <time.h> void signal_handler( int num ); int main( int argc, char ** argv ) { if( argc < 3 ) { exit( 1 ); } int max_cnt = atoi( argv[1] ); int slep_sec = atoi( argv[2] ); int ret = 0; int i = 0; pid_t pid = 0; struct sigaction sa; struct timespec req, rem; sa.sa_handler = signal_handler; sa.sa_flags = 0; sigemptyset( &sa.sa_mask ); sigaction( SIGCHLD, &sa, 0 ); for( i = 0; i < max_cnt; i ++ ) { pid = fork( ); if( pid == 0 ) { fprintf( stdout, "child:[%d] sleep:[%d] Start!! \n", i, slep_sec ); sleep( slep_sec ); fprintf( stdout, "child:[%d] sleep:[%d] End!! \n", i, slep_sec ); exit( 0 ); } } req.tv_sec = slep_sec * 2; req.tv_nsec = 0; rem.tv_sec = 0; rem.tv_nsec = 0; while( 1 ) { ret = nanosleep( &req, &rem ); if( ret == 0 ) { break; } if( errno == EINTR ) { req = rem; rem.tv_sec = 0; rem.tv_nsec = 0; continue; } break; } fprintf( stdout, "parent process end.\n" ); return 0; } void signal_handler( int num ) { ( void )num; pid_t pid; int status; while( 1 ) { pid = waitpid( -1, &status, WNOHANG ); if( pid <= 0 ) { return; } if( WIFEXITED( status )) { fprintf( stdout, "worker %i normally end...\n", pid ); } else { fprintf( stdout, "worker %i abnormally end...[%d][[%s]\n", pid, errno, strerror( errno )); } } }
上記プログラムを試し、別ターミナルで下記コマンドを実行してみてください。ゾンビプロセスがいない事が分かると思います。
$ ps asx | grep signal_test | grep -v grep
4.5.4 SA_NOCLDWAITの使用
先の方法は、子プロセスの終了をシグナルで検知し、ハンドラ内でゾンビプロセスがいたらループしてwaitpidを飛ばすという方法でした。ゾンビプロセスとはwait(2)/waitpid(2)待ちのプロセスで、これによりプロセスの終了情報が得られる、という事でした。
しかしアプリケーションによっては子プロセスの終了情報はいらない、しかし全子プロセスの終了を待って親プロセスが終了したい、一言で言うと行儀よくプログラムを終了したい場合があります。そのような場合、上記のようにハンドラを用意して処理するだけでは対応が困難だったり冗長な処理(上記の例で言うと子プロセス数を外部変数で管理する等)を付加する必要があります。
子プロセスの終了ステータスを拾わず、且つ全子プロセスの終了を待つ事が出来ます。しかしこの方法はカーネル依存です。kernel 2.6以降と言われていますが、確認するには「man sigaction」内に「SA_NOCLDWAIT」があるかどうかで判断してよいと思います。
/*
gcc signal_test_nocldwait.c -o signal_test_nocldwait -W -Wall -g
*/ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <signal.h> #include <errno.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> int main( int argc, char ** argv ) { if( argc < 4 ) { exit( 1 ); } int max_cnt = atoi( argv[1] ); int slep_sec = atoi( argv[2] ); int status; int i = 0; pid_t pid; struct sigaction sa; sa.sa_handler = SIG_IGN; sa.sa_flags = SA_NOCLDWAIT; sigemptyset( &sa.sa_mask ); sigaction( SIGCHLD, &sa, 0 ); for( i = 0; i < max_cnt; i ++ ) { pid = fork( ); if( pid == 0 ) { fprintf( stdout, "child:[%d] sleep:[%d] Start!! \n", i, slep_sec ); sleep( slep_sec ); fprintf( stdout, "child:[%d] sleep:[%d] End!! \n", i, slep_sec ); exit( 0 ); } } sleep( atoi( argv[3] )); fprintf( stdout, "parent process, now wait.\n" ); wait( &status ); fprintf( stdout, "parent process, end.[%d][[%s]\n", errno, strerror( errno )); return 0; }
上記プログラムから、シグナルハンドラを「無視」、フラグにSA_NOCLDWAITをセットする事で実現できます。
上記プログラムは引数を3個とります。1個目は子プロセスの数、2個目は子プロセスの処理時間(sleep秒)。3個目は親プロセスの処理時間(sleep秒)です。
子プロセスの処理時間を長くすると、親プロセスはwait(2)ですべての子プロセスの終了を待ち、子プロセスが終了すれば親プロセスも終了します。子プロセスの処理時間を短くすると、親プロセスがwait(2)を行う時には子プロセスはゾンビにならずに終了しているので、そのまま終わる事ができます。ここではwaitを子プロセス数分用意することなく、1つのwait(2)で全子プロセス分待ってくれます。
まだまだ環境依存な方法ですが、waitpidで実装するよりもずっと簡単に、かつ安全で"行儀の良い"対処ができるので、使用できる環境であれば使っていくべきでしょう。
