🐧 Linux 総合学習プラットフォーム
システムコール ・ 上級

プロセス生成(fork/exec/wait)

Linuxで新しいプログラムを動かす基本は fork と exec の組み合わせです。fork は呼び出したプロセスをほぼ丸ごと複製し、親には子のPID、子には 0 を返すことで両者を区別させます。子プロセスが exec 系(execve など)を呼ぶと、自分の中身を別のプログラムに置き換えて実行を始めます。親は wait(waitpid)で子の終了を待ち受け、子の終了ステータスを回収します。これを怠ると子は「ゾンビプロセス」として残ります。シェルがコマンドを実行する仕組みも、まさにこの fork→exec→wait の流れです。

Linuxで「新しいプログラムを実行する」という当たり前の動作は、たった一つのシステムコールでは済まない。二つのシステムコール(syscall)の組み合わせで成り立っている。

一つはプロセスを複製するfork(fork)、もう一つは複製したプロセスの中身を別のプログラムに入れ替えるexec(exec)だ。

さらに、起動した子の最後を見届けるwaitを加えた三段構えが、自分でプロセスを起こすプログラムの基本骨格になる。

💡
ポイント新しいプログラムの実行は、fork(複製)→exec(中身を入れ替え)→wait(終了を見届ける)の三段構えで成り立つ。
親プロセス例:シェルfork親(そのまま)戻り値=子のPID子(分身)戻り値=0execls 等に変身親は wait で子の終了を待つ

一見まわりくどいやり方に見えるが、この「いったん複製してから中身を入れ替える」という流れこそ、Unix系OSがプロセス生成を柔軟かつ堅牢に保ってきた設計の核心だ。

あなたがシェルでコマンドを打つたびに起きているのも、まさにこの流れそのものだ。まずは一つめの部品、複製から見ていこう。

👯 fork は分身したあと、どちらが自分かをどう知るのか

forkは、呼び出したプロセスをほぼ丸ごと複製し、もう一つのプロセスを生み出すシステムコールだ。複製した側を親プロセス、生み出された側を子プロセスと呼ぶ。

子は親のメモリ内容やプログラムカウンタをそっくり受け継ぐ。そのためforkが返った直後は、親も子も同じコードの同じ場所から実行を続ける。

ここで素朴な疑問が湧く。同じ場所から続くなら、どうやって自分が親なのか子なのかを見分けるのか。

💡
ポイント見分け方はforkの戻り値だ。親には子のPID(正の値)、子には0、複製に失敗したら-1が返る。

鍵はforkの戻り値だ。親には、生まれた子のプロセスID(PID)という正の値が返り、子には0が返る。複製に失敗した場合は-1が返る。

if (fork()) ……戻り値が0以外(親)なら親の処理、0なら子の処理、と条件分岐で書き分けるのが定石。

この値の違いを使い、if (fork()) のように、戻り値が0以外なら親の処理、0なら子の処理、と条件分岐で書き分けるのが定石だ。

親子の違いは基本的にPIDと、親を指すPPID(親プロセスID)だけで、子のPPIDは親のPIDと一致する。これが親子関係の証拠になる。

丸ごと複製すると聞くと、メモリも倍食うのではと心配になるかもしれない。

🔗
たとえforkのコピーオンライトは「コピー用紙を取るのは書き換える瞬間まで待つ」やり方。複製直後は親子で同じメモリを共有し、書き換えた部分だけ実際に複製する。

なお現代のforkはコピーオンライトという賢い仕組みを使っていて、複製した直後は親子で同じメモリを共有し、どちらかが書き換えた瞬間に初めて、その書き換えた部分だけを実際に複製する。

これにより、大きなプロセスをforkしても無駄なメモリ消費を最小限に抑えられる。

🦋 exec は「作る」のではなく「化ける」

forkで作っただけの子は、まだ親のプログラムのコピーにすぎない。

ここでexec系のシステムコールを呼ぶと、そのプロセスの実行イメージが、指定した別の実行ファイルに丸ごと置き換わる。

中核となるのがexecveで、man 2 execveで引くと、実行ファイルのパス、コマンドライン引数の配列、環境変数の配列を渡すことが分かる。

💡
ポイントexecは「新しいプロセスを作る」のではなく「いまのプロセスの中身を入れ替える」。だから入れ替えてもプロセスIDは変わらない。生成ではなく変身だ。

ここで決定的に重要なのは、execは「新しいプロセスを作る」のではなく「いまのプロセスの中身を入れ替える」点だ。そのため、入れ替えてもプロセスIDは変わらない。生成ではなく変身なのだ。

そして成功するとイメージが完全に置き換わるため、execより後ろに書いたコードは二度と実行されない。

つまずきexecが成功すると後ろのコードは二度と実行されない。逆にexec直後のコードに到達したらexec失敗の動かぬ証拠。直後にエラー処理を置く。

