シグナルハンドラを使わないでシグナルをハンドルする

「シグナルハンドラの中でできることは非常に限られているんですよ」というお話を1年半くらい前に書きましたが、この話には続きがあって、ある特定の条件下ではこの制限を緩和することができます。今回はその方法についての解説です。sigwait(3)という関数を使います。


※ この話、うっかり書き忘れていました。ちょっとしたきっかけで思い出したので、暇があるうちに書いておきます。

■「シグナルを待つ」処理 〜従来の方法〜


皆様、「シグナルの到着を待つ」処理を、次のように書いてしまっていないでしょうか?

// シグナルハンドラ
void handler(int signo) {
  // この中で使って良いのは非同期シグナルセーフ(async-signal-safe)な関数のみ
}

を用意して、

  sa.sa_handler = handler;
  sigaction(SIGHUP, &sa, NULL);

...

  while (1) {
    // シグナルの到着を待つ(寝ているのはCPUを使いすぎないようにするため)
    struct timespec ts = {1, 0};
    nanosleep(&ts, 0);
  }

と寝ながらシグナルを待つ(あるいは寝るかわりに、pause(2) か sigsuspend(3) を使ってシグナルの到着を待つ)といった方法です。


これ、間違った方法とまでは言いませんけど、handler関数の中では非同期シグナルセーフ関数しか使えないので、ちょっと窮屈です。たとえば、handlerの中では、mallocもprintf系関数も、strXXX関数も呼べません。「SIGHUPを受信したら設定ファイルを読み直す」なんて機能を実現したくなっても、mallocもsnprintfもナシでは少々キツいものがあります。

■「シグナルを待つ」処理 〜sigwaitを使う方法〜


上記のコード例をsigwaitを使って書き直すと次のようになります

  int signo;
  sigset_t ss;

...

  while (1) {
    if (sigwait(&ss, &signo) == 0) { // シグナルを待つ
      switch(signo) {
      case SIGHUP:
        // ...
      case SIGINT:
        // ...
      }
    } 
  }

この例では、シグナルを受信すると、シグナルハンドラに飛ぶのではなくてsigwait関数が0でリターンします(そのとき、signoには受信したシグナルの番号が入ります)。ですから、従来シグナルハンドラで行っていたような処理は、if文のブロックの中に書く形になります。ポイントは、sigwaitから戻った後の処理では、非同期シグナルセーフではない関数を呼んでも一向に構わないという点です。mallocでもsnprintfでも何でも、好きなように呼び出せます。これなら「設定ファイルの読み直し」もすぐ書けますよね。


このように、sigwait関数を使ってシグナルを処理することを、「非同期シグナルを同期的に扱う」と呼んだりもします。
完全なサンプルはこんな感じになります:

#include <signal.h>

int main() {
  sigset_t ss;
  int ret, signo;

  sigemptyset(&ss);
  ret = sigaddset(&ss, SIGHUP);
  if (ret != 0) 
    return 1;
  ret = sigprocmask(SIG_BLOCK, &ss, NULL);
  if (ret != 0) 
    return 1;

  // ...

  while(1) {
    if (sigwait(&ss, &signo) == 0) {
      /* handle SIGHUP */
    }
  }

  return 0;
}

sigwaitで処理したいシグナル(複数可)を、あらかじめsigprocmaskでブロックしておく形になります。なお、sigwait中にssに含まれていないシグナル(この例ではSIGHUP以外)が到着すると、通常のシグナル処理と同様、「シグナルハンドラが呼ばれる」「無視」「プロセス終了」などの動作になります。この時、処理系によってはsigwaitがEINTRで戻る場合がありますので注意してください。


詳しい方へ: /* handle SIGHUP */ と書かれた部分を処理中に再びSIGHUPが生起されたとしても、次にsigwaitが呼ばれるまでそのシグナルは配送されずに保留されます*1ので、POSIX以前のsignal関数のような「二重配送による誤動作」はいたしません。

■ イディオム (マルチスレッドでシグナル処理)


