fork() + exec() の退場 — Linux カーネルは本当に新しい spawn モデルへ向かうのか

50 年前のプロセス生成慣用句は、もはやマルチスレッド時代の負荷に耐えられない — そういう合意が LWN の議論で形成された。しかし、その先の道は三つに分かれている。

導入 — 2 パーセントのパッチから始まった大きな問い

2026 年 6 月 5 日、LWN は Li Chen が送った spawn template パッチシリーズに関する分析記事を公開した。Chen の提案そのものは簡素である。fork() と exec() を組み合わせて呼ぶ代わりに、「プロセス発射台」 にあたる spawn template をカーネルにあらかじめ構成しておき、同じ実行ファイルを繰り返し起動する際にそのテンプレートを再利用しよう、というものだ。Chen が添付したベンチマークは約 2 パーセントの性能向上 — それ自体としては新しいシステムコールを二つ正当化するには到底足りない小さな数字だった。

しかし LWN のコメントスレッドは、その小さな数字を出発点にして、ただちにより大きな問いへと飛び移った。fork() + exec() という 50 年前の Unix の基本慣用句を、いまこそ本当に引退させるべきなのか。Mateusz Guzik の表現は断固たるものだった。「fork + exec という慣用句そのものが酷い。もはや引退すべき時だ。」 Christian Brauner は Chen の提案を受け入れなかったが、代替案として pidfd ベースの別の道を示した。Josh Poimboeuf は io_uring の上に新しいプロセス生成命令を載せるもう一つのシナリオを投げかけた。三つに分かれた道が、それぞれ異なるトレードオフを抱えて同じ舞台に上がってきたのである。本稿はその三つの道が指し示す未来を追う。

Li Chen の spawn template — 小さなパッチ、大きな問い

Chen が提出したパッチは二つのシステムコールから成る。spawn_template_create() は特定の実行ファイルに対する 「発射テンプレート」 をカーネル内に作る。環境変数、ファイルディスクリプタ設定、シグナル処理などをあらかじめ spawn_template_action 構造体に束ねておく。spawn_template_spawn() はそのテンプレートを用いて実際に新しいプロセスを立ち上げる。発想は fsconfig() / fsopen() / fsmount() 系列に似ている。繰り返し設定すべき項目を一度だけカーネルに登録しておき、呼び出し地点では軽いトリガーを引くだけにする、というやり方である。

ベンチマークが示した約 2 パーセントの改善は小さな数字だ。パッチを擁護する立場からは、同じ実行ファイルをリクエストごとに立ち上げるワークロード — 例えば CGI 時代の fork-per-request サーバや、Bazel の sandbox ヘルパーのように短命な補助プロセスを大量に作り出すビルドシステム — で積み上がれば意味のある差になる、という主張がある。短い子プロセスが数百万個も生成される CI パイプラインを考えれば、2 パーセントはビルドファームを一列ぶん削減してくれる数字である。

問題は、レビュアーたちが同じ数字を正反対の角度から読んだ点にある。Mateusz Guzik の精読は、新しいインターフェースの複雑さと 2 パーセントという利得の隙間を正面から狙い撃った。新しいシステムコール二つ、新しい構造体、新しい資源寿命を導入するコストは永久に残るが、利得は特定パターンのワークロードに限られる。さらに glibc の posix_spawn() 実装は、同じ問題をユーザー空間で — やや鈍重ではあれ — 既に解いている。「コストが判断基準なら、もっと具体的な数字が必要だ」 というコメントがすぐさま続いた。

興味深いのは Chen 本人の反応である。Brauner の pidfd ベースの代替案を聞いたあと、Chen はその方向の方が良いと同意し、「今後の作業はそちらに向ける」 と述べた。パッチそのものはマージされないが、Chen の小さなパッチが投げかけた問いが、より大きな設計議論へと火を移したことになる。LWN は記事の最後の行でこう締めくくった。「spawn テンプレートが Linux カーネルに入ることはないだろう。しかし Chen の次の作業が結実すれば、Linux はついにまともな posix_spawn() 実装を持てるかもしれない。」

マルチスレッド時代の fork() — なぜ 「酷い」 のか

