コンピューティングにおいて、fork はプロセスが自身のコピーを作成する操作です。通常、カーネルのfork、clone、またはその他のシステムコールに対するC 標準ライブラリのラッパーとして実装されます。長年にわたり、fork は UnixおよびUnixライクなオペレーティングシステムにおけるプロセス作成の主要な方法であり、POSIXに準拠するための必須インターフェースとして残っています。しかしながら、パフォーマンスの低さ、スレッドセーフティの欠如、セキュリティ脆弱性の一般的な原因となっていることなどの欠陥により、近年では人気がなくなってきています。[ 1 ]
マルチタスクオペレーティングシステムでは、プロセス(実行中のプログラム)は、例えば他のプログラムを実行するために、新しいプロセスを作成する方法を必要とします。Unix系システムでは、通常、forkとその派生コマンドが唯一の方法です。プロセスが別のプログラムの実行を開始するには、まずforkを実行して自身のコピーを作成します。次に、「子プロセス」と呼ばれるコピーは、子プロセスに必要な環境変更を行い、execシステムコールを呼び出して自身を新しいプログラムで上書きします。つまり、以前のプログラムの実行を停止し、新しいプログラムを実行します。(まれなケースでは、子プロセスはexecを使わずに、別のプロセスとして元のプログラムの他の機能の実行を継続することもあります。)
fork 操作は、子プロセス用に別のアドレス空間を作成します。子プロセスは、親プロセスのすべてのメモリ セグメントの正確なコピーを持ちます。SunOS -4.0以降の仮想メモリモデルに従う最新の UNIX 系では、コピー オン ライトセマンティクスが実装されており、物理メモリを実際にコピーする必要はありません。代わりに、両方のプロセスの仮想メモリ ページは、どちらかのプロセスがページに書き込むまで、物理メモリの同じページを参照できます。書き込みが行われると、そのページがコピーされます。この最適化は、fork を exec と組み合わせて新しいプログラムを実行する一般的なケースで重要です。通常、子プロセスは、開始するプログラムを優先して自身のプログラムの実行を停止する前に、少数のアクションを実行するだけであり、親のデータ構造をほとんど、あるいは全く必要としません。
プロセスがforkを呼び出すと、そのプロセスが親プロセスとみなされ、新しく作成されたプロセスはその子プロセスとなります。forkの後、両方のプロセスは同じプログラムを実行するだけでなく、あたかも両方ともシステムコールを呼び出したかのように実行を再開します。そして、呼び出しの戻り値を調べて、子プロセスか親プロセスかを判断し、それに応じて処理を実行できます。
フォークの歴史は1960年代に遡り、フォークの概念に関する最も初期の言及の1つは、1962年に出版されたメルビン・コンウェイの「マルチプロセッサシステム設計」に登場しました。 [ 2 ]コンウェイの論文は、 L. ピーター・ドイチュによるGENIEタイムシェアリングシステムでのフォークの実装を促し、その概念はケン・トンプソンによってResearch Unixで最初に登場しました。[ 3 ] [ 4 ] [ 5 ]フォークは後にPOSIXの標準インターフェースになりました。[ 6 ]
以下の「Hello, World!」プログラムのバリエーションは、 C言語におけるforkシステムコールの仕組みを示しています。このプログラムは2つのプロセスに分岐し、それぞれのプロセスはforkシステムコールの戻り値に基づいて実行する機能を決定します。ヘッダーファイルのインクルードなどの定型コードは省略されています。
#include <stdio.h> #include <stdlib.h> #include <unistd.h>int main ( void ) { pid_t pid = fork ();if ( pid == -1 ) { perror ( "fork failed" ); return EXIT_FAILURE ; } else if ( pid == 0 ) { printf ( "子プロセスからのメッセージです!\n " ); return EXIT_SUCCESS ; } else { int status ; waitpid ( pid , & status , 0 ); } return EXIT_SUCCESS ; }以下では、このプログラムを詳細に分析する。
pid_t pid = fork ();main関数内の最初の文は、forkシステムコールを呼び出して実行を2つのプロセスに分割します。forkの戻り値は、プロセス識別子(PID)を表すPOSIX型であるpid_t型の変数に記録されます。
if ( pid == -1 ) { perror ( "fork failed" ); return EXIT_FAILURE ; }マイナス1はforkでエラーが発生したことを示します。新しいプロセスが作成されなかったため、エラーメッセージが表示されます。
forkが成功した場合、2つのプロセスが存在し、どちらもforkが戻った時点からメイン関数を実行します。プロセスに異なるタスクを実行させるには、プログラムはforkの戻り値に基づいて分岐し、子プロセスとして実行されるのか、親プロセスとして実行されるのかを判断する必要があります。
else if ( pid == 0 ) { printf ( "子プロセスからこんにちは! \n " ); return EXIT_SUCCESS ; }子プロセスでは、戻り値はゼロ(無効なプロセス識別子)になります。子プロセスは目的の挨拶メッセージを表示した後、終了します。(技術的な理由から、ここではC標準のexit関数ではなく、POSIXの_exit関数を使用する必要があります。)
それ以外の場合は、int status ; waitpid ( pid , & status , 0 );を実行します。もう一方のプロセスである親プロセスは、forkから子プロセスのプロセス識別子(常に正の数)を受け取ります。親プロセスはこの識別子をwaitpidシステムコールに渡し、子プロセスが終了するまで実行を一時停止します。子プロセスが終了すると、親プロセスは実行を再開し、 return文によって終了します。
子プロセスは、親プロセスのファイルディスクリプタのコピーから開始します。[ 6 ]親プロセスと子プロセス間のデータ交換と同期によるプロセス間通信には、パイプを使用できます。[ 7 ]親プロセスは、多くの場合、1つまたは複数のパイプを作成し、フォーク後に、不要になったパイプの端を閉じます。[ 7 ]親プロセスと子プロセス間で同じ変数を共有するには、メモリマッピングまたは共有メモリを使用する必要があります。[ 8 ]
vfork は、呼び出し規約と意味論がほぼ同じである fork の派生形ですが、限られた状況でのみ使用できます。これは、仮想メモリをサポートする最初の Unix である3BSDバージョン Unixに由来します[ 9 ] [ 10 ] [ 11 ]。vfork は POSIX によって標準化され、vfork が fork とまったく同じ動作をすることを許可しましたが、2004 年版[ 12 ]で廃止予定とされ、以降の版ではposix_spawn () (通常は vfork を介して実装されます)に置き換えられました。
vfork システムコールが発行されると、子プロセスが実行を完了するか、「exec」ファミリーのシステムコールのいずれかを介して新しい実行可能イメージに置き換えられるまで、親プロセスは中断されます。子プロセスは親プロセスからメモリ管理ユニットの設定を借用し、メモリ ページはコピーが行われず、特にコピー オン ライトセマンティクスなしで親プロセスと子プロセス間で共有されます。[ 12 ]したがって、子プロセスが共有ページのいずれかを変更しても、新しいページは作成されず、変更されたページは親プロセスからも見えます。ページ コピーがまったく関与しないため (追加のメモリを消費しない)、この手法は、完全コピー環境で exec と一緒に使用する場合、通常の fork よりも最適化されています。POSIX では、exec ファミリーの関数 (およびその他のいくつかの操作) への即時呼び出しの前段階として以外で vfork を使用すると、未定義の動作が発生します。[ 12 ] vfork と同様に、子プロセスはデータ構造をコピーするのではなく借用します。 vforkは、コピーオンライト方式を使用するforkよりも依然として高速です。
System Vは、System VR4が導入されるまでこの関数呼び出しをサポートしていませんでした。なぜなら、この関数呼び出しによって発生するメモリ共有はエラーが発生しやすいためです。
vfork はページ テーブルをコピーしないため、System V のfork実装よりも高速です。しかし、子プロセスは ( execまたはexitが呼び出されるまで) 親プロセスと同じ物理アドレス空間で実行されるため、親のデータとスタックを上書きする可能性があります。プログラマがvfork を誤って使用すると危険な状況が発生する可能性があるため、 vfork を呼び出す責任はプログラマにあります。System V のアプローチと BSD のアプローチの違いは哲学的なものです。カーネルは実装の特異性をユーザーから隠すべきでしょうか、それとも高度なユーザーが実装を利用して論理機能をより効率的に実行できるようにすべきでしょうか。
—モーリス・J・バッハ[ 13 ]
同様に、vfork の Linux man ページでは、その使用を強く推奨していません。[ 9 ]
Linuxが過去の亡霊を蘇らせたのは、実に残念なことだ。BSDのマニュアルページには、「適切なシステム共有メカニズムが実装されれば、このシステムコールは廃止される。vfork()のメモリ共有セマンティクスに依存してはならない。その場合、vfork()はfork(2)と同義となる」と記載されている。
vforkのその他の問題としては、動的リンクとの相互作用によりマルチスレッドプログラムでデッドロックが発生する可能性があることが挙げられます。[ 14 ] vforkインターフェースの代替として、POSIX はfork と exec の動作を組み合わせたposix_spawn関数ファミリーを導入しました。これらの関数は、Linux で行われているようにforkのライブラリ ルーチンとして実装することも、 [ 14 ] Solaris で行われているように、パフォーマンス向上のためにvforkのライブラリ ルーチンとして実装することもできます。 [ 14 ] [ 15 ]ただし、POSIX 仕様では、これらの関数は特に制約のあるハードウェアで動作するオペレーティングシステムやリアルタイム システム向けに「カーネル操作として設計された」とされています。[ 16 ]
4.4BSDの実装ではvforkの実装が削除され、vforkはforkと同じ動作をするようになったが、後にパフォーマンス上の理由からNetBSDオペレーティングシステムで復活した。[ 10 ]
uClinuxなどの一部の組み込みオペレーティングシステムは、メモリ管理ユニットがないためコピーオンライトを実装できないデバイス上で動作する必要があるため、forkを省略してvforkのみを実装しています。
Unix の設計者によって作成された Plan 9 オペレーティングシステムには、親プロセスと子プロセス間で、アドレス空間 (各プロセスに固有のスタック セグメントを除く)、環境変数、ファイルシステム 名前空間などのリソースをきめ細かく共有できる「rfork」と呼ばれる新しい関数のバリアントとして fork が含まれています。[ 17 ]これにより、プロセスと、その中のスレッドの両方を作成するための統一されたインターフェースになります。[ 18 ] FreeBSD [ 19 ]とIRIX はどちらもPlan 9 から rfork システム コールを採用し、後者はそれを「sproc」と改名しました。[ 20 ]
cloneは、 Linux カーネルのシステムコールで、親プロセスと実行コンテキストの一部を共有する子プロセスを作成します。FreeBSD の rfork や IRIX の sproc と同様に、Linux の clone は Plan 9 の rfork に触発されて開発され、スレッドの実装に使用できます (ただし、アプリケーション プログラマは通常、clone の上に実装されたpthreadsなどの高レベル インターフェースを使用します)。Plan 9 と IRIX の「分離スタック」機能は、( Linus Torvaldsによると) オーバーヘッドが大きすぎるため省略されています。 [ 20 ]
VMSオペレーティングシステムの初期設計(1977年)では、フォークのように新しいプロセス用に特定のアドレスの内容を変更するコピー操作は危険だと考えられていました。現在のプロセスの状態のエラーが子プロセスにコピーされる可能性があるためです。ここで、プロセス生成というメタファーが用いられています。新しいプロセスのメモリレイアウトの各コンポーネントは、ゼロから新たに構築されます。この生成メタファーは、後にMicrosoftオペレーティングシステム(1993年)で採用されました。
VM/CMSの POSIX 互換コンポーネント(OpenExtensions) は、子プロセスの実行中に親プロセスが中断され、子プロセスと親プロセスが同じアドレス空間を共有するという、非常に限定的な fork の実装を提供します。[ 21 ]これは基本的に、vforkをforkとラベル付けしたものです。(これは CMS ゲストオペレーティングシステムにのみ適用されます。Linux などの他の VM ゲストオペレーティングシステムは、標準的な fork 機能を提供します。)