POSIX.1「signal(2)は古い、sigaction(2)を使え。」
過去に何回かシグナルに関する話をしたが、今回と次回もシグナルの話にしようと思う.
シグナルそのものについてはsignal(7)のmanページを参照する.
ここでは、シグナルを捕捉する側の話をする.
主に移植性の問題で、signal(2)ではなくsigaction(2)を使うべきだが、sigaction(2)はそのままだと使いにくいので、ラッピングして使い易くして、ちょっとだけ遊んでみた.
signal(2)は使うな、sigaction(2)を使え。
POSIXがそう言っている.
signal() の動作は UNIX のバージョンにより異なる。 また、歴史的に見て Linux のバージョンによっても異なっている。 このシステムコールの使用は避け、 代わりに sigaction(2) を使用すること。
sigaction(2)のラッパー関数
Gistにサンプルコードを上げた.
Gist:sigaction(2)のラッパー関数Signal
Gistに上げたコードは、書籍:unpv12eと、書籍:ふつうのLinuxプログラミングを大いに参考にしている.
過去に書いた独自に定義したエラーメッセージ関数を使っているが、目障りならsigaction(2)の戻り値が0以下(エラー)ならNULLを返すようにして、エラーを呼び出し元で対処するようにしても良い.
とりあえずコンパイルして動作するように、main関数を含めてごちゃごちゃ書いているが、注目して欲しいのは、以下の二箇所.
Sigfunc型の定義
typedef void Sigfunc(int); //何も返さない(void)関数を表す型として、Sigfuc型を定義 Sigfunc *Signal(int , Sigfunc *);
Sigfuncという名前自体は、勝手に付けた名前なので気にする必要が無いが、このtypedefによって結果的に、関数Signalの記述が簡素化される.
ターミナルでman 2 signal
を叩くと同じようなtypedefを見ることができるので、signal(2)の実装もこうなっているっぽい.
ちなみに、これがないと、
void (*Signal(int signo, void (*func) (int))) (int);
となっていかにもごちゃごちゃした感じになる.
関数ポインタ
ところで、関数ポインタの実体は、各プロセスのテキスト領域に配置されている機械語列の先頭アドレスである.
関数ポインタについては、Wikipedia大先生に詳細をお任せするとして、ここではnmコマンドでシンボルテーブルを見てみることにする.
Cでは関数名を書けばそこから関数ポインタが取り出せる.
Gistのサンプルコードは、実行すると関数Signalのアドレスを出力するようにしているので、出力されたアドレスとシンボルテーブルに登録されたアドレスを見比べてみる.
$ gcc -Wall wrapper_sigaction.c $ ./a.out Signal:0x4009d8 <<--- printfの出力 ^CCtrl-C $ nm a.out 00000000004009d8 T Signal <<--- nmコマンド出力 ............ ... 省略 ... ............
確かに関数Signalがシンボルテーブルのテキスト領域(T)に登録されており、そのアドレスはprintfで出力したアドレスと一致する.
ちなみに、直接は関係ないけど、 カーネルのシンボルテーブルの説明の中でnmコマンドの出力結果の見方が書いてあったのでメモ.
sigactionのラッパーとなる関数Signalの定義
Sigfunc * Signal(int signo, Sigfunc *func) { struct sigaction act, oact; /* act:新しく定義する動作,oact:今までの動作 */ act.sa_handler = func; sigemptyset(&act.sa_mask); act.sa_flags = 0; if(signo == SIGALRM){ act.sa_flags |= SA_INTERRUPT; //例えばalarm(2)でI/Oのタイムアウトの管理をしたい場合は、実行中のシステムコールを中断する } else { act.sa_flags |= SA_RESTART; //シグナルに割り込まれたシステムコールを再起動する } if(signo == SIGCHLD){ act.sa_handler = SIG_IGN; /*シグナルを無視*/ act.sa_flags |= SA_NOCLDWAIT; /*子プロセスをゾンビプロセスにしない(Linux2.6以降での機能)*/ } Sigaction(signo, &act, &oact); //独自に定義したラッパー関数e_Sigactionを呼び出す、sigaction(2)が失敗するとexitする return (oact.sa_handler); /*以前のシグナルの設定を戻り値として返す*/ }
基本的には、SA_RESTAETを設定する.これでシグナルに割り込まれたシステムコールがエラーを起こすことなく再開するようになる.
また、SIGCHLDの扱いを上手く設定するとゾンビプロセスに関する悩みがなくなる.もう少しだけ詳しい話はGistに上げたコードにコメントを入れている.
signal(2)の実装上の問題や、sigaction(2)の機能についてもう少し突っ込んだ話はググるか「ふつうのLinuxプログラミング」*1を参照.
結果的に、実装した関数Signalは例えば以下のように呼び出せる.
void sigint_handler(int signum) { write(1, "Ctrl-C\n", strlen("Ctrl-C\n")); exit(0); } int main(int argc, char **argv) { Signal(SIGINT, sigint_handler); Signal(SIGCHLD, NULL); return 0; }
第二引数では、シグナルハンドラの関数ポインタか、SIG_DFL、SIG_IGNを指定することができる.
シグナルで遊んでみる
Gistのサンプルコードでは、SIGINTを捕捉した時に呼び出すシグナルハンドラで"Ctrl-C"と出力するようにしている*2.
手元の環境では実行した後、Ctrl+CでSIGINTシグナルを送付すると以下のようにハンドラが呼び出されていることが確認できる.
# Signal(SIGINT, sigint_handler); の場合 $ ./a.out Signal:0x4009d8 ^CCtrl-C
これに対して第2引数をSIG_DFLにすると、デフォルトの動作に戻る.
# Signal(SIGINT, SIG_DFL); の場合 $ ./a.out Signal:0x4009d8 ^C
独自のシグナルハンドラが呼び出されるわけではないので、Ctrl-Cという文字列は出力されない.
さらに、SIG_IGNにすると、シグナルが無視される.
Signal(SIGINT, SIG_IGN); の場合 $ ./a.out Signal:0x4009d8 ^C^C^C^C^C^C^C^C^C^C^C^C^C <--- Ctrl+Cを連打しても終了しない!!
一通り連打して飽きたらCtrl+\
(SIGQUIT)で終了させる.
あらゆるシグナルをSIG_IGNで無視するようにしたら?
SIGKILL、SIGSTOPは無視することは出来ない.
仮にSignal(SIGKILL, SIG_IGN);
やSignal(SIGSTOP, SIG_IGN);
とした場合、コンパイルは通るが実行時にsigaction(2)がエラーとなる.
Gistのサンプルコードだと、例えばSIGKILLに対してSIG_IGNを設定すると、Sat Feb 15 01:11:19 2014 4950 wrapper_sigaction.c Signal 68 error [-1]=sigaction(9,0x7fff1879aaf0,0x7fff1879aa50):Invalid argument Invalid argument(22)
などと出力される.

ふつうのLinuxプログラミング Linuxの仕組みから学べるgccプログラミングの王道
- 作者: 青木峰郎
- 出版社/メーカー: ソフトバンククリエイティブ
- 発売日: 2005/07/27
- メディア: 単行本
- 購入: 35人 クリック: 450回
- この商品を含むブログ (150件) を見る