Rubyのシグナルハンドラ

toRuby & guRuby 出張版 でシグナルについてやってたので、関連して書いてみます。

どのような時にシグナルハンドラを定義するのか

どのような時にシグナルハンドラを定義するのかという話がありました。

UNIXのデーモンプログラムは、何が由来なのかわかりませんが、SIGHUP で設定ファイルの再読み込みを行うのが慣習になっています。 SIGHUP はデフォルト動作ではプログラムを終了させてしまうだけなので、SIGHUP で特別な処理を行いたいプログラムはシグナルハンドラを定義しています。

本来 SIGHUP は端末が終了した時に端末上で動いていたプログラムに対してOSが発行するためのものです。

たとえば、端末エミュレータを開いて、

% sleep 9999

と実行してる状態で端末エミュレータを閉じると sleep プロセスに SIGHUP が送られます。別の端末から strace コマンドでそのプロセスを見てると SIGHUP で終了していることがわかります。

% sudo strace -p 26416
Process 26416 attached
restart_syscall(<... resuming interrupted call ...>) = ? ERESTART_RESTARTBLOCK (Interrupted by signal)
--- SIGHUP {si_signo=SIGHUP, si_code=SI_USER, si_pid=26390, si_uid=1000} ---
+++ killed by SIGHUP +++

コマンドを「&」つきでバックグランドで起動していても同じです。端末が終了してもプログラムを終了させないようにするには、SIGHUP を無視するか、端末からプロセスを切り離す必要があります。ちゃんと作られているデーモンプログラムは端末から切り離すように作ってあるはずです。Ruby だとプログラム中で

Process.daemon

とすれば、それ以降は端末から切り離されて動作するようになります。

SIGTERM は kill コマンドのデフォルトシグナルなので、プロセスを殺す時に一番良く使われます。プログラムによっては終了時に特別な処理を行わないと整合性が壊れてしまうものがあります。その場合は SIGTERM のシグナルハンドラを定義して、処理を行ってから終了するようにしておく必要があります。

SIGWINCH は、端末の大きさが変更された時にプロセスにそれを知らせるためのシグナルです。端末上で次のコマンドを実行してから、端末のサイズを変更すると「WINCH」と表示されます。

% ruby -e 'trap(:WINCH){puts "WINCH"}; sleep'

Vim や less 等の端末全体を使用するアプリは SIGWINCH が通知された時に画面の再描画を行うようになっています。

シグナルハンドラ内での処理

toRuby & guRuby の場でも言いましたが、シグナルハンドラ中ではあまり複雑なことはやってはいけないです。というよりもできることはかなり限られてると思います。

Ruby の場合、シグナルハンドラ中でどのような操作ができるのかについては、たぶんドキュメントはないんじゃないかと思うのですが、少なくとも Ruby 1.9.3 では IO 処理、たとえば上の例で使った puts も安全ではありません。シグナルの例を示すのに簡単なのでよく使われがちなのですが…。

たとえば Ruby 1.9.3 で次のコマンドを実行して端末サイズを変更するとプログラムが終了します。

% ruby -e 'trap(:WINCH){puts "WINCH"}; loop{puts "main"}'
...
main
main
main
main
-e:1:in `write': deadlock; recursive locking (ThreadError)
    from -e:1:in `puts'
    from -e:1:in `puts'
    from -e:1:in `block in <main>'
    from -e:1:in `call'
...

Ruby 2.0.0 以降では発生しないので、IO はシグナルに対して安全になったのかもしれません。

上の例では Ruby がデッドロックを検出して落ちてくれるのでいいのですが、自分で作ったライブラリを使用する場合はデータが壊れないように対応する必要があります。

Mutex を使って排他制御する方法はうまく動きません。

次のスクリプトは SIGINT を受けると、排他領域で「Interrupt!」と表示します。

mutex = Mutex.new

trap :INT do
  mutex.synchronize do
    puts 'Interrupt!'
    sleep 1
  end
end

sleep

SIGINT は端末上で Ctrl-C を入力するとフォアグランドプロセスに対して通知されるシグナルです。

このスクリプトを動かして Ctrl-C を押すとエラーになります。

% ruby test.rb
^Ctest.rb:4:in `synchronize': can't be called from trap context (ThreadError)
        from test.rb:4:in `block in <main>'
        from test.rb:10:in `call'
        from test.rb:10:in `sleep'
        from test.rb:10:in `<main>'

Mutex#synchronize はシグナルハンドラ中では許されない処理のようです。

なお、Ruby 1.9.3 では Ctrl-C を2回押すと別のエラーになります。

% ruby test.rb
^CInterrupt!
^C<internal:prelude>:8:in `lock': deadlock; recursive locking (ThreadError)
        from <internal:prelude>:8:in `synchronize'
        from test.rb:4:in `block in <main>'
        from test.rb:6:in `call'
        from test.rb:6:in `sleep'
        from test.rb:6:in `block (2 levels) in <main>'
        from <internal:prelude>:10:in `synchronize'
        from test.rb:4:in `block in <main>'
        from test.rb:10:in `call'
        from test.rb:10:in `sleep'
        from test.rb:10:in `<main>'

1.9.3 では Mutex#synchronize は禁止されていないようですが、Mutex#synchronize ブロック実行中にまたシグナルが発生して、同じ Mutex で排他制御しようとしてデッドロックになります。

次のように Mutex#synchronize ではなく、Mutex#try_lock を使用すると動くようです。(Ctrl-C は効かないので、プログラムを停止するには Ctrl-Z して kill %1 するか、他の端末から kill してください)

mutex = Mutex.new

trap :INT do
  if mutex.try_lock
    puts 'Interrupt!'
    sleep 1
    mutex.unlock
  end
end

sleep

これを試してて気がついたのですが、Ruby 1.9.3 と 2.x ではシグナルハンドラの実行のされかたが少し異なるようです。

1.9.3 ではシグナル発生の度にハンドラが実行されますが(上のスクリプトだと if の条件を満たさないので無視される)、2.x ではハンドラ実行中に発生したシグナルは溜められていて、ハンドラブロックを抜けると通知されるようです。

上のようにハンドラ内でも Mutex#try_lock は使えるようですが、これは排他が獲得できなければシグナルを無視してしまうことになるので、あまりよくありません。

結局シグナルハンドラ内で何か複雑な処理を行おうとすることが間違いなのだと思います。

シグナルハンドラ内ではシグナルが発生したことを表すフラグを立てるくらいにしておき、プログラムの都合のいいタイミングでそのフラグを見てシグナルに応じた処理を行うのがいいと思います。

mutex = Mutex.new
sigint = false

Thread.new do
  loop do
    if sigint
      sigint = false
      mutex.synchronize do
        puts 'Interrupt!'
        sleep 1
      end
    end
    sleep 0.1
  end
end

trap :INT do
  sigint = true
end

sleep

上のプログラムは、シグナルハンドラ内では sigint 変数の値を設定しているだけで、実際の処理はスレッドの中で行っています。

この例のままだとシグナル処理中にシグナルが連続発生しても1回分しか処理されないとか、sleep 0.1 してるものの、フラグチェックがビジーループっぽいところがいまいちかもしれません。

シグナルハンドラ中で Array#push して、シグナル処理時に Array#pop すれば回数の問題はクリアできるような気がします。Array は Ruby 標準で C で書かれたクラスなのでシグナルハンドラ中で使ってもアトミックに処理できるんじゃないかと思います。

ビジーループの方は Thread.stopThread#run を使うのがいいかもしれません。これもシグナルハンドラ中で使っていいような気がします。なんとなく。

何かもっとうまい方法があれば教えてください。