読者です 読者をやめる 読者になる 読者になる

百日半狂乱

Shut the fuck up and write some code!!

POSIX.1「signal(2)は古い、sigaction(2)を使え。」

Linux signal sigaction nm C システムコール

過去に何回かシグナルに関する話をしたが、今回と次回もシグナルの話にしようと思う.

ちょうどLinuxのシグナルまとめがホッテントリに上がっていたので、シグナルを飛ばす側の話はこちらを参照するか、より詳細にはsignal(7)のmanページを参照する.

ここでは、シグナルを捕捉する側の話をする.

主に移植性の問題で、signal(2)ではなくsigaction(2)を使うべきだが、sigaction(2)はそのままだと使いにくいので、ラッピングして使い易くして、ちょっとだけ遊んでみた.

signal(2)は使うな、sigaction(2)を使え。

POSIXがそう言っている.

signal() の動作は UNIX のバージョンにより異なる。 また、歴史的に見て Linux のバージョンによっても異なっている。 このシステムコールの使用は避け、 代わりに sigaction(2) を使用すること。

Man page of SIGNAL

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コマンドの出力結果の見方が書いてあったのでメモ.

Wikipedia:System.map

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)などと出力される.

*1:ちなみにこの本、gccプログラミングとかLinuxシステムの入門的な内容が非常にコンパクトにまとまっている.これからLinuxを触ろうという時には、最初の1章から4章を立ち読みするだけでも全然違うと思う.良書.

*2:シグナルハンドラでは基本的にスレッドセーフな関数以外は呼び出すべきでない.外部変数を用いるようなライブラリ関数には注意する.参考:非同期シグナルで安全な関数 (async-signal-safe functions)