それマグで!

知識はカップより、マグでゆっくり頂きます。 takuya_1stのブログ

習慣に早くから配慮した者は、 おそらく人生の実りも大きい。

rubyのspawnで 起動したプロセスを、終了待ちしたり、強制終了したり、親プロセスと一緒に殺す

ruby から外部コマンドを起動する spawn

ruby で外部コマンドを呼び出すときに spawn を使って管理してみた。

spawn の特徴とか。

だいぶ昔に導入されたこの機能については、次のスライドが良いみたい。https://staff.aist.go.jp/tanaka-akira/pub/spawn-2009-04.pdf

1.8 から 1.9 の頃のはなし。

  • `cmd`
  • system “cmd”
  • exec “cmd”,,,
  • open/popen/popen3
  • fork

ここにさらに spawn が加わってた。

spawn でプロセス起動

コマンドラインを指定する。

spawn " sleep 10"

特徴として、実行して投げっぱなし。終了待ちしたりしない。

実行後のプロセスを見てみると。

  PID TTY      STAT   TIME COMMAND
30278 ?        S      0:00 sshd: takuya@pts/0
30279 pts/0    Ss+    0:00  \_ -bash
24760 pts/4    Ss+    0:02 /bin/bash
31470 pts/0    S      0:00 sleep 10

sleep の実行は即座に実行されている。終了待ちしてない。

実行後に親プロセスが正常終了するので、親がinit になる。(つまり、孤児プロセス)

spawn でプロセス起動して、終了待ちをする。

今度は、プロセス起動をして、終了待ちをする。 終了待ちするには Process.wait( pid ) か Process.waitall を使う。

i = spawn(" sleep 10 " )
Process.wait i

実行後のプロセスツリーは次のようになる。

  PID TTY      STAT   TIME COMMAND
30278 ?        S      0:00 sshd: takuya@pts/0
30279 pts/0    Ss     0:00  \_ -bash
31569 pts/0    Sl+    0:00      \_ ruby test.rb
31571 pts/0    S+     0:00          \_ sleep 10

今度は、ruby の子プロセスになっている状態がハッキリわかる。前例のspawnのみは、終了待ちをしないため、rubyが終了してinitが親になっていた。

spanwで起動したプロセスを終了する。

次に、子プロセスが実行中に、SIGNALを送って終了させてみることにする。

これは、時間のかかる処理を待ってる時に待ちきれなくて、止めるなどの利用場面。

i = spawn(" sleep 10 " )     ##spawn 
sleep 5                                  ## 
Process.kill(:TERM, i)           ## 終了させる
sleep 5                               

状態が2つできるので、それぞれ追いかけていく。

  1. spawn 直後から 子プロセスの終了待ち
  2. 子プロセスを強制終了直後

起動直後の状態

起動直後の状態は次のようになっている。

  PID TTY      STAT   TIME COMMAND
30278 ?        S      0:00 sshd: takuya@pts/0
30279 pts/0    Ss     0:00  \_ -bash
31649 pts/0    Sl+    0:00      \_ ruby test.rb
31651 pts/0    S+     0:00          \_ sleep 10

起動したプロセスを待ち合わせているので、ruby は sleep を子供に持ったまま、終了待ちをしている。

SIGTERM 送信後の状態。

起動したプロセスにTERMを送信して、終了した状態。

  PID TTY      STAT   TIME COMMAND
30278 ?        S      0:00 sshd: takuya@pts/0
30279 pts/0    Ss     0:00  \_ -bash
31649 pts/0    Sl+    0:00      \_ ruby test.rb
31651 pts/0    Z+     0:00          \_ [sleep] <defunct>

sleep は終了し、ruby だけが残る。

ここまでのまとめ

spawn したプロセスは子プロセス。

特に指定しないかぎり。ruby から 子プロセスとしてプログラムが実行される。

spawn の待ち合わせはしない。

spawn したら直ぐにruby に戻ってくる。waitで明示的に待たないかぎり、終了待ちはしない。

子プロセスのpid を覚えておく

spawn したプロセスのpid を覚えておくと終了が出来る。

もし、ruby だけを ctrl+C で止めたらどうなるのか?

子プロセスは、孤児になり、init(親)に拾われる。ruby が先に終了した時と同じ。

ruby から spawn したプロセスは ctrl+C では止められない。

ただし、wait している場合はCTRL+Cでwait 中プロセスを止めてくれる。

