さて、前回までで fork とプロセスの関係についてはなんとなく概要が把握できたんじゃないかなと思います。今回は、シグナルについてです。
プロセスが外界とコミュニケーションを取るための方法として、ファイルディスクリプタを通じた入出力というものがあることは前回までで見てきたとおりです。じつは、プロセスが外界とコミュニケーションを取る方法としてもうひとつ、「シグナル」というものがあります。第二回で見たとおり、プロセスは生成されたあとは実行中となり、処理が終わるまでは一心不乱に決められた動きを行っています。しかしたとえば、無限ループに陥ってしまったプロセスなどは、外から「あっちょっと君、ストップ!ストップ!」という感じで止めてあげられる仕組みがないと困りますよね。そういう感じで、外からプロセスに対して「割り込み」を行うための仕組みが「シグナル」です。
なにはともあれ、ためしてみましょう。
まずはプロセスを作りましょう。
$ perl -e 'while(1){sleep}' &
$ ps
毎度おなじみ、sleep するだけの perl プロセスです。ps でpid を確認しておきましょう。
このプロセスに対して、シグナルを送ってみます。
$ kill -INT <さっき確認したpid>
kill というのが、プロセスに対してシグナルを送るコマンドです。今回は -INT を指定することで、「SIGINT」というシグナルを送ってみました。「SIGINT」の他にもいろんなシグナルがありますが、今は置いておきます。さて、ではここでもう一度 ps コマンドでプロセスの様子を見てみましょう。
$ ps
すると、さきほどまで存在していた perl プロセスが無くなっていることがわかると思います。これはいったいどうしたことでしょうか。実は、SIGINTというシグナルを受け取ると、デフォルト値ではそのプロセスは実行を停止するのです。sleep し続けていたプロセスに SIGINT というシグナルを送ったことによりプロセスに「割り込み」をして、そのプロセスの実行を止めてしまったわけですね。
さきほど、「デフォルト値では」と言いましたが、ということは、シグナルを受け取ったときの動作を変更することだってできるわけです。やってみましょうか。
# papas.pl
use strict;
use warnings;
# SIGINTを受け取ったときは sub {} の中身を実行する
$SIG{INT} = sub {
warn "ぬわーーーーっっ!!";
};
# スリープし続ける
while (1) {
sleep;
}
papas.pl という名前で上のようなスクリプトを作成して、バックグラウンドで実行してみましょう
$ perl papas.pl &
さて、それではこのプロセスに対して、SIGINTを送ってみましょう。
$ kill -INT <"perl papas.pl" の pid>
標準エラーに「ぬわーーーーっっ!!」が表示されたかと思います。そして再度 ps してみると、さっきは SIGINT を受け取って停止していたプロセスが、今回はまだ生きていることが見て取れるかと思います。これで、何度 SIGINT を送っても「ぬわーーーーっっ!!」と叫ぶだけで、死なないプロセスの完成です。パパスも適切にシグナル処理さえしていればゲマに殺されることもなかったというのに……。
さて、このままではこのプロセスは生き続けてしまうので、SIGTERMというシグナルを送信して適切に殺してあげましょう。
$ kill -TERM <"perl papas.pl" の pid>
これで無事にパパスは死にました。
上に見たように、シグナルには SIGINT 以外にもいろいろないろいろなシグナルがあります。man 7 signal や man kill に一度目を通しておくと良いでしょう。それぞれのシグナルに、受け取ったときのデフォルトの動作が定義されています。
とりあえずここでは、signal(7) から、 POSIX.1-1990 で規定されているシグナルの種類を引いておきましょう。
Signal Value Action Comment
-------------------------------------------------------------------------
SIGHUP 1 Term Hangup detected on controlling terminal
or death of controlling process
SIGINT 2 Term Interrupt from keyboard
SIGQUIT 3 Core Quit from keyboard
SIGILL 4 Core Illegal Instruction
SIGABRT 6 Core Abort signal from abort(3)
SIGFPE 8 Core Floating point exception
SIGKILL 9 Term Kill signal
SIGSEGV 11 Core Invalid memory reference
SIGPIPE 13 Term Broken pipe: write to pipe with no readers
SIGALRM 14 Term Timer signal from alarm(2)
SIGTERM 15 Term Termination signal
SIGUSR1 30,10,16 Term User-defined signal 1
SIGUSR2 31,12,17 Term User-defined signal 2
SIGCHLD 20,17,18 Ign Child stopped or terminated
SIGCONT 19,18,25 Cont Continue if stopped
SIGSTOP 17,19,23 Stop Stop process
SIGTSTP 18,20,24 Stop Stop typed at tty
SIGTTIN 21,21,26 Stop tty input for background process
SIGTTOU 22,22,27 Stop tty output for background process
Signal のところがシグナルの名前、Value というところがそのシグナルを表す番号(kill -n pid でプロセスにそのシグナルを送ることができます)、Action のところがそのシグナルを受け取ったときのデフォルトの動作です。Term ならばプロセスを終了し、Coreならばコアダンプを吐いて終了します。Ignならばそのシグナルを無視します(なにもしない)し、Stopならば実行を一時停止、Contならば一時停止していたプロセスを再開します。Commentのところに、どのようなときにそのシグナルが送られてくるかが書かれていますね。たとえば SIGCHLD を見てみると、Child stopped or terminatedと書かれています。つまり、子プロセスが止まったり止められたりしたときに、その親プロセスはSIGCHLDを受け取るようになっているわけですね。
微妙なハマりポイントとして、SIGHUP や SIGPIPE があるので、そこだけ少し説明しておきましょう。
まずは SIGHUP についてですが、ログインシェルが死んだときに、そのログインシェルが起動したプロセスにはSIGHUPが送られてきます(じつはこれは正確な説明ではないのだけれど、このあたりの正確な説明は次回できたらします)。これがなにを意味するかというと、たとえば ssh でサーバーにログインして、バックグラウンドでなにかを動かしたまま logout したりすると、そのバックグラウンドプロセスに SIGHUP が送られます。SIGHUP のデフォルトの動作は Term なので、そのバッググラウンドプロセスは死んでしまいます。これを防ぐためには、 nohup コマンドを使ってプロセスを起動するか、プロセス側で SIGHUP を受け取ったときの動作を変更する必要があります。
つぎに SIGPIPE についてです。SIGPIPEは、壊れた pipe に対して書き込みを行ったときに受信されるシグナルです。これが問題を引き起こすことが多いのが、ネットワークサーバーを書いているときです。なんらかのトラブルなどですでに切断されてしまっているソケットに対してなにかを書き込みしようとすると(いくらでもその理由は考えられます)、プロセスは SIGPIPE を受け取ります。SIGPIPE のデフォルトの動作はTermなので、この時点でサーバーは突然の死を迎えることになるわけです。
_人人人人人_
> SIGPIPE <
 ̄YYYYY ̄
