プロセスグループ と フォアグランドプロセス

前回はプロセスとシグナル、そしてシグナルを明示的にプロセスに送るためのコマンド kill について見ました。そして最後にひとつ謎が残ったわけですが、今回はその謎を解いて行きましょう。

プロセスグループ

さて、じつは今まで一度も意識したことはありませんでしたが、プロセスというのはかならずひとつのプロセスグループというものに属します。見てみましょう。

$ perl -e 'sleep' &

$ ps o pid,pgid,command
 PID  PGID COMMAND
1620  1620 -bash
1638  1638 perl -e sleep
1639  1639 ps o pid,pgid,command

毎度おなじみ sleep し続ける perl プロセスをバックグラウンドで実行して、ps を "o pid,pgid,command" 付きで実行してみました。「pidとpgidとcommandを表示する」くらいの意味です。おや、見慣れない PGID というものがありますね。これが、プロセスグループのidです。こんな感じで、プロセスがかならずひとつのプロセスグループに属していることが見て取れるかと思います。なんだか今は PID と同じ数字が PGID のところにも表示されていて、この PGID ってあまり意味や意義がわからない感じですね。

では、ここで、fork と組み合わせてみましょうか。

# fork.pl
use strict;
use warnings;

fork;

sleep;

上記のような、 fork して sleep し続けるだけの fork.pl というスクリプトを作ってバックグラウンドで実行してみましょう。

$ perl fork.pl &

$ ps o pid,pgid,command f
 PID  PGID COMMAND
1620  1620 -bash
1646  1646  \_ perl fork.pl
1647  1646  |   \_ perl fork.pl
1652  1652  \_ ps o pid,pgid,command f

今回は ps に f オプションを付けて tree 状に表示してみました。(BSD系だと f 利かないので Macで試すなら f なしで実行してください)

