はじめに
この連載ではUNIX系OSなどで使われるスレッド「pthread」についてサンプルを交えて説明していきます。pthreadはPOSIXが仕様化したスレッドモデルです。サンプルはCと一部C++、調査環境はFedora 8(2.6.23.1-49.fc8)、32bit、glibc-4.1-2、gcc-4.1.2-33およびFedora Core 6(2.6.18-1.2798.fc6)、32bit、glibc-2.5-3、gcc-4.1.1-30を使用しています。
これまでの記事
- 第1回:pthreadについて(概要・生成)
- 第2回:pthreadについて(同期)
- 第3回:pthreadについて(条件変数・モデル)
- 第4回:pthreadについて(スレッド固有データ)
- 第5回:pthreadについて(スタックサイズ)
- 第6回:pthreadについて(スケジューリング)
- 第7回:pthreadについて(キャンセル)
11. シグナル
スレッドとシグナルはどちらも非同期処理で、基本的には同居させるべきではないと言われています。確かに注意するべきことはありますが、しかし気をつけて使うことでシグナルハンドラを使用する以上の利点を享受できます。
以前、筆者はsignalについてのレポートを作成していて、そこでシグナルとスレッドについて詳細な記述をしています。よって今回は説明をシンプルにまとめます。
11.1 受信
スレッドはあらゆる情報をプロセスと共有していますが、数点だけスレッド固有の情報があります。その中の一つに、シグナルマスク (pthread_sigmask(3))があります。
補足対象シグナルを、スレッド起動前にsigprocmaskでSIG_BLOCKし、スレッドでは同様のシグナルをpthread_sigmaskでSIG_BLOCKしておくことで、プロセスに対して送信されたシグナルをスレッドが補足することが可能です。
/* gcc signal_recv.c -o signal_recv -g -W -Wall -lpthread */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <pthread.h> #include <unistd.h> #include <signal.h> struct { int sigid; char * sigmess; } siginfo[] = { { SIGINT, "signal_func: 'SIGINT' Recv.", }, { SIGQUIT, "signal_func: 'SIGQUIT' Recv.", }, { SIGTERM, "signal_func: 'SIGTERM' Recv.", }, }; #define array( a ) ( sizeof( a )/sizeof( a[0] )) void * signal_func( void * arg ); void * worker_func( void * arg ); pthread_t id[2]; int end_flg = 0; int main( ) { sigset_t ss; size_t i; sigemptyset( &ss ); for( i = 0; i < array( siginfo ); i ++ ) { sigaddset( &ss, siginfo[i].sigid ); } sigprocmask( SIG_BLOCK, &ss, 0 ); pthread_create( &id[0], 0, &signal_func, 0 ); pthread_create( &id[1], 0, &worker_func, 0 ); pthread_join( id[0], 0 ); pthread_join( id[1], 0 ); return 0; } void * signal_func( void * arg ) { ( void )arg; sigset_t ss; int sig; size_t i; pthread_detach( pthread_self( )); sigemptyset( &ss ); for( i = 0; i < array( siginfo ); i ++ ) { sigaddset( &ss, siginfo[i].sigid ); } pthread_sigmask( SIG_BLOCK, &ss, 0 ); while( end_flg == 0 ) { if( sigwait( &ss, &sig )) { printf( "not SIGINT signal!!\n" ); continue; } for( i = 0; i < array( siginfo ); i ++ ) { if( siginfo[i].sigid != sig ) { continue; } printf( "%s (%2d) \n", siginfo[i].sigmess, sig ); break; } } return 0; } void * worker_func( void * arg ) { ( void )arg; int i = 0; for( i = 0; i < 10; i ++ ) { puts( "in worker_func..." ); sleep( 2 ); } end_flg = 1; pthread_kill( id[0], SIGTERM ); return 0; }
実行すると、定期的に"in worker_func..."が出力されます。画面からCtrl+C、Ctrl+\を入力すると、それに対応したシグナルのメッセージが現れます。また、Ctrl+Zで止めて、画面から"kill %"と入力すると、対応したシグナル(SIGTERM)のメッセージが現れます。
停止させるには、Ctrl+Zで止めてから画面で"kill -9 %"と入力するか、20秒程待ってください。
11.2 送信
また、pthread_killを使用することで、プロセスおよびスレッドから対象のスレッドに対してシグナルを送信するができます。pthread_kill は kill(2) と似ています。kill(2)同様、pthread_killも第二引数に0を設定することで、送信対象のスレッドの生存情報を得られます。
/* gcc signal_send.c -o signal_send -g -W -Wall -lpthread */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <pthread.h> #include <unistd.h> #include <signal.h> struct { int sigid; char * sigmess; } siginfo[] = { { SIGINT, "'SIGINT' Recv.", }, { SIGQUIT, "'SIGQUIT' Recv.", }, { SIGTERM, "'SIGTERM' Recv.", }, }; #define array( a ) ( sizeof( a )/sizeof( a[0] )) void * signal_func( void * arg ); int main( ) { pthread_t id; sigset_t ss; size_t i; int rtn; sigemptyset( &ss ); for( i = 0; i < array( siginfo ); i ++ ) { sigaddset( &ss, siginfo[i].sigid ); } sigprocmask( SIG_BLOCK, &ss, 0 ); pthread_create( &id, 0, &signal_func, ( void * )&i ); for( i = 0; i < array( siginfo ); i ++ ) { pthread_kill( id, siginfo[i].sigid ); sleep( 1 ); } rtn = pthread_kill( id, 0 ); printf( "pthread_kill:[%d][%s]\n", rtn, strerror( rtn )); sleep( 1 ); rtn = pthread_cancel( id ); printf( "pthread_cancel:[%d][%s]\n", rtn, strerror( rtn )); sleep( 1 ); rtn = pthread_kill( id, SIGINT ); printf( "pthread_kill:[%d][%s]\n", rtn, strerror( rtn )); return 0; } void * signal_func( void * arg ) { ( void )arg; sigset_t ss; int sig; size_t i; pthread_detach( pthread_self( )); sigemptyset( &ss ); for( i = 0; i < array( siginfo ); i ++ ) { sigaddset( &ss, siginfo[i].sigid ); } pthread_sigmask( SIG_BLOCK, &ss, 0 ); while( 1 ) { if( sigwait( &ss, &sig )) { printf( "not SIGINT signal!!\n" ); continue; } for( i = 0; i < array( siginfo ); i ++ ) { if( siginfo[i].sigid != sig ) { continue; } printf( "%s (%2d) \n", siginfo[i].sigmess, sig ); break; } } return 0; }
実行すると、プロセスからスレッドに対して一定間隔にシグナル(SIGINT、SIGQUIT、SIGTERM)が飛びます。その後スレッドの生存確認を行い、pthread_cancelでスレッドを殺し、再度スレッドの生存確認を行っています。
11.3 任意のタイミングでのスレッドの中断・続行
上記から、スレッドは特定のシグナルが配送されるのを待つことができますが、それを利用してスレッドの任意のタイミングでの一時停止(サスペンド)と再開(レジューム)を実装できます。sigwait/sigwaitifoで待ってもよいのですが、sigsuspendの方で実装しています。
特定のシグナル(サンプルではCtrl+CによるSIGINT)をシグナルハンドラで受信し、ハンドラからスレッドに対してpthread_killでシグナルを配送、スレッドは特定のシグナルを受信したらsigsuspendでサスペンドします。
もう一度対象シグナルが届いたらシグナルハンドラ経由でスレッドにシグナルを飛ばし、スレッドは自動でサスペンドから解放され、スレッドが再開される、という流れです。
プログラム(thread_susp.c)は200行ほどあるので、ダウンロードして使用してください。
guest $ ./thread_susp Thread 3: 15122 16695 18665 15185 d 2: 17938 14153 18015 thread_signal_handler:Path!! suspend start!! <--- Ctrl+C 押下 thread_signal_handler:Path!! resume start!! <--- Ctrl+C 押下 Thread 3: d 5: 24347 24817 25273 23228 24758 26006 19189 22684 thread_signal_handler:Path!! suspend start!! <--- Ctrl+C 押下 thread_signal_handler:Path!! resume start!! <--- Ctrl+C 押下 Thread 1 End!! 38532 40000 34817 31860 36293 36016 30908 38621 Thread 0 End!! 40000 34973 32474 36338 31006 39304 Thread 4 End!! 40000 36897 31362 39816 Thread 7 End!! 35161 37083 31385 40000 Thread 6: 93329 33701 39676 34950 thread_signal_handler:Path!! suspend start!! <--- Ctrl+C 押下 thread_signal_handler:Path!! resume start!! <--- Ctrl+C 押下 Thread 5 End!! 37761 40000 Thread 2 End!! 40000 35254 36985 Thread 6 End!! 39602 40000 Thread 3 End!! 40000 all thread was done... guest $
ただし注意が必要です。どんな時にサスペンドが発生するか分からないので、pthread_cancelの時も述べましたが、例えばmutex_lockされたままでサスペンドされることで他のスレッドの処理が進まないなどの弊害は容易に想像できます。
各スレッドに各クライアントが接続されている場合など、長時間にわたるサスペンドがそのまま性能劣化に繋がってしまうことも考えられます。使うときは全スレッドを一気にサスペンドし、すぐにレジュームするようにした方が良いと思います。
11.4 所感
シグナルと言えばハンドラで捕捉するのが通常だと思いますが、ハンドラ内でできることはかなり限られます。一方、スレッドでハンドラを作成すると自由度があがります。個人的には、シグナル受信処理はスレッドで実装した方が柔軟性もあって良いと思います。