シグナルについて

2006/02/17:読み直すと変な部分があったので、全体的に修正を行った。

signal()とsigaction()について

シグナルハンドラを登録する関数には signal() と sigaction()という二つの関数がある。以下のふたつの理由によりsigaction()を使用すべきである。

  • signal()で登録したシグナルハンドラの挙動は環境によって異なる。(BSD系とSYSTEM V系で異なる)
  • signal()で登録したシグナルハンドラの挙動は SYSTEM V系の場合、安全ではない。(詳細は省略)

また、sigaction()の方が機能が豊富である。

シグナルマスクについて

シグナルマスクという機能がある。これは特定のシグナルを一定期間、ブロックする機能である。
これには、sigprocmask()、もしくは pthread_sigmask()を使用する。pthread を使用したマルチスレッドプログラムを書いている場合には、pthread_sigmask()を使用する。

この機能はマルチスレッド環境に置ける mutex と似ている。つまり、シグナルハンドラが起動してはまずいコードを保護するために使用する。

例えば、次の様な場合を考えてみる。

  • program は、一時ファイルを使用している。
  • 一時ファイルを管理するためにリンクリストを使用している。
  • SIGTERM や SIGINTを受け取るとsignal handlerはlinklistをたどって一時ファイルを削除し、終了するものとする。
 char* tmp_filename;/* 一時ファイルの名前 */
 
 /* (A) */
 /* 一時ファイル作成 */
 tmp_filename = make_tmpfile();

 /* (B) */

 /* リンクリストへの挿入*/
 linklist_insert(linklist, tmp_filename);
 /* (C) */

一時ファイルを作成する部分のコードが上記のようになっているとする。その場合には、(B)の部分でシグナルハンドラを受け取ると、新しく作成した一時ファイルはリンクリストに登録されていないため、削除されない。

その解決策のひとつとしてシグナルマスクが利用できる。(A) の部分から (C) の部分までをシグナルマスクを使用してシグナルをブロックすることで (B) の部分でシグナルハンドラが起動しないことを保証することが可能である。ブロックしている間に配送されたシグナルはブロックが解除された際に配送される。ブロックしている間に同種のシグナルが複数回配送された場合には、ブロックを解除した際には一度だけシグナルを受信する。

2種類のシグナル

UNIX のシグナルは以下の二種類にわけて考えることが可能である。

  • 非同期的なシグナル
  • 同期的なシグナル
非同期的なシグナル

この種類のシグナルには、SIGINT, SIGTERM などがある。SIGALRMなども非同期的なシグナルに分類される。

非同期なシグナルの特徴をあげる。

  • いつ発生するかわからない。
  • マルチスレッドプログラムの場合には、動作しているいずれかのスレッドに配信される。
  • シグナルマスクによってブロックすることが可能である。

また、この種類のシグナルを処理するシグナルハンドラでは、ほとんどの関数が使用することが安全でないとされている。シグナルハンドラ内で安全に使用できる関数は POSIX で規定されている*1

シグナルハンドラ内での処理は非常に限られたことしかできない。そのため、フラグを立てるなどの必要最低限のことをのみを行い、主処理はシグナルハンドラ外で行うことが多い。

同期的なシグナル

この種類のシグナルには SIGSEGV, SIGFPE などが分類される。この種類のシグナルの場合には、発生の原因がプログラム自身にある。例えば、NULL ポインタの指す先への値の書き込みなどを行うと、SIGSEGVが発生する。

同期的なシグナルの特徴をあげる。

  • いつ発生するかわかる。
  • マルチスレッドアプリケーションの場合には、配送される原因となった処理を行ったスレッドにシグナルが配送される。
  • シグナルマスクによってブロックすることが不可能である。

また、この種類のシグナルで致命的なものは、無視できずシグナルハンドラが何もせずに返ると再び同じシグナルが発生する。*2
尚、私はこの種類のシグナルをまともに処理しているプログラムを見たことがない。*3

sigwait()-マルチスレッドアプリケーションでの非同期的なシグナルの処理

マルチスレッドプログラムの非同期的なシグナルのハンドリングは難しい。なぜなら mutex 等 pthread の同期関数がシグナルハンドラの中で安全に使用することができないからである。)

理由を想像すると、シグナルハンドラが動作し、mutex をロックしようとした際に、その mutex がどのスレッドでロックされているかわからないため、同じスレッドで二重にロックしてしまう可能性があるためではないかと思う。また、仮に、どのスレッドでロックしているかわかったとしても、シグナルハンドラが呼ばれたスレッドでロックしていた場合には、mutex で保護しなければならないコードに対して非同期に割り込んでいるため、mutexが必要な処理が満足に行えないと考える。
そのため、シグナル処理をシグナルハンドラ以外で実行するという方法が有用である。*4

尚、同期的なシグナルの場合は、シングルスレッドアプリケーションと同じ処理方法で問題ない*5

マルチスレッドプログラムで非同期シグナルを安全に扱うために、sigwait()*6がある。この関数は指定したシグナルが配送されるまで待機する関数である。

この関数の利用方法は、シグナル処理を行うためのスレッドを作成し、そこでこの関数呼ぶ。そして、関数が返ってきたらシグナル処理を行う。その場合は、シグナルハンドラ内ではないため、mutex などの pthread の同期関数を使用することが可能である。

