シグナルハンドラからの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をロックしにいったきり戻ってきません。デッドロックが発生しています。もうおわかりと思いますが、起きていることを時系列順に書くと次のようになっています。

  1. 親プロセス開始
  2. 親プロセス: a()に入る
  3. 親プロセス: a()のmutexをロック
  4. 親プロセス: a()のnanosleepを実行、寝る
  5. シェル: 親プロセスにSIGHUPを送信
  6. 親プロセス: SIGHUPを受信、シグナルハンドラへジャンプ
  7. 親プロセス: fork実行 (この後親プロセスはnanosleepに戻り、寝終わったらmutexをアンロックしてa()から抜け、main()から抜け、プロセスが終了する)
  8. 子プロセス: a()のmutexがLOCKEDの状態で誕生する
  9. 子プロセス: [b] の位置でa()を呼ぶ
  10. 子プロセス: [a] の位置で、LOCKED状態のmutexを再度ロックしにいき、デッドロックする

OKでしょうか?今回は現象を確実に起こすためにnanosleepという小技を用いましたが、「printf関数内のmutexがロックされている区間でシグナル受信してfork、子プロセスがprintfを呼ぶ」ケースでも全く同様の理由でデッドロックします。おっと、a()はprintfも呼んでいるじゃないか。これは悪い例です(笑)。