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.stop
と Thread#run
を使うのがいいかもしれません。これもシグナルハンドラ中で使っていいような気がします。なんとなく。
何かもっとうまい方法があれば教えてください。