尚、この関数を使用する際の注意点がある。
まず、第一に sigwait() で処理するシグナルをすべてのスレッドにてシグナルマスクを使用してブロックしなくてはならないという点である。
これにはふたつの理由がある。

  • シグナル処理専用スレッド以外のスレッドにシグナルが配送されることを防ぐ
  • シグナル処理専用スレッドで sigwait() で待機いない時にシグナルが配送された場合に、シグナルハンドラが起動することを防ぐ。そのため、sigwait() は実行中に、指定したシグナルのをブロックを解除し、返る前にはブロックした状態に戻す。

第二に、シグナルハンドラに SIG_IGN を指定していると動作しないという点である。

尚、同期的なシグナルは sigwait() を使って処理することはできない。

sigwait()は下記のように利用する。SIGINT を処理することを想定している。

sigset_t mask;

int
main(void)
{
    /* シグナルをブロック
       シグナルマスクの設定は、スレッドを作成する際に引き継がれるため、
       スレッドを作成する前にシグナルをブロックするのが楽である。*/
    sigemptyset(&mask);
    sigaddset(&mask, SIGINT);

    pthread_sigmask(SIG_BLOCK, &mask, NULL);

    /* 本当はもっと引数は多い。*/
    create_thread(signal_thread_func)
    /* このスレッドはデタッチした方がいいかも */
    .
    .
    .
    return 0;
}

void* signal_thread_func(void* data)
{
    int signo;

    sigwait(&mask, &signo);

    /* ここで signal 処理をする */
}

尚、この方法は、linuxthreads*7 を利用した Linux 環境ではうまく動作しない。。それは kill コマンドなどでシグナルを送るとメインスレッド*8のみにしかシグナルが配送されずに、シグナル処理用のスレッドにシグナルが届かない*9。linuxthreadsを利用した環境の場合は シグナル処理スレッドのPID*10を指定してkillしなければ、シグナル処理用のスレッドにシグナルが届かない。

解決策としてパイプをつかって疑似sigwait()を作るアイデアがある。*11
シグナルハンドラ内でパイプに書き込みを行い、シグナル処理用のスレッドでは、パイプからから常に読み込みを行うという方法である。この方法では、どのスレッドでシグナルを受け取ってもシグナル処理用のスレッドでシグナルを処理できるため、linuxthreads 環境の Linux でも動作すると思う。ただし、ターミナルから CTRL+C を押して SIGINT を送る場合などは、プロセスグループに対してシグナルが配信されるため、すべてのスレッドでシグナルを受け取ることとなる。つまり、スレッドの数だけ同じシグナルを受け取ることになる。

尚、linuxthreads を利用した環境では、sigwait()中にsigwait()で待っていないシグナルに関して、ブロックされてしまうという不具合もある。linuxthreadsの環境でのsigwait()の利用は避けた方が無難だと思う。

*1:_Exit() _exit() abort() accept() access() aio_error() aio_return() aio_suspend() alarm() bind() cfgetispeed() cfgetospeed() cfsetispeed() cfsetospeed() chdir() chmod() chown() clock_gettime() close() connect() creat() dup() dup2() execle() execve() fchmod() fchown() fcntl() fdatasync() fork() fpathconf() fstat() fsync() ftruncate() getegid() geteuid() getgid() getgroups() getpeername() getpgrp() getpid() getppid() getsockname() getsockopt() getuid() kill() link() listen() lseek() lstat() mkdir() mkfifo() open() pathconf() pause() pipe() poll() posix_trace_event() pselect() raise() read() readlink() recv() recvfrom() recvmsg() rename() rmdir() select() sem_post() send() sendmsg() sendto() setgid() setpgid() setsid() setsockopt() setuid() shutdown() sigaction() sigaddset() sigdelset() sigemptyset() sigfillset() sigismember() signal() sigpause() sigpending() sigprocmask() sigqueue() sigset() sigsuspend() sleep() socket() socketpair() stat() symlink() sysconf() tcdrain() tcflow() tcflush() tcgetattr() cgetpgrp() tcsendbreak() tcsetattr() tcsetpgrp() time() timer_getoverrun() timer_gettime() timer_settime() times() umask() uname() unlink() utime() wait() waitpid() write()

*2:実装による。Linux で SEGV を無視すると無限ループとなった。

*3:基本的にプログラミングエラーにより発生するシグナルであるため、シグナルを処理するよりもその原因となるプログラミングエラーを除去するべきである。尚、http://www.nminoru.jp/~nminoru/programming/stackoverflow_handling.html にてスタックオーバーフローに対する対処で SIGSEGV を捕捉している

*4:マルチスレッドプログラムに限った話ではないが。

*5:ここでは同期的なシグナルの処理方法については述べていないが

*6:sigwaitinfo(),sigtimedwait()というより高機能な関数も存在する。

*7:NPTLじゃないやつ

*8:メインスレッドっていう表現は可笑しい気がする。要ははじめに main()がされたスレッド

*9:メインスレッドをシグナル処理用に利用すれば良いのかもしれない。主処理はスレッドを新しく作ってそちらで行う。

*10:linuxthredsでは各スレッドでPIDが異なる

*11:実際試してない。