LoginSignup
10
8

More than 5 years have passed since last update.

シグナルハンドリング実験

Last updated at Posted at 2016-02-06

実験したバージョン1:

$ ruby -v
ruby 2.1.4p265 (2014-10-27 revision 48166) [x86_64-darwin14.0]

基本

1.rb
is_end = false

Signal.trap(:INT) { is_end = true }

until is_end
  puts 'start'
  sleep(1)
  puts 'end'
end
$ ruby 1.rb
loop start
loop end
loop start
^Cloop end

$ ruby 1.rb
loop start
^C^C^C^C^C^C^C^C^C^C^C^Cloop end

普通な感じです。

例えばPHPはカスタムハンドラを設定した場合pcntl_signal_dispatchを呼ばないと一向にシグナルが処理されませんが、そういったものを仕込む必要も無いようです。

puts仕込んでみる

2.rb
is_end = false

Signal.trap(:INT) do
  puts 'INT'
  is_end = true
end

until is_end
  puts 'loop start'
  sleep(1)
  puts 'loop end'
end
$ ruby 2.rb
loop start
^CINT
loop end

$ ruby 2.rb
loop start
^CINT
^CINT
^CINT
^CINT
^CINT
loop end

シグナルが送られただけ処理していることが分かります。

デフォルトのシグナル

Signal.trapでシグナルハンドラをセットしますが、これは1つしか指定できないようです。
元々セットされていたハンドラはSignal.trapの戻り値になります。

3.rb
h1 = Signal.trap(:INT) { puts 'INT1' }
h2 = Signal.trap(:INT) { puts 'INT2' }
h3 = Signal.trap(:INT, 'SIG_DFL')
h4 = Signal.trap(:INT, 'DEFAULT')

puts h1
puts h2
puts h3
puts h4

h2.call()
h3.call()
$ ruby 3.rb
DEFAULT
#<Proc:0x007fd0cb9db1f8@3.rb:2>
#<Proc:0x007fd0cb9db1a8@3.rb:3>
DEFAULT
INT1
INT2

初期値はDEFAULTで、Signal.trap(:INT, 'DEFAULT')(もしくはSIG_DFLを指定)すると戻せます。

シグナルハンドラに割り込んでみる

4.rb
is_end = false

Signal.trap(:INT) do
  puts 'handler1'
  sleep(1)
  is_end = true
end

h1 = Signal.trap(:INT) do
  puts 'handler2'
  sleep(1)
  Signal.trap(:INT, h1)
  # シグナルを再発火
  Process.kill(:INT, $$)
end

until is_end
  puts 'loop start'
  sleep(1)
  puts 'loop end'
end
$ ruby 4.rb
loop start
loop end
loop start
^Chandler2
handler1
loop end

$ ruby 4.rb
loop start
loop end
loop start
^Chandler2
^C^C^C^C^C^Chandler1
handler1
handler1
handler1
handler1
handler1
handler1
loop end

ハンドラを再登録後、Process.killでシグナルを再発火することで、元のハンドラも実行しています2

関数化してみる

5.rb
def interrupt_signal(signal, &block)
  h = Signal.trap(signal) do
    block.call
    Signal.trap(signal, h)
    Process.kill(signal, $$)
  end
end

is_end = false

Signal.trap(:INT, nil)
interrupt_signal(:INT) { is_end = true }
interrupt_signal(:INT) { puts '1' }
interrupt_signal(:INT) { puts '2' }
interrupt_signal(:INT) { puts '3' }
interrupt_signal(:INT) { puts '4' }
interrupt_signal(:INT) { puts '5' }

until is_end
  puts 'loop start'
  sleep(1)
  puts 'loop end'
end
$ ruby 5.rb
loop start
loop end
loop start
^C5
4
3
2
1
loop end

$ ruby 5.rb
loop start
loop end
loop start
^C5
4
3
2
1
^C^C^C^C^C^C^C^C^Cloop end

一応使えそうな感じにはなりました。

ちなみにググると...

シグナルハンドラ内ではMutex#synchronizeを使うべき、などのプラクティスが出てきますが、それらも怪しいようです。

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

上記の実験からしても、この解釈が正しいそうだと思っています。

ハンドラ監視用スレッドを別で用意するべきかどうかは、現時点ではあまり判断ついてないです。


  1. 手元にインストールされていたのがコレというだけで深い意味はありません。シグナルハンドリングは結構バージョンによって変わるという話もあるようなので要注意かもしれません。 

  2. 実はこれが正しい方法なのか確証は持てていませんが、これという代替案も浮かばず。 

10
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
8