非同期シグナルとanti-pattern

sigsafeというUNIX向けの小さなライブラリがあります。これは、「シグナルを正確に取り扱うのは非常に難しい、俺たちのライブラリを使うと随分楽になるよ」といった趣旨の、アセンブラとCで書かれたライブラリで、それなりの種類のOS、CPUがサポートされています。


このライブラリ自体の使い方、使い勝手、あるいはどう実装されているかについてはまだ把握しきれていないのですが、ドキュメントに有用な部分があるのでご紹介します。 http://www.slamb.org/projects/sigsafe/api/patternref.html です。非同期シグナルを使用したコーディングを行う際の、一種のアンチパターン集になっています。


悪い例1: signal safe ではない関数をシグナルハンドラから呼んでしまう

 void unsafe_sighandler_a(int signum) {
     printf("Received signal %d?n", signum);
 }
 void unsafe_sighandler_b(int signum) {
     mylist->tail = (struct mylist*) malloc(sizeof(mylist));
     ...
 }

シグナルハンドラでは、SUSv3で"signal safe"と定義された関数以外は呼んではいけません。上のようにprintfやmallocを呼ぶと、デッドロックなどの不可解な現象に悩まされる事になります*1


悪い例2:

 volatile sig_atomic_t signal_received;

 void sighandler(int) { signal_received++; }

 ...

 int retval;
 do {
     if (signal_received) { handle_sig(); }
     // <<here>>
 } while ( (retval = syscall()) == -1 && errno == EINTR ) ;

この例ではシグナル受信をグローバル変数で判定していますが、もし<<here>>と書かれた場所でシグナルを受けたらどうなるでしょうか? syscall() はEINTRで戻らないので、handle_sig() 関数の実行が予期したようなタイミングでは行われなくなることでしょう*2グローバル変数の型を volatile sig_atomic_t にしたところまでは模範的だったのですが...おしいっ。


悪い例3:

 volatile sig_atomic_t signal_received, jump_is_safe;
 sigjmp_buf env;

 void sighandler(int) {
     signal_received++;
     if (jump_is_safe)
         siglongjmp(env, 1);
 }

 ...

 sigsetjmp(env, 1);
 jump_is_safe = 1;
 if (!signal_received) {
     retval = syscall();
 }
 jump_is_safe = 0;

この例では、シグナルを受信するとsiglongjmpでsyscall()から脱出しようとしています。しかし、

  • シグナルハンドラからsiglongjmp()を使うのは規格違反です。例えば、Solaris, Linux ではとりあえず動きますが、Cygwinでは動きません
  • syscall()を中断してしまった場合に、その後再度syscall()を呼んで大丈夫かどうかわかりません
  • sigsetjmp後jump_is_safeを設定する間、jump_is_safeを設定してからsignal_receivedを検査する間などにレースコンディションがあります


悪い例4:

 sigset_t blocked, unblocked;
 int retval;

 pthread_sigmask(SIG_SETMASK, &blocked, NULL);

 ...

 while ((retval = pselect(..., &unblocked)) == -1 && errno == EINTR) {
     printf("Signal received.");
 }

 ...

pselect(2)というシステムコールがあります。このシステムコールは、処理中はシグナルマスクを引数で指定されたものに置き換えます。上の例で、例えば blocked にSIGHUPを、unblocked には空集合を指定してみます。すると、

  1. pselectの処理中にSIGHUPを受けたなら EINTR で戻る
  2. pselectの処理前、あるいは処理完了後にSIGHUPを受けたならシグナルがブロックされているのでシグナル受信処理がpendingされる

となります。上記のコードに問題はありません。


しかし、一部の実装(とくにLinux)には問題があり、pselectがおおよそ次のように実装されてしまっています。

int pselect(int   n,   fd_set   *readfds,  fd_set  *writefds,  fd_set *exceptfds, 
                      const struct timespec *timeout, const sigset_t *sigmask) {
  sigset_t old; int ret;
  pthread_sigmask(SIG_SETMASK, sigmask, &old);
  // (1)
  ret = select(n, readfds, writefds, exceptfds, timeout); // 本当はtimeoutの型変換が必要ですが略
  // (2)
  pthread_sigmask(SIG_SETMASK, &old, 0);
  return ret;
}

単に、pselectをselectのwrapperとして実装しているのです。この実装にはraceがあります。(1)や(2)の場所でSIGHUPを受信すると、EINTRが戻らず、単にシグナル受信を見逃す結果になるでしょう。


FedoraCore2のpselectのmanにも、"Since Linux today does not have a pselect() system call, the current glibc2 routine still contains this race." と注記がありますので、きっと未だ修正されていないのでしょう。


以上、4例をご紹介しました。件のページには他にも悪い例が載っているのでご参照ください。

*1:この件、詳細はそのうち書くつもりです

*2:2004/7/28 handle_signal()という関数はkernel内部でも使われているのでhandle_sig()にリネームしてみました