さて、前回までで fork とプロセスの関係についてはなんとなく概要が把握できたんじゃないかなと思います。今回は、シグナルについてです。
プロセスが外界とコミュニケーションを取るための方法として、ファイルディスクリプタを通じた入出力というものがあることは前回までで見てきたとおりです。じつは、プロセスが外界とコミュニケーションを取る方法としてもうひとつ、「シグナル」というものがあります。第二回で見たとおり、プロセスは生成されたあとは実行中となり、処理が終わるまでは一心不乱に決められた動きを行っています。しかしたとえば、無限ループに陥ってしまったプロセスなどは、外から「あっちょっと君、ストップ!ストップ!」という感じで止めてあげられる仕組みがないと困りますよね。そういう感じで、外からプロセスに対して「割り込み」を行うための仕組みが「シグナル」です。
なにはともあれ、ためしてみましょう。
まずはプロセスを作りましょう。
$ ruby -e 'loop { sleep }' &
$ ps毎度おなじみ、sleep するだけの ruby プロセスです。ps でpid を確認しておきましょう。
このプロセスに対して、シグナルを送ってみます。
$ kill -INT <さっき確認したpid>kill というのが、プロセスに対してシグナルを送るコマンドです。今回は -INT を指定することで、「SIGINT」というシグナルを送ってみました。「SIGINT」の他にもいろんなシグナルがありますが、今は置いておきます。さて、ではここでもう一度 ps コマンドでプロセスの様子を見てみましょう。
$ psすると、さきほどまで存在していた ruby プロセスが無くなっていることがわかると思います。これはいったいどうしたことでしょうか。実は、SIGINTというシグナルを受け取ると、デフォルト値ではそのプロセスは実行を停止するのです。sleep し続けていたプロセスに SIGINT というシグナルを送ったことによりプロセスに「割り込み」をして、そのプロセスの実行を止めてしまったわけですね。
さきほど、「デフォルト値では」と言いましたが、ということは、シグナルを受け取ったときの動作を変更することだってできるわけです。やってみましょうか。
# papas.rb
# SIGINTへのカスタムハンドラーを設定するサンプル
# Signal.trap:指定したシグナルを受け取ったときの動作を定義
# SIGINTを受け取ったときはブロックの中身を実行(デフォルトの終了動作を上書き)
Signal.trap('INT') do
warn "ぬわーーーーっっ!!" # 標準エラー出力にメッセージ表示
end
# 無限ループでスリープし続ける(SIGINTでも終了しない)
loop do
sleep
endpapas.rb という名前で上のようなスクリプトを作成して、バックグラウンドで実行してみましょう
$ ruby papas.rb &さて、それではこのプロセスに対して、SIGINTを送ってみましょう。
$ kill -INT <"ruby papas.rb" の pid>標準エラーに「ぬわーーーーっっ!!」が表示されたかと思います。そして再度 ps してみると、さっきは SIGINT を受け取って停止していたプロセスが、今回はまだ生きていることが見て取れるかと思います。これで、何度 SIGINT を送っても「ぬわーーーーっっ!!」と叫ぶだけで、死なないプロセスの完成です。パパスも適切にシグナル処理さえしていればゲマに殺されることもなかったというのに……。
さて、このままではこのプロセスは生き続けてしまうので、SIGTERMというシグナルを送信して適切に殺してあげましょう。
$ kill -TERM <"ruby papas.rb" の 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を受け取るようになっているわけですね。
前章で学んだゾンビプロセスの問題を解決する実践的な方法として、SIGCHLDシグナルを活用した子プロセスの非同期回収があります。
子プロセスが終了すると、親プロセスには自動的にSIGCHLDシグナルが送られます。このシグナルをキャッチして適切にwaitを行うことで、ゾンビプロセスの発生を防ぐことができます:
# SIGCHLDハンドラーによる子プロセス回収のサンプル
Signal.trap('CHLD') do
# 複数の子プロセスが同時に終了する場合に備えてループで回収
loop do
begin
# waitpid(-1, Process::WNOHANG):ノンブロッキングで任意の子プロセスをwait
# 子プロセスがなければ即座にnilを返す
pid = Process.waitpid(-1, Process::WNOHANG)
break unless pid # 回収すべき子プロセスがなければ終了
puts "Child process #{pid} has been reaped"
rescue Errno::ECHILD
# 子プロセスが存在しない場合の例外をキャッチ
break
end
end
end
# 複数の子プロセスを生成
5.times do
if fork
# 親プロセス:何もしない(SIGCHLDハンドラーが子を回収)
else
# 子プロセス:ランダムな時間後に終了
sleep rand(3)
exit
end
end
# 親プロセスは他の作業を継続
puts "Parent process continues working..."
sleep 10この手法により、親プロセスは子プロセスの終了を明示的に待つ必要がなく、子プロセスが終了した瞬間に自動的に回収が行われます。特にWebサーバーのようにリクエストごとに子プロセスを生成するアプリケーションでは、この仕組みが重要になります。
微妙なハマりポイントとして、SIGHUP や SIGPIPE があるので、そこだけ少し説明しておきましょう。
まずは SIGHUP についてですが、ログインシェルが死んだときに、そのログインシェルが起動したプロセスにはSIGHUPが送られてきます(じつはこれは正確な説明ではないのだけれど、このあたりの正確な説明は次回できたらします)。これがなにを意味するかというと、たとえば ssh でサーバーにログインして、バックグラウンドでなにかを動かしたまま logout したりすると、そのバックグラウンドプロセスに SIGHUP が送られます。SIGHUP のデフォルトの動作は Term なので、そのバッググラウンドプロセスは死んでしまいます。これを防ぐためには、 nohup コマンドを使ってプロセスを起動するか、プロセス側で SIGHUP を受け取ったときの動作を変更する必要があります。
つぎに SIGPIPE についてです。SIGPIPEは、壊れた pipe に対して書き込みを行ったときに受信されるシグナルです。これが問題を引き起こすことが多いのが、ネットワークサーバーを書いているときです。なんらかのトラブルなどですでに切断されてしまっているソケットに対してなにかを書き込みしようとすると(いくらでもその理由は考えられます)、プロセスは SIGPIPE を受け取ります。SIGPIPE のデフォルトの動作はTermなので、この時点でサーバーは突然の死を迎えることになるわけです。
_人人人人人_
> SIGPIPE <
 ̄YYYYY ̄