fork() が最初に導入された 1970 年代の PDP の Unix では、プロセスは軽く、メモリは高価で、スレッドは存在すらしていなかった。fork() は呼び出し元のアドレス空間を子にそのまま複製する素朴なパターンだった。1980 年代に copy-on-write (COW) が入り、子がすぐに exec() を呼んでメモリを全部捨てると分かっているカーネルは、実際の複製コストを回避できるようになった。50 年近く動いてきたこのパターンが、いまどうして 「酷い」 と言われるのか。

答えはマルチスレッドアプリケーションにある。LWN のコメントの一つはこう書いた。「vfork + exec はマルチスレッドアプリでは酷い。COW で一瞬使って捨てるためだけのページを山ほど作り出すか、さもなければ全スレッドを止めるかになる。」 fork() は呼び出し時点のすべてのスレッドを切り落とす。呼び出したスレッド一本だけが子プロセスへ追従し、残りは消える。このとき子プロセスが抱えている mutex や condition variable の状態はどこへ行くのか。他のスレッドがロックを掴んだまま切り落とされていたら、そのロックは永遠に解かれないまま子プロセスの中に残る。

ここで登場するのが async-signal-safe 関数の一覧である。fork() 後 exec() までの間に呼べる関数は、事実上シグナルハンドラから呼べる関数に限定される。printf() も駄目。malloc() も駄目。ライブラリの初期化コードはどれも安全ではない。POSIX 仕様はこの事実を明文化したが、現実のコードベースにはこの規則を守れずに起きるデッドロックが満ち溢れている。Python の multiprocessing が macOS で spawn を既定に切り替えた理由も、JVM が fork() と exec() の間に絶対に他の処理を挟まないよう強迫的にコードを組んできた理由も、すべて同じ落とし穴に発する。

vfork() は部分的な答えだった。親を一時停止させてメモリ複製を避け、exec() が終わるまで親スレッドを止める。コメントの中で一人の開発者が訂正したところによれば、vfork() は 「プロセス全体ではなく呼び出しスレッドだけを止める」 という。しかしその呼び出しスレッドが他の処理のクリティカルセクションにいたら、別のスレッドがそのクリティカルセクション直前で止まってデッドロックに陥る可能性がある。vfork() は綺麗な迂回路ではなく、また別の落とし穴である。さらに深刻なのは COW それ自体にもある。マルチスレッドプロセスが fork() を呼ぶと、親の全ページが一旦 read-only にマークされる。他のスレッドが一つでもページに書き込むたびにページフォールトが起き、ページが複製される。子が exec() を呼ぶまで、親は自分自身のメモリを一ページずつ複製するコストを丸ごと引き受ける。「山ほど作って即捨てる」 というコメントの表現は正確である。

posix_spawn() は POSIX が 1990 年代に既に認識していた答えである。親と子を明確に分離し、子の環境 — ファイルディスクリプタ、シグナルマスク、作業ディレクトリ — を呼び出し時点で attr 構造体として明示的に記述する。ただし LWN のコメントが指摘するように、glibc を含むほとんどの libc は posix_spawn() を内部的に vfork() + exec() で実装している。POSIX の抽象化が綺麗でも、その下にはなお fork() の影が敷かれている。本当の意味で 「ネイティブ」 な posix_spawn() — つまり fork() の意味論を経由せずに直接新しいプロセスを生成するカーネルレベルの実装 — は、いまだ Linux にない。今回の LWN の議論が最終的に向かおうとする場所も、まさにそこである。

三つの未来 — pidfd_config、io_uring、posix_spawn

Chen の spawn テンプレートが拒否された跡地に、三つの異なる道が提案された。それぞれが異なる抽象化層を狙う。

pidfd_config の道 — Christian Brauner の提案。 Brauner は既に pidfd インターフェース (pidfd_open()pidfd_send_signal()、その後追加された pidfd_getfd()) を通じて、プロセスを 「ファイルディスクリプタのようなオブジェクト」 として扱う抽象化を定着させてきた。彼が提案する流れはこうだ。まず pidfd_open() で 「空のプロセス」 を作る。その空のプロセスに pidfd_config() という新しいシステムコールを繰り返し呼んで、環境変数、ファイルディスクリプタ、資格情報を埋めていく。最後に exec() に類する呼び出しで実行ファイルをロードする。形は fsconfig() / fsopen() / fsmount() 系列とそっくりだ。Brauner の設計美学がそのまま顔を出す道である。強みは一貫性だ。pidfd という既に検証された抽象化の上に段階的に積み上げられ、ユーザー空間でステップごとにデバッグするのも容易だ。

