シェルスクリプトから"foreman start"したときにCtrl-Cで終了できないという現象に遭遇しました。なぜこのようなことが起こったのかについて調べてみたので解説します。一見不可解におもえるプロセスの振舞いをUNIXプロセスの仕組みをひもとき説明してみたというおもむきの記事です。
概要
foremanはシェルスクリプトから、"foreman start"のように起動したときにCtrl-C終了できません。シェルスクリプトでなくてもssh経由で ssh -t host foreman start
のようにした場合でも同様の問題が発生します。これは、foremanがsetpgrpシステムコールを呼び出してプロセスグループのリーダになるという動作をしていたのが原因でした。
現象
以下のように "foreman start"をシェルスクリプトから実行すると、Ctrl-Cによりforemanを停止できなくなります。
$ cat foreman_wrapper.sh foreman start $ sh foreman_wrapper.sh # foreman のログが表示される # Ctrl-C で停止できない
また以下のようにssh経由でremoteのforemanをstartとさせた場合も同様な現象が発生します。
$ ssh -t host foreman start # foreman のログが表示される # Ctrl-C で停止できない
調べてみるとForeman::Engine 内での Process.setpgrpの呼び出し がこの現象をひきおこしているようです。foreman起動時にforemanのプロセスがプロセスグループのリーダーになることを保証しています。foremanに送出されたSIGNALが子プロセスにも伝わるようにこのようにしているのだと思われます。このコード自体は妥当そうです。
現象の再現
なぜ、Process.setpgrpしてシェルスクリプトからforemanを起動したときにCtrl-Cが入力できないようなことがおこるのでしょうか? まずは、この現象の再現を試みてみます。
setpgrpシステムコールを呼び出してプロセスグループのリーダになるプログラムを用意します。SIGINTなどのシグナルを与えなければ終了しないようにしておきます。
# run.rb Process.setpgrp # プロセスグループのリーダーになる t = Thread.new do loop do puts "hello" sleep 1 end end t.join
実行すると無限に hello を出力しつづけます。Ctrl-Cを入力してSIGINTを送るととまります。
$ ruby run.rb
hello
hello
hello
hello # Ctrl-CをおしてSIGINTを送出するととまる
このプログラムが動作している時、プロセスは以下のような様子になっています。
PID PPID PGID SID TPGID TT STAT COMMAND 3591 3493 3493 3493 -1 ? S sshd: vagrant@pts/0 3592 3591 3592 3592 8311 pts/0 Ss \_ -bash 8311 3592 8311 3592 8311 pts/0 Sl+ \_ ruby run.rb
通常は親プロセス(bash) と 子プロセス(ruby run.rb) のPGID(プロセスグループID)は一致しますが、この場合は子プロセスがsetpgrpシステムコールを呼び出してプロセスグループのリーダーになることを要求し(正確には自分のPGIDを自分のPIDに設定する)、新しいプロセスグループに所属するようにするため、PGIDが違っています。
STATについている+はそのプロセスがフォアグラウンドのプロセスグループに属していることを示します。Ctrl-Cなどのキーボード入力割り込みは、フォアグラウンドのプロセスグループにわたされます。この場合は、 ruby run.rb
のSTATに+がついているので、キーボードからの入力が ruby run.rb
に向かいます。
ここで、上のプログラムをシェルスクリプト経由で実行するようにします。するとCtrl-Cが ruby run.rb
に到達しなくなります。
# wrapper.sh ruby proc.rb # 上のプログラムをシェルスクリプト経由で起動
$ sh wrapper.sh
hello
hello
^C^C^C^Chello # とまらぬ!!!
hello
このときのプロセスの様子をみてみます。
PID PPID PGID SID TPGID TT STAT COMMAND 3591 3493 3493 3493 -1 ? S sshd: vagrant@pts/0 3592 3591 3592 3592 6795 pts/0 Ss \_ -bash 6795 3592 6795 3592 6795 pts/0 S+ \_ sh wrapper.sh 6796 6795 6796 3592 6795 pts/0 Sl \_ ruby run.rb
ご覧のとおり ruby run.rb
のSTATに+がついていません。よって、ruby run.rb
にキーボードからの入力が向かわなくなり、Ctrl-Cによってプログラムが終了できなくなりました。
解説
シェルスクリプト経由で起動したProcess.setpgrpするプロセスがフォアグラウンドにならない理由は、もうすこしOSやシェルの動作を調べなければなりません。
まず、前提として、プロセスグループのリーダーになった ( = 新しくプロセスグループがつくられた) ときにそのプロセスグループが勝手にフォアグラウンドになることはありません 。プロセスをフォアグラウンドにするにはtcsetpgrpシステムコールを適切な方法で呼び出す必要があります。また、プロセスグループのID ( = PGID ) はプロセスグループのリーダーのプロセスID ( = PID)とおなじになっています。
シェルはあたらしくコマンドを実行するときそのコマンドのために新しいプロセスグループを作ります。sh wrapper.sh
をはさまず直接 ruby run.rb
を起動すると、そこでできた新しいプロセスグループには ruby run.rb
のプロセスしかいませんので、 ruby run.rb
のプロセスは新しいプロセスグループのリーダーになります(下図 ①)。自分自身をプロセスグループのリーダーにするsetpgrpシステムコールを呼び出しても、プロセスの状態に変化はありません。シェルから ruby run.rb
が起動されたときは、当然フォアグラウンドのプロセスグループは ruby run.rb
のプロセスが所属しているプロセスグループになっており、これも変化しません。
一方、sh wrapper.sh
から ruby run.rb
を起動した場合、 ruby run.rb
はsh wrapper.sh
の起動時に作られたプロセスグループに所属します(下図 ②から③)。
そこで ruby run.rb
の中で、setpgrpシステムコールを呼び出します。setpgrpシステムコールは呼び出したプロセスのPGIDを呼び出したプロセスのPIDに設定するという動作をします。もとのPGIDはプロセスグループのリーダーになっているsh wrapper.sh
のPIDと同じ値になっていたため、sh wrapper.sh
と同じプロセスグループに属していましたが、sepgrpシステムコールの動作によりPGIDが ruby run.rb
のPIDである新しいプロセスグループが作成され以下の図のようになります。
フォアグラウンドのプロセスグループは勝手に変化をしませんから、sh wrapper.sh
が所属しているプロセスグループのままです。つまり、 ruby run.rb
が所属しているプロセスグループはフォアグラウンドではなくなりました。この結果、Ctrl-Cなどのキーボード入力は ruby run.rb
のプロセスにはわたらなくなります。
結局どうすればよいか
今回はそもそもちょっとした起動スクリプトから起動したforemanのプロセスをCtrl-Cで終了させることが目的でした。振舞いの原因がわかったことで、以下の解決策が考えられました。
- foremanのプロセスグループをフォアグラウンドにする(tcsetpgrpシステムコールを使う)
- foremanのプロセスに直接SIGINTを送出する
今回は、そもそもSIGINTが送出さえできれば良いということになり、結局、以下のような 実行したいコマンドをforkして終了をまつようなスクリプトを間にはさむことで解決することにしました。spawnが便利です (参考: https://gist.github.com/ujihisa/325036)。
#!/usr/bin/env ruby puts "Starting..." pid = spawn *%w(bundle exec foreman start) trap("INT") { puts "Shutting down..." Process.kill(2, pid) } Process.waitpid2(pid)
まとめ
foremanのようにプロセスグループを作るプロセスはフォアグラウンドプロセスでなくなることがあり、Ctrl-Cなどのキーボード入力をうけつけなくなることがわかりました。
なんか Ctrl-C がきかねーんだけど、とかいいながら調査していたら、UNIXプロセスに少しくわしくなりました。まちがっている点などありましたらぜひ教えてください。Working With Unix Processesを読んだら理解が深まりそう!
参考資料
- Working With Unix Processes まだ、読んでないけど、id:shiba_yu36 先生によるとこのあたりの話がくわしいらしい
man setpgid
- メモの日々(2008-03-24)
- PC覚え書き | プログラムのデーモン化