4. シグナルの実装(SIGHUP)
4.7 SIGHUP
通常、プロセスを起動している端末が無くなると、その端末から起動しているプロセス達に対し、SIGHUPが送られます。SIGHUPのデフォルトは「終了」なので、捕捉していないプロセスは強制終了します。
一方、プロセスがデーモン化していたりnohupコマンド経由で起動したプロセスは、SIGHUPが送られません。一般的にはバックグラウンドで起動する場合にはnohupになるようになっています。
SIGHUPを捕捉するプログラムでよく見かける実装は「プロセスの再起動」です。本当に再起動するのではない場合、途中処理を強制的に終わらせ、あたかも起動したかのように再スタートします。単にシグナルを補足するだけなら今までやってきていますが、再起動はどのように実装するのでしょうか。プロセスによっては子プロセスを起動している場合もあるので、子プロセスを終了させてから再度起動する必要があります。
ポイントが2つあります。
- 子プロセスの終了方法
- 任意の場所への移動方法
4.7.1 子プロセスの終了方法
子プロセスのプロセスIDを見つけてくるのに、psコマンドでプロセス一覧を参照できますが、これを頼りに子プロセスを探すのはよくありません。psコマンドでは、その時点でのスナップショットでしかないため、実際に終了処理を行う際に子プロセスが増減している可能性があるからです。
このような場合はプロセスグループをすべて同じ値にし、その値に対してシグナルを送る事ですべての子プロセスを一気に終了させます。
プロセスグループですが、プロセスには必ずプロセスIDとプロセスグループIDが割り当てられています。プロセスIDは一意の値ですが、プロセスグループIDは同一グループ内のプロセスに同じ値が振られています。普通に起動されたプロセスの場合、プロセスIDとプロセスグループIDは同値です。子プロセスの場合、グループIDは親プロセスのプロセスIDになっています。親プロセスが先に終了すると、子プロセスのグループIDはinitプロセスのプロセスID(つまり1)になります。
ここで注意しなくてはいけないのは、親プロセスIDと子プロセスグループIDが同じということです。プロセスグループIDを親と子で変えておかないと、プロセスグループIDで終了シグナルを送ると同時に親も終了する事になってしまいます。子プロセスのグループIDを親と違うものを設定し、かつ子プロセス間で同じ値にする必要があります。
プロセスグループを変更する方法
プロセスグループを変更するための関数としてsetpgid(2)があります。詳しくはman setpgidで見て欲しいのですが、要は複数起動する子プロセスのうち、一番最初に起動した子プロセスのプロセスIDをプロセスグループIDとしてしまいます。
なお、サンプルでは親プロセスの処理でg_pid(子プロセス用グループID)にプロセス番号を設定しています。これをしておかないと次に生成する子プロセスにとってはg_pidが初期値(0)のままになってしまうので、親プロセス側で子プロセスのプロセスIDをセットしています。
プロセスグループにシグナルを送る方法
次にプロセスグループIDに対してシグナルを送る方法です。これの詳細は後述する4.8章を参照して欲しいのですが、kill(2)を使います。第1引数にマイナス値のプロセスグループIDを、第2引数に配送したいシグナル値を設定し実行すればOKです。
/*
gcc signal_test_gpid.c -o signal_test_gpid -W -Wall -g
*/ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <signal.h> #include <errno.h> #include <unistd.h> #include <sys/wait.h> void signal_handler( int num ); void wait_child ( int max_cnt ); pid_t g_pid; int max_cnt; int main( int argc, char ** argv ) { if( argc < 2 ) { exit( 1 ); } struct sigaction sa; int i; sa.sa_handler = signal_handler; sa.sa_flags = SA_RESTART; sigemptyset( &sa.sa_mask ); sigaction( SIGHUP, &sa, 0 ); max_cnt = atoi( argv[1] ); g_pid = 0; fprintf( stdout, "parent:[%d][%d] Start!! \n", getpid( ), getpgrp( )); for( i = 0; i < max_cnt; i ++ ) { pid_t pid = fork( ); if( pid == 0 ) { /* child process */ fprintf( stdout, "child:[%d][%d][%d] Start!! \n", i, getpid( ), getpgrp( )); if( g_pid == 0 ) { g_pid = getpid( ); } setpgid( 0, g_pid ); fprintf( stdout, "child:[%d][%d][%d] Start!! \n", i, getpid( ), getpgrp( )); while( 1 ) { sleep( 3600 ); } exit( 0 ); } else if( pid > 0 ) { /* parent processs */ if( g_pid == 0 ) { g_pid = pid; } } } pause( ); fprintf( stdout, "[%s] normally end ... bye.\n", argv[0] ); return 0; } void signal_handler( int num ) { ( void )num; kill( -g_pid, SIGTERM ); wait_child( max_cnt ); } void wait_child( int max_cnt ) { pid_t pid; int status; while( max_cnt ) { pid = wait( &status ); 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 )); } } }
第1引数は起動したいプロセス数です。実行後、親のプロセスIDを控え、他のターミナルから下記コマンドを実行してください。
$ kill -HUP 12345(この数字は親のプロセスIDです)
子プロセスはシグナルを受けて終了し、waitによってゾンビ化が解消され親プロセスは正常終了している事が分かると思います。
4.7.2 任意の場所への移動方法
今までのシグナルでは、ハンドラで補足した後はプロセスを終了するか、何もしないかだけでした。終了しない場合は「sa_flags = SA_RESTART」としておけば本流を続行するだけでよいので、プログラム上で特に何も気にする必要はありません。しかし今回は任意の場所に飛ぶ必要があります。
ハンドラから異なる関数へ移動するには、sigsetjmp(3)、siglongjmp(3)を使います。あらかじめ移動したい場所にsigsetjmp(3)を設定し、ハンドラ内で処理を終えたらsiglongjmp(3)を実行します。今回の例ではプログラムの最初に戻るだけで戻り値などはまったく見ていません。実際はsigsetjmp(3)の戻り値からsiglongjmp(3)から戻ってきたかどうかを判定できるので適切に復帰処理を行って終了するようにします。
/*
gcc signal_test_hap.c -o signal_test_hap -W -Wall -g
*/ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <signal.h> #include <errno.h> #include <unistd.h> #include <sys/wait.h> #include <setjmp.h> void signal_handler( int num ); void wait_child ( int max_cnt ); pid_t g_pid; int max_cnt; sigjmp_buf rp; int main( int argc, char ** argv ) { if( argc < 2 ) { exit( 1 ); } struct sigaction sa; int i; sigsetjmp( rp, 1 ); sa.sa_handler = signal_handler; sa.sa_flags = SA_RESTART; sigemptyset( &sa.sa_mask ); sigaction( SIGHUP, &sa, 0 ); sigaction( SIGINT, &sa, 0 ); max_cnt = atoi( argv[1] ); g_pid = 0; fprintf( stdout, "parent:[%d][%d] Start!! \n", getpid( ), getpgrp( )); for( i = 0; i < max_cnt; i ++ ) { pid_t pid = fork( ); if( pid == 0 ) { /* child process */ fprintf( stdout, "child:[%d][%d][%d] Start!! \n", i, getpid( ), getpgrp( )); if( g_pid == 0 ) { g_pid = getpid( ); } setpgid( 0, g_pid ); fprintf( stdout, "child:[%d][%d][%d] Start!! \n", i, getpid( ), getpgrp( )); while( 1 ) { pause( ); } exit( 0 ); } else if( pid > 0 ) { /* parent processs */ if( g_pid == 0 ) { g_pid = pid; } } } while( 1 ) { pause( ); } return 0; } void signal_handler( int num ) { ( void )num; kill( -g_pid, SIGTERM ); wait_child( max_cnt ); if( num == SIGHUP ) { siglongjmp( rp, 1 ); } else { exit( 0 ); } } void wait_child( int max_cnt ) { pid_t pid; int status; while( max_cnt ) { pid = wait( &status ); max_cnt --; if( WIFEXITED( status )) { fprintf( stdout, "worker %i normally end... [%d]\n", pid, max_cnt ); } else { fprintf( stdout, "worker %i abnormally end...[%d][[%s]\n", pid, errno, strerror( errno )); } } }
第1引数は起動したいプロセス数です。実行後、親のプロセスID下記コマンドを控え、他のターミナルから下記コマンドを実行してください。
$ kill -HUP 12345(この数字は親のプロセスIDです)
先ほどとは異なり、再度プロセスが起動しているのが分かると思います。プログラムを起動したターミナルでCtrl+C、もしくは他のターミナルで下記コマンドを打つことで、子プロセス含め正常終了します。
$ kill -INT 12345(この数字は親のプロセスIDです)
ちなみに基本的には sigsetjmp/siglongjmp の使用は気をつけたほうが良いでしょう。私も sigsetjmp/siglongjmp の使用を積極的に薦めはしません。
