シグナルハンドラからのforkするのは安全か? (2) シングルスレッドの場合 - サンプルコード
シングルスレッドのコードでシグナルハンドラ中でforkし、子プロセスが非同期シグナルセーフな関数を呼んでデッドロックする実例です。
非同期シグナルセーフな関数として a() を用意しました。この関数は入り口でmutexをロック、中で10秒寝て、mutexをアンロックして戻ります。
#include <sys/types.h> #include <time.h> #include <unistd.h> #include <signal.h> #include <pthread.h> #include <stdio.h> void a(void) { static pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER; const struct timespec t = {10, 0}; printf("enter a(), pid = %d\n", getpid()); fflush(stdout); pthread_mutex_lock(&m); // [a] ここでデッドロック! printf("befre sleep, pid = %d\n", getpid()); fflush(stdout); nanosleep(&t, 0); pthread_mutex_unlock(&m); printf("exit a(), pid = %d\n", getpid()); fflush(stdout); } void handler(int signo) { pid_t pid = fork(); /* fork is async-signal-safe */ if (pid == 0) { /* child */ a(); // [b] 非同期シグナルセーフ関数の呼び出し _exit(0); } return; } int main(void) { printf("hello, pid = %d\n", getpid()); signal(SIGHUP, handler); a(); return 0; }
このコードを、次のように実行してみます。親プロセスが動き始めてから約1秒後にSIGHUPを送り、処理をシグナルハンドラに分岐させるわけです。
$ ./a.out & ( sleep 1 ; kill -HUP $! ) [3] 5139 hello, pid = 5139 enter a(), pid = 5139 befre sleep, pid = 5139 enter a(), pid = 5142 exit a(), pid = 5139 (この後何も表示されず)
...見事に子プロセス5139は、a()のmutexをロックしにいったきり戻ってきません。デッドロックが発生しています。もうおわかりと思いますが、起きていることを時系列順に書くと次のようになっています。
- 親プロセス開始
- 親プロセス: a()に入る
- 親プロセス: a()のmutexをロック
- 親プロセス: a()のnanosleepを実行、寝る
- シェル: 親プロセスにSIGHUPを送信
- 親プロセス: SIGHUPを受信、シグナルハンドラへジャンプ
- 親プロセス: fork実行 (この後親プロセスはnanosleepに戻り、寝終わったらmutexをアンロックしてa()から抜け、main()から抜け、プロセスが終了する)
- 子プロセス: a()のmutexがLOCKEDの状態で誕生する
- 子プロセス: [b] の位置でa()を呼ぶ
- 子プロセス: [a] の位置で、LOCKED状態のmutexを再度ロックしにいき、デッドロックする
OKでしょうか?今回は現象を確実に起こすためにnanosleepという小技を用いましたが、「printf関数内のmutexがロックされている区間でシグナル受信してfork、子プロセスがprintfを呼ぶ」ケースでも全く同様の理由でデッドロックします。おっと、a()はprintfも呼んでいるじゃないか。これは悪い例です(笑)。