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設定 / 最近の記事は一覧から

2004-07-03

[] 非同期シグナルと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()にリネームしてみました

giveupgiveup 2006/08/29 01:55 こんにちは、悪い例3についてですが
シグナルハンドラからsiglongjmp()を使うのは規格違反....
というところが気になります。
どこに規格違反とあるのかは探せてないのですが
setjmpとsigsetjmp、longjmpとsiglongjmpがどう違うのかが分からなくて
実験したところ
シグナルハンドラの中では、longjmpはダメだけどsiglongjmpは大丈夫なような気がしています。
どう、ダメかというと http://www.nminoru.jp/~nminoru/programming/stackoverflow_handling.html
の part4 の下の方にあったのですが、
longjmp使うとシグナルコンテクストから普通のコンテクストに戻ったときに、シグナルのマスク状態をひきずってて
再度シグナルを受けたときに、ハンドラに行かないようです。
siglongjmpだとこれがひきずらない様子でした。
sigprocmask(0, NULL, &sigset);
を、シグナルハンドラの中と外でやって確認しました。
sigaction(signo, NULL, &sa);
して、sa.sa_mask と sa.sa_handler
も見てみましたがこっちは変化なしでした。
linuxとcygwinでやってみましたが、同じように動いた様子でした。

なので結論は、シグナルハンドラの中でこそ、siglongjmpを使うものなのかと思いました。
それ以外の違いは分かっていません。

yupo5656yupo5656 2006/09/30 19:35 この頃わたしが書いた記事全般に言えることですが、一般論としての危険性を把握した上で、自分のコードや自分の環境にあわせて、原則から逸脱したコードを書くのは問題ないと思います。規格は、ものをあれこれ考えるときの便利な出発点というだけで、絶対的な結論ではないですからね。

で、siglongjmp()の件。この関数がasync-signal-safeではないとされているのは、ジャンプ先でsafeでない関数が呼ばれる可能性が高いというのが第一の(ある意味しょーもない)理由です。これは、SUSv3のRationaleに書いてあります。これ以外の理由(cygwin固有の事情)のほうは、すみません、昔のことすぎて忘れてしまいました。なんだったかな。今は問題ないのかもしれませんね。

非同期シグナルハンドラ内でのsiglongjmp()ですが、「関数Aであるmutexをlockしている最中にシグナルに割り込まれて、siglongjmpして、その先でまたAを呼んでデッドロック」みたいなことが起きなければ良いので、「適切にシグナルをmaskしてシグナルに割り込まれるタイミングを制御しきっている」とか、「ジャンプ先で危険な関数を一切呼ばないようにしている」とか、そんな対策をしてあるなら問題ないと思いますよ。まぁ、siglongjmpのソースコードも一応確認したほうがいいかな。。。

なお、nminoruさんのコード例は、同期シグナル(SIGSEGV)からの復帰ですので、この記事の非同期シグナルハンドラの話とは別になるかと。

SUSv3 Rationaleはここです。さっき、akrさんに紹介してもらいました。
http://www.opengroup.org/onlinepubs/009695399/xrat/xsh_chap02.html#tag_03_02_04_04

規格云々、async-signal-safe云々は、ここに記載があります。
http://d.hatena.ne.jp/yupo5656/20040712/p2

以上、回答になっているかよくわかりませんが、とりいそぎ。

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


画像認証

トラックバック - http://d.hatena.ne.jp/yupo5656/20040703/p3
Connection: close