動かし続けることを前提としたプロセスでは、このあたりのシグナルをきちんとハンドリングしてあげないとハマることが多いので、頭の片隅に置いておくといいかもしれません。
さて、シグナルについて基本的なことは見て来れたかと思います。では、forkなどと組み合わせて使った時にはどういう動きをするのでしょうか?見てみましょう。
まずは以下のようなスクリプトを用意してみます。
# fork_and_sleep.rb
# プロセスグループの動作を確認するためのサンプル
# forkで子プロセスを作成(親と子両方が続きを実行)
fork
# 親プロセスも子プロセスも無限ループでスリープ
loop do
sleep
endforkして子プロセスを作ったあと、親プロセスも子プロセスもスリープし続けるものですね。バックグラウンドで実行します。
$ ruby fork_and_sleep.rb &ps コマンドで様子を見てみましょう
$ ps f PID TTY STAT TIME COMMAND
16753 pts/2 Ss 0:00 -bash
16891 pts/2 S 0:00 \_ ruby fork_and_sleep.rb
16892 pts/2 S 0:00 | \_ ruby fork_and_sleep.rb
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 を送ってみましょう。
$ ruby fork_and_sleep.rb &
$ ps f PID TTY STAT TIME COMMAND
16753 pts/2 Ss 0:00 -bash
17288 pts/2 S 0:00 \_ ruby fork_and_sleep.rb
17289 pts/2 S 0:00 | \_ ruby fork_and_sleep.rb
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 ruby fork_and_sleep.rb
「!?」 今度は子プロセスが残っています(親プロセスが死んだからinitの子供になっており、ツリーの表示も変わっています)。
さて、なぜこのようなことが起こるのでしょうか。この挙動を理解するには、「プロセスグループ」という新しい概念を学ぶ必要があります。
次回はプロセスグループについて見てみましょう。多分次回が最終回!