シグナルハンドラからの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も呼んでいるじゃないか。これは悪い例です(笑)。