前回はプロセスとシグナル、そしてシグナルを明示的にプロセスに送るためのコマンド kill について見ました。そして最後にひとつ謎が残ったわけですが、今回はその謎を解いて行きましょう。
前回の謎のおさらい:
- fg + Ctrl+C では親子プロセスが一緒に死んだ
- kill -INT では親プロセスだけが死んで子プロセスが残った
この違いの秘密は「プロセスグループ」という概念にあります。
さて、じつは今まで一度も意識したことはありませんでしたが、プロセスというのはかならずひとつのプロセスグループというものに属します。
プロセスグループとは、簡単に言うと「関連するプロセスたちの集まり」です。会社でいう「部署」みたいなものですね。
見てみましょう。
$ ruby -e 'sleep' &
$ ps o pid,pgid,command PID PGID COMMAND
1620 1620 -bash
1638 1638 ruby -e sleep
1639 1639 ps o pid,pgid,command
毎度おなじみ sleep し続ける ruby プロセスをバックグラウンドで実行して、ps を "o pid,pgid,command" 付きで実行してみました。「pidとpgidとcommandを表示する」くらいの意味です。おや、見慣れない PGID というものがありますね。これが、プロセスグループのidです。こんな感じで、プロセスがかならずひとつのプロセスグループに属していることが見て取れるかと思います。なんだか今は PID と同じ数字が PGID のところにも表示されていて、この PGID ってあまり意味や意義がわからない感じですね。
では、ここで、fork と組み合わせてみましょうか。
# fork.rb
# プロセスグループの基本的な動作を確認するサンプル
# forkで子プロセスを作成(子は親と同じプロセスグループに属す)
fork
# 親も子も無限スリープ
sleep上記のような、 fork して sleep し続けるだけの fork.rb というスクリプトを作ってバックグラウンドで実行してみましょう。
$ ruby fork.rb &
$ ps o pid,pgid,command f PID PGID COMMAND
1620 1620 -bash
1646 1646 \_ ruby fork.rb
1647 1646 | \_ ruby fork.rb
1652 1652 \_ ps o pid,pgid,command f
今回は ps に f オプションを付けて tree 状に表示してみました。
さて、こうして見てみると、親プロセスであるプロセス(pid 1646)は PID と PGID が同じ数字ですが、そこから fork で生成された子プロセス(pid 1647)は、PID と PGID が別の数字になっています。そして、子プロセスのほうの PGID は、fork元である親プロセスの PGID になっているのがわかるでしょうか。
こんな感じで、実は fork された子プロセスは、親プロセスと同じプロセスグループに属するようになります。逆の言い方をすると、forkで子プロセスを作ることによって、「自分と同じプロセスグループに属するプロセス」が一個ふえるわけですね。
ちなみに、プロセスグループにはリーダーが存在して、PGID と同じ数字の PID のプロセスが、プロセスグループのリーダーです。forkすると、同じグループに属する子分ができる、みたいな感じですね。
さて、今かんたんに「fork すると子プロセスは自分と同じプロセスグループに属するようになる」と言いましたが、これはちょっとおかしいですね。そうです、以前見たように、すべてのプロセスは pid 1 のプロセスから fork で作られたのでした。そうなると、すべてのプロセスは pid 1 のプロセスと同じプロセスグループに属することになってしまいます。すべてのプロセスが同じグループに属すなら、グループの意味がないですね。だから、forkしたあと、プロセスグループをいじる仕組みが必要になってきます。それが setpgrp システムコールです。では例を見てみましょう。
# fork_setpgrp.rb
# 子プロセスが新しいプロセスグループを作るサンプル
pid = fork
if pid
# 親プロセス:元のプロセスグループのリーダーのまま
sleep
else
# 子プロセス:新しいプロセスグループを作成
# Process.setpgrp:引数なしで呼び出すと、
# 自分の新しいプロセスグループを作成してそのリーダーになる
# これにより子プロセスの PGID は自分の PID と同じになる
Process.setpgrp
sleep
end上記のようなスクリプトを fork_setpgrp.rb という名前で保存して、バックグラウンドで実行、ps で確認してみましょう
$ ps o pid,pgid,command f PID PGID COMMAND
1620 1620 -bash
1666 1666 \_ ruby fork_setpgrp.rb
1667 1667 | \_ ruby fork_setpgrp.rb
1673 1673 \_ ps o pid,pgid,command f
今度は、子プロセスは親プロセスと同じ PGID ではなくなりました。setpgrp システムコールを引数なしで呼び出したことにより、今までグループ内で子分役をやっていた子プロセスが、新しく自分のグループを作り、リーダーになっていることが見て取れるかと思います。なんだかベンチャー界隈でよく聴く独立譚みたいな話ですね。
ちなみに、PGID は、親の側からいじることもできます。
# 親プロセスが子プロセスのプロセスグループを変更するサンプル
pid = fork
if pid
# 親プロセス:子プロセスのプロセスグループを変更
# Process.setpgrpを引数付きで呼び出す
# 第1引数:変更したいプロセスのPID
# 第2引数:新しいプロセスグループID(ここでは子のPIDと同じ)
pgid = pid
Process.setpgrp(pid, pgid)
sleep
else
# 子プロセス:新しいプロセスグループのリーダーになる
sleep
endこういう感じで親プロセスのほうで引数付きで setpgrp を呼び出すことで、子プロセスの PGID を設定することもできます。
こんな感じで、プロセスが fork で子プロセスを作ったとき、その時点ではその子プロセスは親プロセスと同じプロセスグループに属しています。プロセスグループを変更したいときには、この子プロセスの PGID を setpgrp システムコールでいじってあげれば良いわけですね。
ちなみに、シェルから起動されたプロセスは、シェルが勝手に setpgrp を呼んでくれるので、それぞれがプロセスグループのリーダーとなっています。
さて、いままでの話だけでは、「プロセスグループってのがあるのはわかったけど、そんなもんがあってなにがうれしいの」という感じがしますね。うれしいことのひとつとして、kill でプロセスグループに属する全てのプロセスに一気にシグナルを送れる、というものがあります。kill で pid を指定する部分に、"-" を付けてあげると、pid ではなくて pgid を指定したことになります。やってみましょう。
$ ruby fork.rb &
$ ps o pid,pgid,command f PID PGID COMMAND
1678 1678 -bash
1699 1699 \_ ruby fork.rb
1700 1699 | \_ ruby fork.rb
1701 1701 \_ ps o pid,pgid,command f
$ kill -INT -1699 # 1699 ではなくて -1699 としている
$ ps o pid,pgid,command f # 一気にふたつのプロセスが消えている PID PGID COMMAND
1678 1678 -bash
1702 1702 \_ ps o pid,pgid,command f
ではここで、前回の謎に回答しましょう。前回謎だった挙動は、「fg でプロセスをフォアグラウンドにしてから Ctrl+C で SIGINT を送信したときは子プロセスごと殺されたのに、 kill -INT でバックグラウンドのプロセスに SIGINT を送信したら親プロセスだけが殺される」という挙動でしたね。
勘のいいひとはすでにお気づきかもしれないですが、実は、「フォアグラウンド」とされる範囲は、プロセス単位ではなくて、プロセスグループ単位で決まっているのです。いくつか、例を見てみましょう。
# fork_and_trap_sigint.rb
# 同じプロセスグループ内の親子プロセスが一緒にSIGINTを受け取るサンプル
# SIGINTハンドラーを設定(デフォルトの終了ではなく例外を発生)
Signal.trap('INT') do
raise "got SIGINT!" # このメッセージが親と子両方で表示される
end
# forkで子プロセスを作成(同じプロセスグループに属す)
fork
# 親も子もスリープ(Ctrl+Cで両方にSIGINTが送られる)
sleep上記のようなスクリプトをフォアグラウンドで実行して、Ctrl+C でSIGINTを送ってみましょう。すると、"got SIGINT!" がふたつ標準エラーに出力されるはずです。これは、子プロセスと親プロセスが同じプロセスグループに属しているため、このふたつのプロセスがフォアグラウンドで実行されているからですね。
では今度は、プロセスグループが別の場合を見てみましょう。
# fork_and_setpgrp_and_trap_sigint.rb
# 子プロセスが異なるプロセスグループに属してSIGINTを受け取らないサンプル
# SIGINTハンドラーを設定
Signal.trap('INT') do
raise "got SIGINT!" # 親プロセスだけで表示される
end
pid = fork
if pid
# 親プロセス:フォアグラウンドプロセスグループに所属
sleep # Ctrl+CでSIGINTを受け取る
else
# 子プロセス:新しいプロセスグループを作成
# Process.setpgrpで新しいプロセスグループのリーダーになる
# これにより子プロセスはフォアグラウンドプロセスグループから抜ける
Process.setpgrp
sleep # Ctrl+CでSIGINTを受け取らない
end上記のようなスクリプトをフォアグラウンドで実行して、Ctrl+C でSIGINTを送ってみましょう。すると、"got SIGINT!" が今度はひとつだけ出力されるはずです。これは、子プロセスが親プロセスのプロセスグループを抜けて別のプロセスグループになったため、フォアグラウンドから抜けてしまったためです。
別の例も見てみましょう。
# read_stdin_in_child.rb
# 同じプロセスグループ内の子プロセスが標準入力を受け取るサンプル
pid = fork
if pid
# 親プロセス:標準入力を閉じて子の終了を待つ
STDIN.close # 親は標準入力を使わない
Process.waitpid(pid) # 子プロセスの終了を待機
else
# 子プロセス:フォアグラウンドプロセスグループに属すので標準入力を受け取れる
STDIN.each_line do |line|
print line # エコーサーバーとして動作
end
end上記のようなスクリプトを作成し、フォアグラウンドで実行してみましょう。親プロセスは子プロセスが終わるまで待ってるのでそこでブロックしています。子プロセスは標準入力からの入力を受け取ろうとそこでブロックしています。
ここでターミナルになんか文字を打ち込めば、子プロセスがその入力を受け取ってエコーしてくれます。
ではこれをsetpgrpとの合わせ技でやるとどうなるでしょう?
# setpgrp_and_read_stdin_in_child.rb
# 異なるプロセスグループの子プロセスが標準入力を受け取れないサンプル
pid = fork
if pid
# 親プロセス:標準入力を閉じて子の終了を待つ
STDIN.close
Process.waitpid(pid)
else
# 子プロセス:新しいプロセスグループを作成
# Process.setpgrpで新しいプロセスグループのリーダーになる
# これにより子プロセスはフォアグラウンドプロセスグループから抜ける
# 結果、ターミナルからの入力を受け取れなくなる
Process.setpgrp
# STDINからの入力をエコーしようとするが、
# フォアグラウンドではないので入力を受け取れない
STDIN.each_line do |line|
print line
end
end上記のようなスクリプトをフォアグラウンドで実行してみましょう。さっきとは異なり、ターミナルになにかを打ち込んでもおうむがえししてこないのが見て取れると思います。これは子プロセスが親プロセスとは別のPGIDに属したことによって、フォアグラウンドで実行されているプロセスグループから抜けたためですね。
さて、これで前回謎だった挙動にも説明がつきましたね。これで、プロセスグループの解説はおしまいにします。
これにてこのシリーズはおしまいです。いかがだったでしょうか? 一度プロセスまわりについてまとめておきたいという動機で書き始めたのですが、これを書きながらわたしも理解があやふやなところが洗い出せたりして、なにかと有意義でした。
もしもこのドキュメントが役に立つと思っていただけたなら、勉強会とかそういうのであなたが属すコミュニティや会社に役立ててもらえたらとても嬉しいです。そのとき、「使ったよ!」とコメント欄とかメールとかで知らせてくれると、単純にわたしが喜びます(言わなくても自由に使っていただいてかまわないですけど)。