以前、「マルチスレッドとシグナルは混ぜるなキケン」と書いたことがあったような気がするんですが、次のようにすれば、マルチスレッドプログラムで比較的安全にシグナルを扱うことができます。
たとえば、外部からSIGHUPを送られたら何か処理をするプログラムを書く時は次のようにします:

  1. シグナル処理を一手に引き受けるスレッドをひとつ生成する(スレッドAと呼ぶ)
  2. スレッドAを含む全てのスレッドで、pthread_sigmask(3)を用いてSIGHUPをブロック(SIG_BLOCK)する
  3. スレッドAは、sigwaitを用いてシグナルの到着をひたすら待つ
  4. シグナルが届いたら、お好きなように処理をする
    • 外部からSIGHUPが送られると、それは必ずスレッドAに配送されます(POSIXでそう決まっています)
    • sigwaitが0で戻った後の処理では、前述の通り非同期シグナルセーフではない関数を呼べます。ですから、たとえばシグナルの到着をスレッドA以外に伝えるのに、条件変数でもなんでも好きなように使うことができます*2


もっと言うと、シグナルを扱わなければならないシングルスレッドのプログラムを、上記の形のマルチスレッドプログラムに書き換えてしまうのもアリじゃないか思います。シグナルハンドラなんてこの世から消えてしまえばいいんだ(暴言)。


スレッド間通信の道具としてシグナルを使ったりするのは、以前書いたとおりやめたほうがいいです。それは変わりません。

■ Note


sigwaitはわりと新しい関数でうまく動かない環境があるかもしれないことと、同一のシグナルに対してsigwaitとsigactionを併用した結果は(確か)未規定であることに注意してくださいませ。glibc-2.3.4だと、sigwaitが勝ってsigactionで指定したハンドラは呼ばれないみたいですね。


ところで、一番最初の「シグナルを寝て待つ」コードの解説にはちょっと嘘があります。本当のことを書くと、pauseまたはsigsuspendでシグナルを待つ例については、シグナルハンドラ内でどんな関数を呼んでも大丈夫です*3。なぜかというと、「シグナルに割り込まれた(async-signal-safeではない)関数をシグナルハンドラ内から再度呼び出す場合を除いて、POSIX全ての 関数は、シグナルハンドラから呼んでも規格に書かれたとおりに動く*4」と決まっているからです。規格の2.4.3の最後のほうですね。


でも…非同期にシグナルハンドラに飛ばされるより、同期的にsigwait()で処理するほうが、規格の2.4.3末尾の記述を知らない人を安心させることができるし、デバッグもきっと楽です。あーあと、pauseは競合状態を引き起こしやすいですし、sigsuspendはマルチスレッド環境での使用に難がある*5ので使わないほうが良いですね。そんな事情もあるんで、やっぱりsigwaitのほうが優れていると思います。

■ Links


規格以外だと、

が参考になります。Sunの方には、「sigwaitから戻った後、async-signal-safe ではない関数を呼んでも大丈夫だよ*6」と明示的に書かれていてgoodです。IBMのマニュアルには、コード例が付いています。


(追記 2005/2/18) sigtimedwaitとか、sigwaitinfoという高機能なvariantもあります。使い方は一緒*7なので、man pageを参照ください。

*1:sigprocmask(SIG_BLOCK, {SIGHUP});してますからね

*2:sem_post(3)のような煩雑な非同期シグナルセーフ関数を使わなくて済む

*3:pauseとsigsuspendは両方ともasync-signal-safeだから。nanosleepはsync-signal-safeではありませんから、nanosleep版については、ハンドラ内でnanosleepやnanosleepが内部で使っている関数を呼ぶとまずいです

*4:原文: In the presence of signals, all functions defined by this volume of IEEE Std 1003.1-2001 shall behave as defined when called from or interrupted by a signal-catching function, with a single exception: when a signal interrupts an unsafe function and the signal-catching function calls an unsafe function, the behavior is undefined.

*5:シグナルマスクのrestore関連

*6:原文: The signal-handling thread is not restricted to using Async-Signal-Safe functions and can synchronize with other threads in the usual way.

*7:sigwaitとは異なり、エラー通知がerrno経由な点には注意...