はじめに
cmd1 | cmd2 | cmd3 を実行しているときに Ctrl-C を押すと、なんとなく「まとめて kill された」ように見えます。
ただ、正確には少し違います。
-
Ctrl-Cが送るのはSIGKILLではなくSIGINT - 相手は親プロセス 1 個ではなく foreground process group
- シェルはパイプでつないだコマンド群を同じ process group に入れる
この 3 点だけ押さえると、なぜパイプ全体が止まるのか、なぜ daemon 化した子には届かないのかが見通しやすくなります。
この記事のゴール
この記事では、次の疑問をまとめて整理します。
| 疑問 | この記事で分かること |
|---|---|
Ctrl-C は誰に届くのか |
foreground process group に SIGINT が届く |
| なぜパイプ全体が止まるのか | シェルが同じ process group にまとめるから |
daemon はなぜ残るのか |
session と端末から独立するから |
| まとめて止めたいときはどう見るか |
PID ではなく PGID を見ると整理しやすい |
先に結論
-
Ctrl-Cは端末の foreground process group にSIGINTを送る - パイプでつないだ一連のコマンドは、通常は同じ process group に属する
- そのため
cmd1 | cmd2 | cmd3はCtrl-Cでまとめて止まりやすい -
daemonはsetsidなどで元の端末から切り離されるので、元のシェルのCtrl-Cは届かない
まずは用語を 1 枚で整理する
| 用語 | ざっくりした意味 |
Ctrl-C との関係 |
|---|---|---|
session |
端末をぶら下げる大きな単位 | controlling terminal と結びつく |
process group |
ジョブ制御やシグナル配送の単位 | foreground group が SIGINT を受ける |
shell |
コマンドを起動し、group を組み立てる役 | パイプ全体を同じ group に入れる |
daemon |
端末から独立して動く常駐プロセス | 元の端末の Ctrl-C は届きにくい |
関係を図にするとこうです。
図中の略称だけ補足します。
| 図中ラベル | 意味 |
|---|---|
session |
session leader は login shell など |
fg PG |
foreground process group |
bg PG |
background process group |
Ctrl-C は何をしているのか
よく「Ctrl-C でプロセスを kill する」と言いますが、実際に起きていることは次です。
| 操作 | 実際の動作 |
|---|---|
Ctrl-C |
端末ドライバが foreground process group に SIGINT を送る |
kill PID |
指定した PID にシグナルを送る |
kill -- -PGID |
指定した process group 全体にシグナルを送る |
つまり Ctrl-C は「今この端末の前面で動いている group に割り込みを送る」動作です。
親プロセスだけを狙っているわけではありません。
なぜパイプ全体が止まるのか
たとえば次を考えます。
sleep 100 | cat >/dev/null
シェルはこの 2 つを起動するとき、通常は同じ process group に入れます。
そのため Ctrl-C が飛ぶ先は個々の PID ではなく、その PGID 全体です。
流れを図にするとこうです。
図では shell を別に置いていますが、Ctrl-C の配送先は shell ではなく fg PG です。
ps で見ると確かに同じ group になる
バックグラウンドで起動して jobs と ps を見ると、同じ PGID になっていることが確認できます。
sleep 100 | cat >/dev/null &
jobs -l
ps -o pid,ppid,pgid,sid,tty,stat,comm --forest -g <PGID>
イメージは次のような出力です。
| PID | PPID | PGID | SID | COMMAND |
|---|---|---|---|---|
| 1001 | 999 | 1001 | 1001 | sleep |
| 1002 | 999 | 1001 | 1001 | cat |
ここで重要なのは PID が別でも PGID が同じ、という点です。
Ctrl-C で効いているのはこの PGID 側です。
session はどこで効いてくるのか
process group の外側には session があります。
session は controlling terminal と結びつく単位で、foreground process group はその中で前面実行されている group です。
判定の流れを表にするとこうです。
| レイヤ | 役割 | 例 |
|---|---|---|
| terminal | 入力元 | 今開いている端末 |
| session | その端末に属するまとまり | ログインシェルの世界 |
| process group | 前面実行や停止制御の単位 | パイプ全体のジョブ |
| process | 実際の個々の実行単位 |
sleep proc, cat proc など |
このため Ctrl-C を理解するときは、PID だけでなく terminal -> session -> process group -> process の順で見ると混乱しにくいです。
daemon が止まらない理由
daemon は端末から独立した常駐プロセスです。
多くの場合、次のような手順で元の端末から切り離されます。
| 手法 | 何をするか | 効果 |
|---|---|---|
nohup |
SIGHUP を無視する |
端末クローズ耐性を上げる |
setsid |
新しい session と process group を作る |
元の端末から切り離す |
| daemon 化 |
fork、setsid、標準入出力切り離し |
常駐プロセス化する |
特に重要なのは setsid です。
これで新しい session に入ると、元の端末の foreground process group とは別世界になります。
ここでは child proc が setsid() を呼ぶことで new session 側へ移り、元の terminal からの SIGINT は old fg PG にだけ届きます。new session 側にはその配送経路がありません。
nohup だけでは別の話
混ざりやすいので、nohup も並べておきます。
| コマンド | 主に防ぐもの |
Ctrl-C への効き方 |
|---|---|---|
nohup cmd |
端末切断時の SIGHUP
|
Ctrl-C から完全独立するとは限らない |
setsid cmd |
端末との結びつき | 元の端末の Ctrl-C が届きにくくなる |
nohup は「端末を閉じたとき対策」であって、Ctrl-C 対策そのものではありません。
まとめて止めたいときの実務メモ
関連する一式を止めたいときは、PID ではなく PGID を見ると整理しやすいです。
kill -INT -- -<PGID> # Ctrl-C 相当
kill -- -<PGID> # 既定は SIGTERM
kill -KILL -- -<PGID> # 最終手段
Bash のジョブ制御を使っているなら、ジョブ番号で扱うのも手軽です。
sleep 100 | cat >/dev/null &
kill %1
使い分けを表にするとこうです。
| 止め方 | 向いている場面 |
|---|---|
kill PID |
単独プロセスを止めたい |
kill -- -PGID |
パイプや関連ジョブをまとめて止めたい |
kill %1 |
今のシェルのジョブとして止めたい |
まとめ
-
Ctrl-Cが送るのはSIGKILLではなくSIGINT - 宛先は親プロセスではなく foreground process group
- パイプ全体が止まるのは、シェルが同じ process group にまとめるから
-
daemonが残るのは、sessionごと端末から独立するから - まとめて制御したいときは
PIDよりPGIDを意識すると整理しやすい
Linux のプロセス管理は PID だけを見ていると分かりにくいですが、session と process group を一段上の単位として見ると、Ctrl-C の挙動がかなり素直に見えるようになります。