シグナルとkill

さて、前回までで 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の子供になっており、ツリーの表示も変わっています)。

さて、なぜこのようなことが起こるのでしょうか。この挙動を理解するには、「プロセスグループ」という新しい概念を学ぶ必要があります。

というわけで次回予告

次回はプロセスグループについて見てみましょう。多分次回が最終回!

see also

Perl Hackers Hub 第6回 UNIXプログラミングの勘所(3)