2014年10月17日金曜日

安全なシグナルハンドラの書き方


シグナルが送信された場合に、
OSは宛先となる稼働中のプロセスの処理に割り込むことができる。

(シグナル例)
SIGINT  : 割り込み(CTRL + C)
SIGTSTP : 実行中断(CTRL+Z)
SIGTERM : 強制終了(killコマンドのデフォルトの発生するシグナル)
SIGHUP  : 擬似端末クローズ時のハングアップ

シグナルを受信したプロセスにシグナルハンドラを登録しておけば、
シグナル受信時にそのルーチンを実行させることができる。
シグナルハンドラはプログラミングにおいて考慮すべき、
いわずもがなのの基本であるが、これを安全に書くのは難しい。


まずは、シグナルハンドラの基本的な使い方を復習する。
【シグナルグナルハンドラの基礎】

ただし種々の問題があることを知っておかなければならない。
【シグナルグナルハンドラの問題点】

その対策を最終章で検討する。
【sigwaitを使い、シグナル処理スレッドを使う】



【シググナルハンドラの基礎】
以下の方針で簡単なブログラムを書く。
● 標準入力から標準出力する
● 標準入力を待つ間はSIGINTを受け付ける
● 標準出力中はSIGINTを受け付けない
つまり、入力があれば出力してから終了する

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>

void handler(int no);

int main() {
  ssize_t ret;
  char buf[256];

  struct sigaction sa;
  sigset_t unblock_mask, block_mask;

  // シグナル受信時のハンドラ(呼び出す関数)関連の設定
  sa.sa_handler = handler;
  sa.sa_flags = SA_RESTART; 

  // シグナルマスク(ブロックするシグナルの集合)を初期化
  sigemptyset(&block_mask);

  // ブロックするシグナルをシグナルマスクとして設定(ここではSIGINT)
  // 対象となるシグナルセットするだけでまだブロックされない
  sigaddset(&block_mask, SIGINT);
  // 直接sigactionのmaskに設定もできるので以下でも可
  // sigaddset(&sa.sa_mask, SIGINT);

  // 設定したシグナルセットを有効化
  // マスクされたシグナルはブロック(正確には保留 ※1)される
  // 現在のマスク(未ブロックマスクセット)は第3引数に保存される
  sigprocmask(SIG_SETMASK, &block_mask, &unblock_mask);

  // 特定シグナル受信時の動作指示
  /* SIGINTは先にマスク(ブロック対象)にしているので、
    この段階ではSIGINTのシグナルハンドラは動作しない */
  sigaction(SIGINT, &sa, 0);


  // 標準入力を待つ間はSIGINTを受け付けるように変更
  // ブロックしないシグナルセットへ変更
  /* sigactionで設定したSIGINTシグナルを受信時に、
    指定したハンドラが呼ばれるようになる */
  sigprocmask(SIG_SETMASK, &unblock_mask, NULL);
  ret = read(0, buf, sizeof(buf));

  // 標準入力を待つ間はSIGINTを受け付けないように変更
  // ブロックするシグナルセットへ変更
  sigprocmask(SIG_SETMASK, &block_mask, &unblock_mask);

  // この間にCtrl+Cを送ってもハンドラは動作しない(確認のため意図的にsleep)
  sleep(10);
  write(1, buf, ret);

  return 0;
}


void handler(int num) {
  char *mes = "signal get\n";
  write( 1, mes, strlen(mes));
}
※1
シグナルをトリガにして時間のかかる処理をしている時に
シグナルをブロック中に、シグナルが複数回届いた場合は、
ブロックが解除されると通常は1回だけシグナルが送信される。



【シググナルハンドラの問題点】
大きく2つシグナルハンドラには問題がある。
● リエントラント問題
● 最適化問題

先のコードでprintfではなく、writeを使っていたことに気づいていただろうか。
printfは非同期シグナルセーフな関数ではない(リエントラント性が考慮されていない)ためだ。
シグナルハンドラからは非同期シグナルセーフな関数だけを呼ばなければならない。

mutexを使うコードにおいて、非同期シグナルセーフな関数を使ったためデッドロックする例
※mutexの有無に関わらずデッドロックすることはありえる。

最適化問題に関しては下のリンク参考になるだろう。
UNIX上でのC++ソフトウェア設計の定石 (2)



【sigwaitを使い、シグナル処理スレッドを使う】
非同期に発生するシグナルに対応するハンドルを書くのは難しい。
ただし、sigwaitという関数を利用すると、安全に、簡単に対応できる。

シグナルハンドラの問題点を解消するために、
適当なコードを書いてみる。

方針
● sigaction関数は使わない。
● メインスレッド、その他サブスレッドすべてで
   特定のシグナル(ここではSIGINT)をマスク(ブロック)する。
● 別途、特定のシグナルを受け持つ専用のスレッドを生成する。
● その専用スレッドでもシグナルはマスクするのだが、
   シグナルの到着はsigwaitを利用して、その瞬間だけマスクを解除させる。

sigwait以降の処理では、非同期シグナルセーフではない関数を呼び出しても
問題が発生しないというのが一番のメリットであろう。
ただし、スレッドを使うので、スレッドセーフであることには気をつけなくてはならない。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <signal.h>

void *signal_thread(void *arg);

int main() {
  // メインスレッド内で特定のシグナルをマスク(ブロック)
  sigset_t block_mask;
  sigemptyset(&block_mask);
  sigaddset(&block_mask, SIGINT);
  sigprocmask(SIG_SETMASK, &block_mask, NULL);

  // SIGINT処理専用スレッドを生成
  pthread_t pt;
  pthread_create(&pt, NULL, &signal_thread, NULL);
  pthread_detach(pt);

  // 実処理
  int cnt = 0;
  int max_cnt = 10;
  for(cnt; cnt < max_cnt; cnt++) {
    sleep(1) ;
    printf("cnt : %d\n", cnt );
  }
  return 0;
}


void *signal_thread(void *arg) {
  int sig;

  // 本スレッド内でマスク(ブロック)
  sigset_t block_mask;
  sigemptyset(&block_mask);
  sigaddset(&block_mask, SIGINT);
  // sigprocmask(SIG_SETMASK, &block_mask, NULL);

  while(1) {
    if(sigwait(&block_mask, &sig) == 0) {
      switch(sig) {
      case SIGINT:
        printf("SIGINT[%d] was called\n", sig );
        break;
      default:
        printf( "It's not SIGINT signal\n" );
        continue;
        break;
      }
    }
  }

  printf( "sigwait end!!\n" );
  return NULL;
}
※ビルド時はスレッドライブラリを読み込むこと。ex) gcc ***.c -lpthread



【触れなかった課題】
sigwaitを使いコードを書き直す際に、
最初に記載したreadしてwriteさせていたものではなく、
別のサンプルコードを利用したのには理由がある。

実はread、writeコードにはsigwaitでは解決できない問題が残っているのである。

read後はそれを必ずwriteするために、SIGINTをブロックする仕様だった。
しかし、write処理前のシグナルマスクを設定する前に、
シグナルがきたらハンドラが起動してしまう。
サンプル例ではハンドラが起動しても何もしないので影響はないが。

ブロックするシステムコールの直前なり直後のシグナル処理は難しいのである。
解決手段としてsigsafeを利用することができるようである。

本件で扱った内容と、sigsafeに関してはbinary hacksが参考になる。


0 件のコメント:

コメントを投稿