Dev Study
← OSとカーネルの土台
レッスン 3 / 10·約 9 分·第10期 OSとカーネルの土台

プロセスは複製で生まれる — forkとexecが分かれている理由

アプリと DB の下に何が積まれているか(左が下、右が上)
  1. 物理機リージョン/AZ
  2. ハイパーバイザVM exit で捕まえる
  3. VMゲストOS
  4. コンテナnamespace / cgroup
  5. プロセスアプリ / DB今ここ

本コース1本目『カーネルはどこで動いているのか』で、ps の角括弧の意味を見ました。今回はプロセスの生まれ方と終わり方を見ます。

まず結論

新しいプロセスは無から起動されるのではなく、既存のプロセスの複製として生まれます(`fork`)。別のプログラムにするのは中身の入れ替え(`execve`)で、これは別の呼び出しです。

終了した子は、親が回収するまで一覧に残ります。これがゾンビで、異常ではなく回収前の段階です。

? 読む前に予測

1回の `fork()` の呼び出しは、いくつの値を返すでしょうか。

当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。

抽象原理

プロセスは、既にあるプロセスの複製として生まれる

本コース1本目『カーネルはどこで動いているのか』で、ps の見え方を扱いました。では、そこに並ぶプロセスはどこから来たのでしょうか。

「OSがプログラムを読み込んで起動する」と考えたくなりますが、UNIX系OSの答えは違います。新しいプロセスは、既にあるプロセスの複製として生まれます。 fork(2) の DESCRIPTION はこう始まります。

fork() creates a new process by duplicating the calling process.

複製なので、生まれた直後の子は親と同じプログラムを同じ続きから動かします。では別のプログラムはどう動かすのか。答えは別の呼び出しで、execve(2) が中身を入れ替えます。

This causes the program that is currently being run by the calling process to be replaced with a new program

複製する役と、入れ替える役が分かれている。 これが起動が2段になっている理由です。

新しいプログラムを別プロセスで動かすまで
元のプロセスプログラムA
fork() — 複製する
親A / 戻り値 = 子のPID
子A / 戻り値 = 0
子の側で execve() — 中身を入れ替える
親A のまま
子B に置き換わる
  • ・fork は1回の呼び出しで2か所に戻る。戻り値が違う
  • ・execve はプロセスを増やさず中身だけを置き換える。成功すると戻らない

第12週『OSの役割 — 1つのCPUを、みんなで分け合う』で、プロセスもスレッドも同じ構造で管理され、違いは clone に渡すフラグだけだと見ました。複製の度合いを選べると言い換えれば、fork はその一番よく使う組み合わせです。

具体実物

1回の fork が2つの値を返すところを実際に見る

fork を1回だけ呼んで子を1つ作り、その子がすぐ終了するプログラムを動かしました。ループはありません。 繰り返し fork するコードはプロセスが指数的に増えるので書きません。

fork → ゾンビ → 回収(生ログ全文。Apple Silicon / Docker / alpine:3 / kernel 6.10.14-linuxkit aarch64, 2026-09-17)コマンド出力
before fork: getpid()=1 getppid()=0
PARENT: fork() returned 16, getpid()=1
CHILD : fork() returned 0, getpid()=16 getppid()=1
--- child should now be a zombie; parent has not called wait() ---
PID PPID STAT COMMAND COMMAND
16 1 Z forkdemo <defun [forkdemo] <defunct>
16 (forkdemo) Z 1
--- after waitpid: reaped=16 WIFEXITED=1 WEXITSTATUS=42 ---
PID PPID STAT COMMAND
ps exit=1

2行目と3行目を見比べてください。同じ fork() が、親には 16 を、子には 0 を返しています。 子の getpid() は 16 で親が受け取った値と一致し、getppid() は 1 で親のPIDと一致します。

PID が 1 と 16 と小さいのはコンテナの中だからです。普通の Linux ならもっと大きな値になります。

✓ ここまでの確認

`fork()` の直後、親と子はどちらも同じ次の行から再開します。自分が親か子かをプログラムはどう判別しますか。

抽象原理

終わった子は、親が回収するまで残る

上のログにはもう1つの論点が写っています。子は _exit(42) で終了済みなのに ps にまだ出ています。wait(2) の NOTES がこの状態を定義しています。

A child that terminates, but has not been waited for becomes a "zombie". The kernel maintains a minimal set of information about the zombie process (PID, termination status, resource usage information) in order to allow the parent to later perform a wait to obtain information about the child.

