memologue RSSフィード

書いている人

日記というよりは備忘録、ソフトウェア技術者の不定期メモ。あるいはバッドノウハウ集。プライベートで調査した細々した諸々のスナップショット。嘘が散りばめられています。ISO/IEC 14882(C++)とPOSIX, GCC, glibc, ELFの話ばかりで、WindowsやMacの話はありません。特に記載がなければLinux/x86とILP32が前提です。時間の経過と共に古い記事は埋もれてしまいます。検索エンジンから飛んできた場合は、ページ内検索をご利用いただくかgoogleキャッシュを閲覧してみてください。技術的な記事を書きためる場所として使っています。言及してもらえると喜びます。主要な記事の一覧を書いておきます:

にんきコンテンツ: [GCC] mainを一度も呼ばないばかりか蹂躙する / Binary2.0 Conference 2006 発表資料 / C++ で SICP / ついカッとなって実行バイナリにパッチ / hogetrace - 関数コールトレーサ

昔のPOSIX関係の記事: シグナルの送受に依存しない設計にしよう / シグナルハンドラで行ってよい処理を知ろう / マルチスレッドのプログラムでのforkはやめよう / スレッドの「非同期キャンセル」を行わない設計にしよう / スレッドの「遅延キャンセル」も出来る限り避けて通ろう / マルチスレッドプログラミングの「常識」を守ろう / C++でsynchronized methodを書くのは難しい / シグナルハンドラからのforkするのは安全か? / g++ の -fthreadsafe-statics ってオプション知ってます? / 非同期シグナルとanti-pattern / localtimeやstrtokは本当にスレッドセーフにできないのか / UNIXの規格について / マルチスレッドと共有変数 - volatile?なにそれ。 / type punning と strict aliasing / アセンブラで遊ぶ時に便利なgdb設定 / 最近の記事は一覧から

2006-01-14

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

「シグナルハンドラの中でできることは非常に限られているんですよ」というお話を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経由な点には注意...

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証