プロセス生成の内部(fork/clone/vfork/exec)
fork が巨大プロセスでも一瞬で複製できる理由を、CoWとページテーブルの実装から腑に落とせます。clone のフラグ設計、vfork の最適化、exec のイメージ置換まで原理から押さえます。
- 1.fork はアドレス空間を即コピーせず、ページテーブルだけ複製して両者を読み取り専用で共有し、書き込み時にCoWで該当ページだけ分けます。
- 2.Linuxでは fork/vfork/pthread_create はすべて clone の薄いラッパーで、CLONE_VM/FILES/THREAD等のフラグで「どのリソースを共有するか」を選ぶだけの違いです。
- 3.vfork は親を止めて子が exec/exit するまでアドレス空間を生で共有し、exec はページテーブルを丸ごと捨てて新イメージで張り直すためCoWのコストすら払いません。
プロセス生成は「複製」と「置換」の二段構え
UNIX 系でプログラムを起動する典型は fork() で自分を複製し、子側で exec() して別プログラムに化ける、という二段構えです。なぜ「複製してから置換」という一見無駄な手順を踏むのか。fork と exec を分離しておくと、両者の間でファイルディスクリプタの差し替えやリダイレクトなど、子の実行環境を親が自由に整えられるからです。シェルのパイプやリダイレクトはこの隙間で実現されています。
本稿の主役は、この複製が 巨大プロセスでも一瞬で終わる からくりです。鍵は「アドレス空間を本当にコピーしない」こと。前提としてプロセスのアドレス空間とコピーオンライト(CoW)を押さえておくと理解が早まります。
fork の内部:ページテーブルだけを複製する
fork がやることは、直感に反して メモリのコピーではありません。実体(物理ページ)はそのまま、ページテーブルだけを複製 し、親子の全ての書き込み可能ページを 読み取り専用 に落とします。
- 子の
task_struct(プロセス記述子)を確保し、親の多くのフィールドを引き継ぐ - 親のページテーブルを走査し、子用に同じ物理ページを指すエントリを作る
- 書き込み可能だったページは、親子双方 のエントリを read-only かつ CoW 印付きにする
- 各物理ページの参照カウントを増やす(複数プロセスが共有中だと記録する)
この時点でコピーされたのは管理構造(ページテーブル)だけで、データ本体は1バイトも複製されていません。だから 10 GB のプロセスでも fork はミリ秒で返ります。
fork 直後(共有・read-only):
親 PTE ─┐
├─▶ [物理ページ X](rw→ro, CoW印) refcount=2
子 PTE ─┘
子が書き込んだ瞬間(ページフォルト→CoW):
親 PTE ───▶ [物理ページ X](refcountが1に戻れば rw へ復帰)
子 PTE ───▶ [物理ページ X' のコピー](rw)
実際にどちらかが書き込むと、read-only ページへの書き込みなので CPU が ページフォルト を起こし、カーネルがそのページだけ複製して書き込み側に専用ページを与えます。詳細はデマンドページングとページフォルトと同じ機構です。
fork のコストはプロセスサイズではなく「フォルトを起こすページ数」にほぼ比例します。子が exec で別プログラムに化けるつもりなら、複製したページの大半は触られないまま捨てられます。この「どうせ捨てるものをコピーしない」性質が、後述の vfork/exec 最適化の動機です。
clone:fork も pthread も同じ一つの入口
Linux では fork・vfork・pthread_create はそれぞれ別の仕組みではなく、すべて clone()(内部的には kernel_clone)という1つのシステムコールのラッパー です。違いは「どのリソースを親と共有するか」を指定するフラグだけです。プロセスとスレッドの区別が Linux では連続的になる理由がここにあります(プロセスとスレッド)。
| CLONE フラグ | 共有されるもの | 立てないと(=複製) |
|---|---|---|
| CLONE_VM | アドレス空間(mm_struct) | CoWで別空間として複製 |
| CLONE_FILES | ファイルディスクリプタ表 | fd表を複製(fork時の既定) |
| CLONE_FS | cwd・ルート・umask | fs情報を複製 |
| CLONE_SIGHAND | シグナルハンドラ表 | ハンドラ設定を複製 |
| CLONE_THREAD | 同一スレッドグループ(共有PID) | 新しいPIDの別プロセスに |
組み合わせを変えるだけで生成物の性質が変わります。フラグをほぼ立てなければ独立プロセス(=fork)、CLONE_VM | CLONE_FILES | CLONE_FS | CLONE_SIGHAND | CLONE_THREAD をまとめて立てれば、アドレス空間もファイルも共有する スレッド になります。「スレッドはアドレス空間を共有するプロセス」という説明は、この CLONE_VM の有無として実装に直接対応しています。
/* スレッド生成は概ねこのフラグ集合(実際は glibc が組み立てる) */
clone(fn, stack,
CLONE_VM | CLONE_FS | CLONE_FILES |
CLONE_SIGHAND | CLONE_THREAD, /* mm/fd/シグナル/PIDを共有 */
arg);
CLONE_VM を立てた場合はページテーブルの複製すら起きません。親子(=スレッド)が 同一の mm_struct を共有 し、参照カウントを増やすだけで済みます。コンテナを支える CLONE_NEWPID 等の名前空間フラグも同じ枠組みで、何を分離するかを選びます(名前空間と cgroups)。
vfork:CoW のコストすら惜しむ最適化
fork の CoW は速いとはいえ、ページテーブルの複製と read-only 化、そして直後に exec すれば全部捨てるという無駄が残ります。vfork はこれを極限まで削る最適化です。
vfork は アドレス空間を一切複製せず、親子が生のまま同じ mm_struct を共有 します(CLONE_VM | CLONE_VFORK)。その代わり、子が exec か _exit を呼ぶまで 親は強制的に休止 させられます。親子が同じメモリ・同じスタックを同時に走ると破壊し合うため、親を止めて衝突を防ぐわけです。
vfork した子は、共有スタックを壊さないため exec か _exit 以外をしてはいけません。return で関数から戻る、ローカル変数へ代入する、vfork を呼んだ関数のスタックフレームを変更する——いずれも親のスタックを破壊し未定義動作になります。現代では fork の CoW が十分速いため、posix_spawn の内部実装など限られた用途以外で vfork を直接使う理由はほとんどありません。
つまり vfork は「どうせ exec で捨てるなら、コピーも read-only 化もせず、親を止めて一瞬だけメモリを貸す」という割り切りです。直後に exec するという前提があるからこそ成立する最適化です。
exec:ページテーブルを丸ごと捨てて張り直す
exec は 新しいプロセスを作りません。呼び出したプロセスの中身を、指定した実行ファイルのイメージで 丸ごと置き換える 操作です。PID は変わらず、メモリの中身だけが入れ替わります。
カーネルが行うのは概ね次の流れです。
- 実行ファイルのヘッダ(ELF など)を読み、フォーマットを判定する
- 現在のアドレス空間(古い
mm_struct、全マッピング)を 解体して破棄 する - 新しい空のアドレス空間を作り、ELF のセグメント(text/data 等)を ファイルからメモリマップ する
- スタックを作り、引数
argvと環境変数envp、補助ベクタを積む - エントリポイントへ制御を移す(実コードはデマンドページングで遅延ロード)
ここで重要なのは、exec が 古いアドレス空間を捨てる こと。だから fork 直後に exec すると、CoW で苦労して read-only 共有していたページも、結局フォルトを起こす前に全部破棄されます。vfork が「最初からコピーしない」と割り切れるのは、この破棄が確実だからです。
| 観点 | fork | vfork | exec |
|---|---|---|---|
| 新プロセス | 作る(PID増) | 作る(PID増) | 作らない(PID不変) |
| アドレス空間 | CoWで複製 | 親と生で共有 | 破棄して新規構築 |
| 主なコスト | ページテーブル複製+書込時フォルト | ほぼゼロ(親は休止) | 旧空間解体+新ELFマップ |
| 典型用途 | サーバの子プロセス・並行処理 | 直後にexecする橋渡し | 別プログラムへの変身 |
実行ファイルのコード本体は exec の瞬間に全部読み込まれるのではなく、ファイルとして mmap され、実際に実行されたページから デマンドページング で順次ロードされます。これにより、使われない関数のコードはディスクから読まれずに済みます。
「fork はメモリ全体をコピーする」は誤り——コピーするのはページテーブルで、データはCoWで遅延複製される。fork の戻り値は親では子のPID、子では0、失敗時は-1で1回の呼び出しで2回返るように見える。exec は成功すると元のコードへ戻らない(戻り値がないのは置換が起きるため)。子の終了ステータスを親が wait で回収しないとゾンビプロセスが残る、という関係も併せて問われやすい。
まとめ
fork はアドレス空間を即コピーせず、ページテーブルだけ複製して親子を read-only で共有し、書き込み時にCoWで該当ページだけ分けるため巨大プロセスでも一瞬で返ります。Linuxでは fork/vfork/pthread_create はすべて clone のラッパーで、CLONE_VM/FILES/FS/SIGHAND/THREAD 等のフラグで「何を共有するか」を選ぶだけの違いです。vfork は親を止めてアドレス空間を生で共有しCoWのコストすら省き、exec は旧空間を破棄して新ELFをマップし直すためCoWの複製は無駄に終わります。土台はプロセスのアドレス空間とコピーオンライト(CoW)で確認できます。
OS Article
プロセス生成の内部(fork/clone/vfork/exec)を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
fork
比較で見る軸
難易度: advanced / カテゴリ: OS / タグ数: 6
導入後に効く点
Linuxでは fork/vfork/pthread_create はすべて clone の薄いラッパーで、CLONE_VM/FILES/THREAD等のフラグで「どのリソースを共有するか」を選ぶだけの違いです。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- OS
- タグ数
- 6
判断チェックリスト
- 自社の用途が「fork / clone」に近いか確認する。
- 強みである「fork はアドレス空間を即コピーせず、ページテーブルだけ複製して両者を読み取り専用で共有し、書き込み時にCoWで該当ページだけ分けます。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。