UNIX上でのC++ソフトウェア設計の定石 (1)

UNIXは、Windowsなどの“開発者に優しい”OSと比較すると、シグナルやスレッドの利用に関して制限事項が多いですが、それを知らずにアーキテクチャ設計および実装を行ってしまうケースが散見されます。これは、再現困難なバグの温床となるでしょう。


そこで、罠に嵌らないための「鉄則」を何回かに分けて書いてみようと思います。

鉄則1: シグナルの送受に依存しない設計にしよう

  • 他プロセスおよび自プロセスに対し、非同期シグナルを配送して処理の流れを変える設計はやめよう
    • 非同期シグナルとは、SIGUSR1・SIGUSR2・SIGINT・SIGTERM などの、killシステムコールによって生成・配送されるシグナルのこと
    • 単に無視(SIG_IGN)するのは問題なし
  • スレッドとシグナルの併用もやめよう

説明:


同期シグナルとは、SIGSEGV,SIGBUS,SIGPIPE,SIGSYS,SIGILL,SIGFPE のように、特定の命令の実行*1が原因で自プロセスに配送されるシグナルのことで、非同期シグナルとはそれ以外のシグナルです。非同期シグナルはいつ何時配送されてくるか予測ができません。シグナル受信時に特定の処理を行わせる「シグナルハンドラ」をプログラム中に記述することができます。シグナル受信によってそのシグナルハンドラに処理を分岐するプログラムは、「任意の場所で分岐処理が発生する」プログラムと言えます。このようなプログラムは次のような問題を孕みます:

  1. バグを埋めやすい。「任意の場所」とは、「C/C++上での単一命令の途中」も含まれますが、これはプログラマの思考の限界・暗黙の仮定を容易に飛び越えてしまいがちです。C++の例外による分岐より遥かに多い分岐パターンを考慮してプログラムを行う必要があります。
  2. テストケースが爆発するホワイトボックステストによってブランチカバレッジ100%を達成しても、シグナルによる分岐までは網羅できない。かといってシグナルによる分岐まで100%網羅するテストは全く現実的ではない。一般に、「コード上の特定箇所を実行中のシグナル受信による誤動作」というバグが良く発生する*2ことと併せて考えると、テスト困難性はソフトウェアの品質低下につながり得る。


経験上、「子プロセスの終了(SIGCHLD受信)を検出し、必要な処理を行う」というシグナル処理はどうしても必要になるケースがありますが、それ以外のシグナル処理、例えば

  • 自プロセスの状態変化をシグナルで他プロセスに伝える
  • メインスレッドが、入出力関数でブロックしているサブスレッドにシグナルを送出し、ブロックを解除する

などは、よくよく考えてから行うべきでしょう。前者であれば、横着せずに「普通の」プロセス間通信をすればよいかもしれませんし、後者は、折角スレッドを使用しているのですから、ブロックしても問題ないように再設計すべきかもしれません。


何れにせよ、もしシグナルを使わざるを得ないのならよくある落とし穴を全て*3把握しておくことと、マルチスレッドのソフトウェアを設計する場合と似た、あるいはそれよりも厳しい制約・注意事項がでてくる事を肝に銘じる事が必要でしょう。

*1:例えばヌルポインタ参照

*2:id:yupo5656:20040703 のsigsafeの記事を参照

*3:とりあえず「鉄則2」を :-)