残す理由が明記されています。親が後から wait して子の情報を取れるようにするためです。残るのはPID・終了ステータス・資源使用量だけで、メモリは解放済み。異常ではなく、終了と回収の間に必ず通る段階です。ただし同じ NOTES は放置した場合の帰結も書いています。

As long as a zombie is not removed from the system via a wait, it will consume a slot in the kernel process table, and if this table fills, it will not be possible to create further processes.

1つあることは正常、増え続けることは問題。 この書き分けが要点です。

具体実物

ゾンビの見え方と、回収された瞬間

生ログの6行目です。

   16     1 Z    forkdemo <defun [forkdemo] <defunct>

読み方は3か所です。STAT が Z — proc_pid_stat(5) の state フィールドの定義で Z は Zombie。コマンド欄が <defunct>。そして角括弧が付いています。

この角括弧は1本目の論点の裏付けです。これはカーネルスレッドではなく、ただの終了したユーザプロセスです。ps(1) は「args が取得できないとき、代わりに実行ファイル名を角括弧で囲んで表示する」と定めており、ゾンビは /proc/PID/cmdline が空なのでこの規定どおり角括弧で出ます。角括弧はカーネルスレッドの印ではありません。

7行目 16 (forkdemo) Z 1 は /proc/16/stat の先頭4フィールドで、3番目が Z、4番目が親のPID 1。ps と一致します。

8〜10行目では、親が waitpid を呼ぶと 16 が返り、WIFEXITED=1 / WEXITSTATUS=42 — 子が _exit(42) で渡した値を受け取れています。直後の ps -p 16 はヘッダ以外何も表示せず終了コード 1。回収した瞬間にゾンビが消えたということです。

✓ ここまでの確認

「`execve` は新しいプロセスを作る」という説明の評価はどれですか。

⇄ 2つを見比べる
具体例 A

fork — 同じプログラムが2つになる

呼んだプロセスの複製が1つできて2つになります。プログラムも実行位置も同じで、違うのは戻り値だけ。親は子のPIDを、子は0を受け取り、それを見て自分がどちら側かを判別します。

具体例 B

execve — プロセスは1つのまま中身が変わる

プロセスは1つのままで増えません。呼んだプロセスが動かしているプログラムが、指定したものに置き換わります。成功すると戻る先のコードがもう存在しないので戻らず、失敗したときだけ -1 が返ります。

fork と execve。どちらも「プログラムを動かす」話に見えますが、2つを分けておくことに何の意味があるのでしょうか。

片方だけを呼ぶ場面を想像してみてください。それぞれ何ができるでしょうか。

この章のまとめ

  • 新しいプロセスは既存プロセスの複製として生まれる。fork(2) は「呼び出したプロセスを複製して新しいプロセスを作る」と定義されている。
  • 1回の fork() が2か所に戻る。 親には子のPID、子には 0。失敗時は親に -1 で子は作られない。実測でも親に 16、子に 0 が返った。
  • execve(2) はプロセスを作らない。 プログラムが「新しいプログラムに置き換えられる」。成功すると戻らない。
  • だから「新しいプログラムを別プロセスで動かす」は複製と入れ替えの組み合わせ。分かれているから、その隙間で子の設定を整えられる。
  • 終了した子は親が回収するまでゾンビとして残る。保持されるのはPID・終了ステータス・資源使用量だけ。1つあるのは正常、増え続けるのが問題。 親が先に死んでも init(1) か最寄りの subreaper が回収する。
  • ゾンビも [forkdemo] <defunct> のように角括弧で出る。角括弧はカーネルスレッドの印ではない。

自分の現場で確かめる

読んだだけでは身につきません。手元の環境で実際に見てみてください。

環境:

理解度チェック

読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。

問題 1 / 3正解: 0

新しいプログラムを別プロセスとして動かすとき、UNIX系OSで必要な操作の組み合わせはどれですか。

用語集(3語)
fork(2)
呼び出したプロセスを複製して新しいプロセスを作るシステムコール。man-pages では子は親の正確な複製(exact duplicate)とされ、例外項目だけが異なる。成功時、親には子のPIDが、子には0が返る。
execve(2)
呼び出したプロセスが動かしているプログラムを、指定したものに置き換えるシステムコール。プロセスは新しく作られない。成功すると戻らず、失敗時だけ -1 が返る。
ゾンビ (zombie) / 回収 (reap)
終了したが親にまだ `wait` されていない子プロセスがゾンビ。カーネルはPID・終了ステータス・資源使用量だけを保持する。`ps` の STAT では `Z`、コマンド欄では `<defunct>`。親が `wait` を呼んでこれを取り除くのが回収で、行わないとプロセステーブルの枠が消費され続ける。