さて、前回までで fork とかファイルとかのことはだいたいわかってきたかと思います。今回は、プロセスの「生から死まで」の一生について詳しく見ていきましょう。特に、「親が死んだ子供は養子になるしかない」「子供が親の見てないところで死ぬとゾンビになってしまう」という、プロセスの終了にまつわる複雑な状況について理解を深めます。
まず、プロセスの一生を整理してみましょう:
生成 → 実行 → 終了 → 回収
↓ ↓ ↓ ↓
fork running exit wait
1. 生成(fork)
- 親プロセスがforkシステムコールで子プロセスを作成
- 子プロセスは親のメモリ空間をコピーして独立した環境を獲得
2. 実行(running)
- プロセスが実際に処理を行っている状態
- 走行中、待ち状態、ブロック中の3つの状態を循環
3. 終了(exit)
- プロセスが処理を完了し、exitシステムコールで終了
- しかし、この時点ではまだ完全には消滅していない
4. 回収(wait)
- 親プロセスがwaitシステムコールで子の終了を確認
- この段階で初めてプロセスが完全に消滅
重要なのは、プロセスはexitしただけでは完全に消滅しないということです。親プロセスによる「回収」が必要なのです。
プロセスツリーにおいて、親プロセスが子プロセスより先に死んでしまった場合、どうなるでしょうか?この状況を「孤児プロセス」と呼びます。実例で見てみましょう。
# 親が先に死んで子が孤児になるサンプル
pid = fork
if pid
# 親プロセス:waitpidで子を待たずに先に終了
sleep 1 # 1秒待ってから終了
exit # 子をwaitせずに親が先に死んでしまう
else
# 子プロセス:親が生きている間と死んだ後の親のPIDを確認
# Process.ppid:親プロセスのPIDを取得
puts Process.ppid # 親が生きている時の親のPID
# 親が死ぬまで待つ(2秒 > 1秒なので親は先に死ぬ)
sleep 2
# 親が死んだ後の親プロセスPID(initプロセスの1になる)
puts Process.ppid
endさて、上のような Ruby スクリプトを実行すると、結果はどうなるでしょうか。
実行結果の流れ:
- 子プロセスの1回目の
Process.ppid:親プロセスが生きているので、そのPIDが表示される - 1秒後:親プロセスが終了し、プロンプトが戻ってくる
- さらに1秒後:2回目の
Process.ppidが実行され、プロンプトが戻った画面に「1」と表示される
なぜ「1」が表示されるのでしょうか?
親プロセスに先立たれた子プロセス(孤児プロセス)は、initプロセス(PID=1)が代わりに親となって面倒を見てくれるのです。これは単なる設計上の親切ではなく、システムの安定性のために必要不可欠な仕組みです。
なぜinitが養子縁組をするのか:
- すべてのプロセスには親が必要(プロセスツリーの整合性維持)
- 子プロセスが終了したとき、誰かがwaitして回収する必要がある
- 孤児プロセスを放置すると、システムリソースの管理が困難になる
initプロセスは「プロセスの里親」として、孤児となったすべてのプロセスを引き取り、適切に管理します。
孤児プロセスとは逆の状況もあります。子プロセスが終了したにもかかわらず、親プロセスがwaitしてくれない場合です。この状況で生まれるのが「ゾンビプロセス」です。
ゾンビプロセスは以下の特徴を持ちます:
- 既に実行は終了している:プロセス自体はexitして処理を終えている
- 完全には消滅していない:親プロセスがwaitしていないため、プロセステーブルに残っている
- リソースは解放済み:メモリやファイルディスクリプタは既に解放されている
- 終了ステータスだけが残存:親プロセスが受け取るための終了コードだけが保持されている
つまり、ゾンビプロセスは「死んでいるけど成仏できない」状態なのです。コードで見てみましょう
# ゾンビプロセスを作るサンプル
pid = fork
if pid
# 親プロセス:子のPIDを表示してから無限ループ
puts pid # 子プロセスのPIDを表示
# 無限ループで忙しくて子プロセスをwaitしない
# このため子が死んでも回収されずゾンビになる
loop do
sleep
end
else
# 子プロセス:すぐに終了するが親にwaitされない
exit # 即座に終了(しかしゾンビになる)
end上記のようなスクリプトを zombie.rb として保存して、バックグラウンド実行してみましょう
$ ruby zombie.rb &親プロセスの puts pid が利いて、子プロセスのpidが出力されたかと思います。さて、この親プロセスは、まだバックグラウンドで無限ループしています。一方、子プロセスは即 exit しているので、もう実行が終了しています。しかし、親はこの終了を wait していません。この子プロセスは、実行がおわってもう死んでいるのに、誰にも看取られていない(wait されていない)状態です。
そこで、先ほどターミナルに表示された pid がどうなっているのか、ps コマンドで確認してみましょう。
$ ps <さっきターミナルに表示されたpid>どうなりましたか?環境によって多少の違いはあるかもしれませんが、私の環境では
PID TTY STAT TIME COMMAND
3668 pts/2 Z 0:00 [ruby] <defunct>
と表示されました。STAT の部分に Z と出ていますね。これは、このプロセスがゾンビプロセスとなっていることを表します。
それでは、無限ループ中の親プロセスを fg でフォアグラウンドに戻して、Ctrl + Cで止めましょう。その状態で再度 ps でプロセスの状態を見てみると、さっきまでゾンビだったプロセスも、無事に成仏してなくなっていることが確認できると思います。
何が起こったのか:
- 親プロセスの終了:Ctrl+C で親プロセスが終了(子をwaitせずに死亡)
- ゾンビプロセスの孤児化:ゾンビ状態だった子プロセスが孤児プロセスになる
- initによる養子縁組:initプロセスが孤児となったゾンビプロセスの親になる
- 即座のwait:initプロセスが即座にwaitを実行してゾンビプロセスを回収
- 完全な消滅:ゾンビプロセスが無事に成仏
この仕組みにより、最終的にはすべてのゾンビプロセスがinitプロセスによって回収されることが保証されています。
今回学んだプロセスのライフサイクルから、以下の重要なポイントが理解できます:
プロセスの終了は単純な「消滅」ではなく、以下の2段階で行われます:
- 第1段階(exit):プロセス自身が終了を宣言、リソースを解放
- 第2段階(wait):親プロセスが終了を確認、プロセステーブルから除去
この2段階構造により、親プロセスは子プロセスの終了ステータスを確実に受け取れます。
孤児プロセス(親が先に死ぬ)
- 問題:子プロセスの管理者がいなくなる
- 解決:initプロセスが自動的に養子縁組して管理
ゾンビプロセス(子が死んでも親がwaitしない)
- 問題:プロセステーブルにエントリが残り続ける
- 解決:親プロセス終了時にinitが孤児ゾンビを回収
プログラマーとして以下を心がけるべきです:
- 子プロセスをforkしたら必ずwaitする:ゾンビプロセスの発生を防ぐ
- 適切なタイミングでのwait:長時間応答しない子プロセスへの対処
initプロセス(PID=1)は単なる「最初のプロセス」ではなく、以下の重要な役割を持ちます:
- プロセスツリーの根:すべてのプロセスの最終的な祖先
- 孤児プロセスの里親:親を失ったプロセスの養子縁組
- ゾンビ回収係:放置されたゾンビプロセスの最終的な回収
このように、Unix系OSのプロセス管理は非常によく設計されており、様々な異常状況でもシステムの安定性が保たれるようになっています。
次回はようやくシグナルについて書く予定です。共有メモリの話と、スレッドの話もその後にできればしたいけど、気力ないかもしれないので次回が最終回の可能性が微レ存……