動かし続けることを前提としたプロセスでは、このあたりのシグナルをきちんとハンドリングしてあげないとハマることが多いので、頭の片隅に置いておくといいかもしれません。
さて、シグナルについて基本的なことは見て来れたかと思います。では、forkなどと組み合わせて使った時にはどういう動きをするのでしょうか?見てみましょう。
まずは以下のようなスクリプトを用意してみます。
# fork_and_sleep.pl
use strict;
use warnings;
fork;
# スリープし続ける
while (1) {
sleep;
}
forkして子プロセスを作ったあと、親プロセスも子プロセスもスリープし続けるものですね。バックグラウンドで実行します。
$ perl fork_and_sleep.pl &
ps コマンドで様子を見てみましょう
$ ps f
PID TTY STAT TIME COMMAND
16753 pts/2 Ss 0:00 -bash
16891 pts/2 S 0:00 \_ perl fork_and_sleep.pl
16892 pts/2 S 0:00 | \_ perl fork_and_sleep.pl
16928 pts/2 R+ 0:00 \_ ps f
「f」を付けて ps を実行すると親子関係が一目でわかります。この場合は 16891 が親プロセス、16892 が子プロセスですね。では、fg でフォアグラウンドに戻して、Ctrl + C を押してみましょう。Ctrl+C は、プロセスに対してSIGINTを送信します。
OKですか? そうしたら、ここで再度 ps を実行してみましょう
$ ps f
PID TTY STAT TIME COMMAND
16753 pts/2 Ss 0:00 -bash
17140 pts/2 R+ 0:00 \_ ps f
子プロセスも一緒に消えていますね。では、今度は fg -> Ctrl+C のコンボではなく、kill -INT で SIGINT を送ってみましょう。
$ perl fork_and_sleep.pl &
$ ps f
PID TTY STAT TIME COMMAND
16753 pts/2 Ss 0:00 -bash
17288 pts/2 S 0:00 \_ perl fork_and_sleep.pl
17289 pts/2 S 0:00 | \_ perl fork_and_sleep.pl
17293 pts/2 R+ 0:00 \_ ps f
$ kill -INT 17288 # 親プロセスにSIGINTを送る
$ ps f
PID TTY STAT TIME COMMAND
16753 pts/2 Ss 0:00 -bash
17352 pts/2 R+ 0:00 \_ ps f
17289 pts/2 S 0:00 perl fork_and_sleep.pl
「!?」 今度は子プロセスが残っています(親プロセスが死んだからinitの子供になっており、ツリーの表示も変わっています)。
さて、なぜこのようなことが起こるのでしょうか。この挙動を理解するには、「プロセスグループ」という新しい概念を学ぶ必要があります。
次回はプロセスグループについて見てみましょう。多分次回が最終回!