ただしruby へ kill -INT interrupt を送信した時は止まらなかったり、

ただし、プロセスグループ作ってたら止まらなかったり、自分が親だったら止まったり。

なんでだろうな?このへん良く分からない。

UNIXのプロセス管理をチャント復習したうえで、ruby の spawn がどうしてるのかを調べないとわからなさそう。

プロセスをいっぱい起動してみる。

次に、プロセスを次々と起動してみる。

pids = []
pids << spawn("sleep","10")
pids << spawn("sleep","10")
pids << spawn("sleep","10")
pids << spawn("sleep","10")
pids << spawn("sleep","10")

Process.waitall

次々と起動した結果がコレ。

  PID TTY      STAT   TIME COMMAND
30278 ?        S      0:00 sshd: takuya@pts/0
30279 pts/0    Ss     0:00  \_ -bash
30467 pts/0    T      0:07      \_ vim test.rb
31800 pts/0    Sl+    0:00      \_ ruby test.rb
31802 pts/0    S+     0:00          \_ sleep 10
31804 pts/0    S+     0:00          \_ sleep 10
31806 pts/0    S+     0:00          \_ sleep 10
31808 pts/0    S+     0:00          \_ sleep 10
31810 pts/0    S+     0:00          \_ sleep 10

これは、小さなバッチ処理を複数起動するのに便利。

また、起動したプロセスをまとめて Process.waitlall で待ち合わせ出来るのが楽チンでいいな。

プロセスのグループ管理。

プロセスを実行するときに親を決められる。

pids = []
pids << spawn("sleep","60" , :pgroup => Process.pid )
pids << spawn("sleep","60" , :pgroup => Process.pid )
pids << spawn("sleep","60" , :pgroup => Process.pid )
pids << spawn("sleep","60" , :pgroup => Process.pid )
pids << spawn("sleep","60" , :pgroup => Process.pid )


pids.each{|i|
  puts "子 #{i} <- グループ #{Process.getpgid(i)} "
}

puts "ruby の pid = #{Process.pid}"

Process.waitall

いっぱい起動しても、チャント管理できる。

たくさん起動して、全部に終了シグナルを送る

プロセスグループのpid を-1 した値とシグナルを Process.kill で送ると、グループ全部にシグナルを送って終了してくれる。

i = spawn("sleep", "10")
pids = []
pids << spawn("sleep","60" , :pgroup => Process.pid )
pids << spawn("sleep","60" , :pgroup => Process.pid )
pids << spawn("sleep","60" , :pgroup => Process.pid )
pids << spawn("sleep","60" , :pgroup => Process.pid )
pids << spawn("sleep","60" , :pgroup => Process.pid )

sleep 3
Process.kill(:TERM, -1*Process.pid)

孫プロセスはどうなるのか、そのうち試したい。

:pgroup にtrueを設定するとプロセスグループを新規作成してその中で動く

pids = []
pids << spawn("sleep","60" , :pgroup => true )
pids << spawn("sleep","60" , :pgroup => true )
pids << spawn("sleep","60" , :pgroup => true )
pids << spawn("sleep","60" , :pgroup => true )
pids << spawn("sleep","60" , :pgroup => true )


puts "ruby の pid = #{Process.pid}"

このへんも良く分からない、子プロセスの親が自分じゃないのにwaitall で待てる?

spawn したプロセスをどうするか

複数起動するときは グループを指定する。

終了待ちが必要なら wait する。

それでも終了しなかったり、不要になれば シグナルを送る。

一定時間経過したら終了する。

WEBカメラなどをストリーミングして不要になったらプロセスを消すとか出来そう

id = spawn(cmd, Process.pid) 
watch_thread = Process.detatch(id)
begin
  Timeout.timeout(3000) do
    watch_thread.join
  end
rescue Timeout::Error
  Process.kill(:TERM, -1*Process.pid)   
end

SIGNAL を受けてプロセスグループを管理することも出来そう

trap :INT do
  Process.kill(:TERM, -1*Process.pid)   
end

リアルタイム性はないけれど、GPIOをずっと監視するスレッドやネットワークdaemon 作ったり消したり出来そうじゃん。楽しそう。

参考資料 trap / sigal / spawn について。

http://yukithm.blogspot.jp/2014/03/kill.html

http://route477.net/d/?date=20090705

https://staff.aist.go.jp/tanaka-akira/pub/spawn-2009-04.pdf

Rubyのシグナルハンドラ - @tmtms のメモ