逆に言えば、execの直後のコードに処理が到達したということは、execが失敗した動かぬ証拠だ。だからexecの直後にはエラー処理を置く、というのがお決まりの書き方になる。

execveのほかにも、引数の渡し方や、環境変数を引き継ぐか、PATHからコマンドを探すかが異なるexecl・execlp・execv・execvpなどの仲間がある(末尾がpの系統はPATHを探索する)。

この「分身(fork)してから変身(exec)する」二段構えにより、子を起こす段階と、何のプログラムに化けさせるかという段階を分けて扱える。

🪦 子を見送らないと居座り続けるもの

子を起こしたら、親には最後の責任が残る。waitやwaitpidで子の終了を待ち受け、その終了ステータスを回収する後始末だ。

waitは子が終わるまで親を一時停止させ、終了した子のPIDを返す。

回収したステータスからは、専用のマクロを使って情報を取り出す。WIFEXITEDで正常終了かどうかを調べ、正常ならWEXITSTATUSで終了コード(0〜255の範囲)を取り出す。

WIFEXITED ……正常終了か判定。WEXITSTATUS ……終了コード(0〜255)を取り出す。WIFSIGNALED ……シグナル終了か判定。WTERMSIG ……そのシグナル番号。

シグナルによる終了かどうかはWIFSIGNALEDで、そのシグナル番号はWTERMSIGで分かる。

この後始末を怠るとどうなるか。終了した子は、終了情報だけを残した抜け殻、すなわちゾンビプロセスとして居座り続ける。

つまずきwaitを怠ると終了した子がゾンビプロセスとして居座る。psでステータス欄がZ、コマンド名のうしろに<defunct>と出るのが目印。

psで見るとステータス欄がZ、コマンド名のうしろに<defunct>と表示されるのが目印だ。

ゾンビが一つ二つあるだけなら実害は小さいが、待ち合わせを忘れたプログラムが子を量産し続けると、プロセス番号の枠を食い潰してシステムに支障をきたす。

では逆に、親が子より先に死んでしまったら、その子はゾンビになるのか。ならない。

コツ親が先に終わった子は孤児(orphan)と呼ばれ、ただちにPID 1の初期化プロセス(systemdなど)に引き取られる。引き取り先がwaitするのでゾンビにはならない。

親が子より先に終わってしまった場合、その子は孤児(orphan)と呼ばれ、ただちにPID 1の初期化プロセス(systemdなど)に引き取られる。引き取り先がきちんとwaitしてくれるため、孤児はゾンビにはならない。

🐚 シェルの中で三つが一本につながる

別々に学んだfork・exec・waitは、実は一連の流れとして組み合わさって使われる。

あなたがシェルでlsと打つたび、シェルは内部で次の三段階を踏んでいる。

まずforkで自分の分身(子)を作り、次にその子の中でexecしてlsという別プログラムに変身させ、最後に親であるシェルはwaitでlsの終了を待ってからプロンプトを返す。

🔗
たとえ分身→変身→待ち合わせ。シェルがlsを実行する裏側そのもの。&を付けるとwaitをすぐ行わず、プロンプトがすぐ戻ってくる。

コマンドの末尾に&を付けてバックグラウンド実行すると、最後の待ち合わせをすぐには行わず、プロンプトがすぐ戻ってくる、という違いになる。

この「分身→変身→待ち合わせ」のパターンは、自分でプロセスを起動する多くのプログラムに共通する土台だ。

組込みの世界でも、メインのプログラムが外部のコマンドや別の実行ファイルを起動するときには、内部でこの三段階が走っている。

実際の動きは、gccでコンパイルした自作プログラムをstrace -fで追うと自分の目で確かめられる。

strace -f ……子プロセスまで追跡。clone(現代のLinuxにおけるforkの実体)・execve・wait4(waitの実体)が順番に並ぶ様子を観察できる。

-fは子プロセスまで追跡するオプションで、これを付けるとforkで分かれた先の動きまで見逃さずに追える。clone(現代のLinuxにおけるforkの実体)・execve・wait4(waitの実体)といった呼び出しが順番に並ぶ様子を観察できる。

fork・exec・waitという三つの部品が、頭の中の理屈ではなく、実際に発行される一連のシステムコールとしてつながって見えたとき、プロセス生成の仕組みが本当に自分のものになる。

この項目に出てくる用語

forkふぉーく
自分とほぼ同じ子プロセスを複製するシステムコール。
execいぐぜっく
現在のプロセスを別のプログラムに置き換えて実行する。
システムコールしすてむこーる
アプリがカーネルに処理を依頼する公式の窓口。

関連コマンド

stracegcc./a.out

▶ 学習アプリでこの続きを学ぶ・演習する