さて、こうして見てみると、親プロセスであるプロセス(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.pl
use strict;
use warnings;

my $pid = fork;

die "fork failed" unless defined $pid;

if ($pid) {
    # 親プロセス
    sleep;
}
else {
    # 子プロセス

    # setpgidシステムコールを
    # 引数なしで呼び出すと、
    # 自分のプロセスグループを作ってそこのリーダーになる
    setpgrp;
    sleep;
}

上記のようなスクリプトを fork_setpgrp.pl という名前で保存して、バックグラウンドで実行、ps で確認してみましょう

$ ps o pid,pgid,command f
 PID  PGID COMMAND
1620  1620 -bash
1666  1666  \_ perl fork_setpgrp.pl
1667  1667  |   \_ perl fork_setpgrp.pl
1673  1673  \_ ps o pid,pgid,command f

今度は、子プロセスは親プロセスと同じ PGID ではなくなりました。setpgrp システムコールを引数なしで呼び出したことにより、今までグループ内で子分役をやっていた子プロセスが、新しく自分のグループを作り、リーダーになっていることが見て取れるかと思います。なんだかベンチャー界隈でよく聴く独立譚みたいな話ですね。

ちなみに、PGID は、親の側からいじることもできます。

use strict;
use warnings;

my $pid = fork;

die "fork failed" unless defined $pid;

if ($pid) {
    # 親プロセス

    # setpgrp を引数付きで呼び出す
    my $pgid = $pid;
    setpgrp $pid, $pgid;
    sleep;
}
else {
    # 子プロセス
    sleep;
}

こういう感じで親プロセスのほうで引数付きで setpgrp を呼び出すことで、子プロセスの PGID を設定することもできます。

いったんまとめ

こんな感じで、プロセスが fork で子プロセスを作ったとき、その時点ではその子プロセスは親プロセスと同じプロセスグループに属しています。プロセスグループを変更したいときには、この子プロセスの PGID を setpgrp システムコールでいじってあげれば良いわけですね。

ちなみに、シェルから起動されたプロセスは、シェルが勝手に setpgrp を呼んでくれるので、それぞれがプロセスグループのリーダーとなっています。

プロセスグループ全体に kill をしてみよう

さて、いままでの話だけでは、「プロセスグループってのがあるのはわかったけど、そんなもんがあってなにがうれしいの」という感じがしますね。うれしいことのひとつとして、kill でプロセスグループに属する全てのプロセスに一気にシグナルを送れる、というものがあります。kill で pid を指定する部分に、"-" を付けてあげると、pid ではなくて pgid を指定したことになります。やってみましょう。

$ perl fork.pl &

$ ps o pid,pgid,command f
 PID  PGID COMMAND
1678  1678 -bash
1699  1699  \_ perl fork.pl
1700  1699  |   \_ perl fork.pl
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.pl
use strict;
use warnings;

$SIG{INT} = sub {
    die "got SIGINT!";
};

fork;

sleep;

上記のようなスクリプトをフォアグランドで実行して、Ctrl+C でSIGINTを送ってみましょう。すると、"got SIGINT!" がふたつ標準エラーに出力されるはずです。これは、子プロセスと親プロセスが同じプロセスグループに属しているため、このふたつのプロセスがフォラグランドで実行されているからですね。

では今度は、プロセスグループが別の場合を見てみましょう。

# fork_and_setpgrp_and_trap_sigint.pl
use strict;
use warnings;

$SIG{INT} = sub {
    die "got SIGINT!";
};

my $pid = fork;
die "fork failed" unless defined $pid;

if ($pid) {
    # 親プロセス
    sleep;
}
else {
    # 子プロセス

    # setpgrpを呼び出して新しいプロセスグループのリーダーになる
    # これにより、子プロセスは親プロセスと異なるプロセスグループに
    # 属すことになり、フォアグランドで実行されている
    # プロセスグループから抜ける
    setpgrp;
    sleep;
}

上記のようなスクリプトをフォアグランドで実行して、Ctrl+C でSIGINTを送ってみましょう。すると、"got SIGINT!" が今度はひとつだけ出力されるはずです。これは、子プロセスが親プロセスのプロセスグループを抜けて別のプロセスグループになったため、フォアグランドから抜けてしまったためです。

別の例も見てみましょう。

# read_stdin_in_child.pl
use strict;
use warnings;

my $pid = fork;
die "fork failed" unless defined $pid;

if ($pid) {
    # 親ではstdin閉じる
    close(STDIN);

    # 子の終了待つ
    waitpid($pid,0);
}
else {
    # STDINからの入力をエコーする
    while(my $line = <STDIN>) {
        print $line;
    }
}

上記のようなスクリプトを作成し、フォアグラウンドで実行してみましょう。親プロセスは子プロセスが終わるまで待ってるのでそこでブロックしています。子プロセスは標準入力からの入力を受け取ろうとそこでブロックしています。

ここでターミナルになんか文字を打ち込めば、子プロセスがその入力を受け取ってエコーしてくれます。

ではこれをsetpgrpとの合わせ技でやるとどうなるでしょう?

# setpgrp_and_read_stdin_in_child.pl
use strict;
use warnings;

my $pid = fork;
die "fork failed" unless defined $pid;

if ($pid) {
    # 親ではstdin閉じる
    close(STDIN);

    # 子の終了待つ
    waitpid($pid,0);
}
else {
    # setpgrpを呼び出して新しいプロセスグループのリーダーになる
    # これにより、子プロセスは親プロセスと異なるプロセスグループに
    # 属すことになり、フォアグランドで実行されている
    # プロセスグループから抜ける
    setpgrp;


    # STDINからの入力をエコーする
    while(my $line = <STDIN>) {
        print $line;
    }
}

上記のようなスクリプトをフォアグラウンドで実行してみましょう。さっきとは異なり、ターミナルになにかを打ち込んでもおうむがえししてこないのが見て取れると思います。これは子プロセスが親プロセスとは別のPGIDに属したことによって、フォアグランドで実行されているプロセスグループから抜けたためですね。

さて、これで前回謎だった挙動にも説明がつきましたね。これで、プロセスグループの解説はおしまいにします。

おわりに

これにてこのシリーズはおしまいです。いかがだったでしょうか? 一度プロセスまわりについてまとめておきたいという動機で書き始めたのですが、これを書きながらわたしも理解があやふやなところが洗い出せたりして、なにかと有意義でした。

もしもこのドキュメントが役に立つと思っていただけたなら、勉強会とかそういうのであなたが属すコミュニティや会社に役立ててもらえたらとても嬉しいです。そのとき、「使ったよ!」とコメント欄とかメールとかで知らせてくれると、単純にわたしが喜びます(言わなくても自由に使っていただいてかまわないですけど)。