Skip to content

Latest commit

 

History

History
260 lines (189 loc) · 13.7 KB

File metadata and controls

260 lines (189 loc) · 13.7 KB

シグナルとkill

さて、前回までで 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
end

papas.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シグナルを活用した子プロセスの非同期回収があります。

子プロセスが終了すると、親プロセスには自動的に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
end

forkして子プロセスを作ったあと、親プロセスも子プロセスもスリープし続けるものですね。バックグラウンドで実行します。

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

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

というわけで次回予告

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

see also

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