io_uring の道 — Josh Poimboeuf の提案。 Josh は io_uring の ring それ自体を 「プロセス発射台」 として使おうと提案した。空のプロセスを作った上で、その中で ring を走らせ、ファイルディスクリプタを受け取ったり設置したりし、最後に一度ないし複数回の exec() を試みる、という構図である。このアプローチの強みは、io_uring が既に備える非同期・バッチ処理の能力をそのまま活用できる点にある。弱点も明確だ。あるコメントが指摘した通り、「io_uring には seccomp サポートがない。プロセスを立ち上げる道具にとって、それは深刻な欠陥である」。seccomp フィルタはコンテナランタイムやブラウザサンドボックスの第一防衛線だ。それが欠けた spawn 経路を導入することは、セキュリティ面では後退になる。Linux の spawn 抽象化が最終的に seccomp とどう出会うのかは、io_uring の道にとって最大の宿題である。

posix_spawn() の道 — 保守派の提案。 実は最も多くのコメントが同意した立場は、「新しいシステムコールはもう作らず、glibc の posix_spawn() を磨き込もう」 という側だった。「libc の大部分は質の高い posix_spawn() 実装でこの足元の火種を処理している」 というコメントが代表的だ。しかしこの道の限界は明確である。posix_spawn() が内部的に vfork() + exec() の上にある限り、マルチスレッドデッドロックの可能性は残り続ける。ユーザー空間でいくら巧妙に attr を設計しても、カーネルが本当に fork() の意味論から自由にならない限り、根本問題は解決しない。だから保守派の提案でさえ、最終的にはカーネル内の 「ネイティブな spawn 経路」 という同じ終着点に向かう。

三つの道のどれを進もうと、合意された出発点は一つだ。fork() + exec() は、もはやマルチスレッド時代の負荷を支えられない。ただ、Linux コミュニティがこの問題を解決する流儀は、新しい抽象化を一気に作るビッグバンよりも、既存の抽象化 (pidfd、io_uring、posix_spawn) を一歩ずつ拡張する漸進主義に近い。Chen のパッチが拒否された理由もそこにある。二つの新しいシステムコールで一度に答えを出そうとしたことが、小さな合意を積み上げていくコミュニティの呼吸と合わなかったのだ。次の LTS カーネルが出る頃には、pidfd_config の最初の試案がメーリングリストに上がってくる可能性が最も高い。それがマージされる時点が、Linux における fork() の 「公式引退」 の始まりとなるだろう。

結論 — POSIX の亡霊と決別できるか

fork() + exec() は酷いのか。1970 年代の単一スレッドプロセスモデルでは、優雅な発明だった。親と子を同じコードパスで扱い、exec() という一行で別のプログラムへ乗り換えるという発想は美しい。1990 年代のマルチスレッド時代には、その優雅さが落とし穴になった。2020 年代のコンテナ・io_uring・pidfd 時代には、もはや重荷ですらある。

しかし Mateusz Guzik の 「引退」 という言葉は、ただちに 「削除」 を意味するわけではない。fork() は Linux カーネルに永遠に残る。shell の子プロセスも、init も、コンテナランタイムのブートストラップ段階も、すべて fork() の上に立っている。引退という表現が指すのは 「新規コードではもう推奨しない」 という意味に近い。本当の問いはその次だ。我々は fork() が持っていた素朴さ — 二行のコードで新しいプロセスを立ち上げられるという素朴さ — を失わずに、マルチスレッド安全性とセキュリティ隔離と性能のすべてを手にできるのか。Chen の小さなパッチが拒否された場所から始まったこの問いは、次の LTS カーネルが出る頃には、何らかの形で答えを受け取らねばならない。その答えが pidfd_config の姿で来ようと、io_uring の姿で来ようと、我々がシステムコール本の最初の章で出会うあの絵は、50 年ぶりに初めて描き直されることになる。


出典: