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

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

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

Linuxで「新しいプログラムを実行する」という当たり前の動作は、実は二つのシステムコール(syscall)の組み合わせで成り立っています。プロセスを複製するfork(fork)と、複製したプロセスの中身を別のプログラムに入れ替えるexec(exec)です。さらに、起動した子の最後を見届けるwaitを加えた三段構えが、自分でプロセスを起こすプログラムの基本骨格になります。この「いったん複製してから中身を入れ替える」という一見まわりくどいやり方こそ、Unix系OSがプロセス生成を柔軟かつ堅牢に保ってきた設計の核心です。あなたがシェルでコマンドを打つたびに起きているのも、まさにこの流れそのものです。

fork ― 分身を作る

forkは、呼び出したプロセスをほぼ丸ごと複製し、もう一つのプロセスを生み出すシステムコールです。複製した側を親プロセス、生み出された側を子プロセスと呼びます。子は親のメモリ内容やプログラムカウンタをそっくり受け継ぐため、forkが返った直後は、親も子も同じコードの同じ場所から実行を続けます。では、どうやって自分が親なのか子なのかを見分けるのか。鍵はforkの戻り値です。親には、生まれた子のプロセスID(PID)という正の値が返り、子には0が返ります。複製に失敗した場合は-1が返ります。この値の違いを使い、if (fork()) のように、戻り値が0以外なら親の処理、0なら子の処理、と条件分岐で書き分けるのが定石です。親子の違いは基本的にPIDと、親を指すPPID(親プロセスID)だけで、子のPPIDは親のPIDと一致します。これが親子関係の証拠になります。なお現代のforkはコピーオンライトという賢い仕組みを使っていて、複製した直後は親子で同じメモリを共有し、どちらかが書き換えた瞬間に初めて、その書き換えた部分だけを実際に複製します。これにより、大きなプロセスをforkしても無駄なメモリ消費を最小限に抑えられます。

exec ― 中身を入れ替える

forkで作っただけの子は、まだ親のプログラムのコピーにすぎません。ここでexec系のシステムコールを呼ぶと、そのプロセスの実行イメージが、指定した別の実行ファイルに丸ごと置き換わります。中核となるのがexecveで、man 2 execveで引くと、実行ファイルのパス、コマンドライン引数の配列、環境変数の配列を渡すことが分かります。ここで決定的に重要なのは、execは「新しいプロセスを作る」のではなく「いまのプロセスの中身を入れ替える」点です。そのため、入れ替えてもプロセスIDは変わりません。生成ではなく変身なのです。そして成功するとイメージが完全に置き換わるため、execより後ろに書いたコードは二度と実行されません。逆に言えば、execの直後のコードに処理が到達したということは、それはexecが失敗した動かぬ証拠です。だからexecの直後にはエラー処理を置く、というのがお決まりの書き方になります。execveのほかにも、引数の渡し方や、環境変数を引き継ぐか、PATHからコマンドを探すかが異なるexecl・execlp・execv・execvpなどの仲間があります(末尾がpの系統はPATHを探索します)。この「分身(fork)してから変身(exec)する」二段構えにより、子を起こす段階と、何のプログラムに化けさせるかという段階を分けて扱えるわけです。

wait ― 子を見送り、ゾンビを防ぐ

子を起こしたら、親には最後の責任が残ります。waitやwaitpidで子の終了を待ち受け、その終了ステータスを回収する後始末です。waitは子が終わるまで親を一時停止させ、終了した子のPIDを返します。回収したステータスからは、専用のマクロを使って情報を取り出します。WIFEXITEDで正常終了かどうかを調べ、正常ならWEXITSTATUSで終了コード(0〜255の範囲)を取り出します。シグナルによる終了かどうかはWIFSIGNALEDで、そのシグナル番号はWTERMSIGで分かります。この後始末を怠るとどうなるか。終了した子は、終了情報だけを残した抜け殻、すなわちゾンビプロセスとして居座り続けます。psで見るとステータス欄がZ、コマンド名のうしろに<defunct>と表示されるのが目印です。ゾンビが一つ二つあるだけなら実害は小さいですが、待ち合わせを忘れたプログラムが子を量産し続けると、プロセス番号の枠を食い潰してシステムに支障をきたします。なお、親が子より先に終わってしまった場合、その子は孤児(orphan)と呼ばれ、ただちにPID 1の初期化プロセス(systemdなど)に引き取られます。引き取り先がきちんとwaitしてくれるため、孤児はゾンビにはなりません。

三つが一本につながる ― シェルの正体と観察

別々に学んだfork・exec・waitは、実は一連の流れとして組み合わさって使われます。あなたがシェルでlsと打つたび、シェルは内部で次の三段階を踏んでいます。まずforkで自分の分身(子)を作り、次にその子の中でexecしてlsという別プログラムに変身させ、最後に親であるシェルはwaitでlsの終了を待ってからプロンプトを返します。コマンドの末尾に&を付けてバックグラウンド実行すると、最後の待ち合わせをすぐには行わず、プロンプトがすぐ戻ってくる、という違いになります。この「分身→変身→待ち合わせ」のパターンは、自分でプロセスを起動する多くのプログラムに共通する土台です。組込みの世界でも、メインのプログラムが外部のコマンドや別の実行ファイルを起動するときには、内部でこの三段階が走っています。実際の動きは、gccでコンパイルした自作プログラムをstrace -fで追うと、自分の目で確かめられます。-fは子プロセスまで追跡するオプションで、これを付けるとforkで分かれた先の動きまで見逃さずに追え、clone(現代のLinuxにおけるforkの実体)・execve・wait4(waitの実体)といった呼び出しが順番に並ぶ様子を観察できます。fork・exec・waitという三つの部品が、頭の中の理屈ではなく、実際に発行される一連のシステムコールとしてつながって見えたとき、プロセス生成の仕組みが本当に自分のものになります。

この項目に出てくる用語

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

関連コマンド

stracegcc./a.out

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