Unixでは、新しいプロセスを作成する場合は常に、現在のプロセスをフォークして、親プロセスとまったく同じ新しい子プロセスを作成します。次に、execシステムコールを実行して、親プロセスのすべてのデータを新しいプロセスのデータで置き換えます。
なぜ最初に親プロセスのコピーを作成し、新しいプロセスを直接作成しないのですか?
簡単に言えば、fork
はUnixにあります。これは、当時の既存のシステムに簡単に合わせることができ、また Berkeleyの先行システム がフォークの概念を使用していたためです。
From The Unix Evolution of the Unix Time-sharing System (関連テキストはhighlighted):
最新の形式のプロセス制御は、数日で設計および実装されました。それが既存のシステムにいかに簡単に適合するかは驚くべきことです。同時に、デザインのわずかに変わった機能のいくつかが、存在するものへの小さく簡単にコード化された変更を表すため、正確に存在する方法を簡単に確認できます。良い例は、fork関数とexec関数の分離です。新しいプロセスを作成するための最も一般的なモデルには、プロセスが実行するプログラムの指定が含まれます。 Unixでは、フォークされたプロセスは、明示的なexecを実行するまで、親と同じプログラムを実行し続けます。関数の分離は確かにUnixに固有のものではなく、実際には、トムソンによく知られているバークレーのタイムシェアリングシステムに存在していました。それでも、それがUnixに存在することを想定するのは理にかなっているようです。主に、elseをほとんど変更せずにフォークを簡単に実装できるためです。システムはすでに複数の(つまり2つの)プロセスを処理しました。プロセステーブルがあり、プロセスはメインメモリとディスクの間でスワップされました。フォークの最初の実装は必要なだけ
1)プロセステーブルの拡張
2)既存のスワップIOプリミティブを使用して現在のプロセスをディスクスワップ領域にコピーし、プロセステーブルにいくつかの調整を加えたfork呼び出しの追加。
実際、PDP-7のfork呼び出しには、正確に27行のアセンブリコードが必要でした。もちろん、オペレーティングシステムとユーザープログラムの他の変更が必要でしたが、それらのいくつかはかなり興味深く予期しないものでした。しかし、そのようなexecが存在しなかったという理由だけで、fork-execを組み合わせると、かなり複雑になります。その機能は、明示的なIOを使用して、シェルによって既に実行されています。
その論文以来、Unixは進化してきました。 fork
の後にexec
が続くことは、プログラムを実行する唯一の方法ではなくなりました。
vfork は、新しいプロセスがフォークの直後にexecを実行する予定の場合に、より効率的なフォークになるように作成されました。 vforkを実行した後、親プロセスと子プロセスは同じデータスペースを共有し、親プロセスは子プロセスがプログラムを実行するか終了するまで中断されます。
posix_spawn は、新しいプロセスを作成し、単一のシステムコールでファイルを実行します。呼び出し元の開いているファイルを選択的に共有し、その信号処理とその他の属性を新しいプロセスにコピーできるようにする一連のパラメーターを受け取ります。
[ ここ から私の回答の一部を繰り返します。]
新しいプロセスを最初から作成するコマンドだけではないのですか?すぐに置き換えられるだけのものをコピーするのはばかげて非効率的ではないですか?
実際、それはおそらくいくつかの理由でそれほど効率的ではありません:
カーネルはcopy-on-write system;を使用するため、fork()
によって生成される「コピー」は少し抽象化されています。実際に作成する必要があるのは、仮想メモリマップだけです。コピーがすぐにexec()
を呼び出す場合、プロセスが必要とする処理を何も行わないため、プロセスのアクティビティによって変更された場合にコピーされたであろうほとんどのデータを実際にコピー/作成する必要はありません。使用する。
子プロセスのさまざまな重要な側面(たとえば、その環境)は、コンテキストなどの複雑な分析に基づいて個別に複製したり設定したりする必要はありません。これらは、呼び出しプロセスのそれと同じであると想定されているだけです。これは、私たちがよく知っているかなり直感的なシステムです。
#1をもう少し詳しく説明すると、少なくともほとんどの場合、「コピー」されてもその後アクセスされないメモリは実際にはコピーされません。このコンテキストでの例外は、 might で、プロセスをフォークした場合、子がexec()
に置き換えられる前に親プロセスを終了させます。 might と言います。これは、十分な空きメモリがある場合、親の多くがキャッシュされる可能性があり、これがどの程度悪用されるかはわかりません(OSの実装によって異なります)。 。
もちろん、それは表面的には、空白のスレートを使用するよりも more コピーを使用する方が効率的ではありません。システムは、同じ方法でコピーする汎用の空白/新しいプロセステンプレートを持つことができます。1 ただし、コピーオンライトフォークと比較して、実際には何も保存されません。したがって、#1は、「新しい」空のプロセスを使用する方が効率的ではないことを示しています。
ポイント#2は、フォークを使用する方が効率的である理由を説明しています。完全に異なる実行可能ファイルであっても、子の環境は親から継承されます。たとえば、親プロセスがシェルで、子プロセスがWebブラウザーの場合、_$HOME
_はどちらの場合も同じですが、どちらかが後で変更する可能性があるため、これらは2つの別々のコピーである必要があります。子の1つは、元のfork()
によって生成されます。
1.文字通り意味をなさないかもしれない戦略ですが、プロセスの作成には、そのイメージをディスクからメモリにコピーする以上のことが含まれるということが私の要点です。
Unixが新しいプロセスを作成するためのfork
関数しかなかった理由は、 nixの哲学 の結果だと思います。
彼らは1つのことをうまく行う1つの機能を構築します。子プロセスを作成します。
新しいプロセスで何をするかはプログラマ次第です。彼はexec*
関数の1つを使用して別のプログラムを開始できます。または、execを使用して同じプログラムの2つのインスタンスを使用できなかったため、便利な場合があります。
使用できるため、自由度が大きくなります
さらに、fork
およびexec*
関数呼び出しを覚えるだけでよく、1970年代にはそれを行う必要がありました。
プロセス作成には2つの哲学があります。継承を伴うフォークと、引数を伴う作成です。 Unixは明らかにフォークを使用します。 (たとえば、OSE、およびVMSはcreateメソッドを使用します。)Unixには継承可能な多くの特性があり、定期的に追加されます。継承により、これらの新しい特性を既存のプログラムを変更せずに追加できます。引数付きの作成モデルを使用して、新しい特性を追加することは、create呼び出しに新しい引数を追加することを意味します。 Unixモデルはより単純です。
また、非常に便利なfork-without-execモデルが提供され、プロセスはそれ自体を複数の部分に分割できます。これは、非同期I/Oの形式が存在しない場合に重要であり、システムで複数のCPUを利用する場合に役立ちます。 (プレスレッド。)私はこれを何年にもわたって、最近でも何度も行ってきました。本質的には、複数の「プログラム」を単一のプログラムにコンテナ化できるため、破損やバージョンの不一致などの余地はまったくありません。
Fork/execモデルは、特定の子がforkとexecの間に設定された根本的に奇妙な環境を継承する機能も提供します。特に、継承されたファイル記述子のようなもの。 (stdio fdの拡張です。)createモデルには、create呼び出しの作成者が想定していなかったものを継承する機能はありません。
一部のシステムでは、ネイティブコードの動的コンパイルもサポートできます。この場合、プロセスは実際に独自のネイティブコードプログラムを作成します。言い換えると、ソースコード/コンパイラ/リンカーのサイクルを経ずにディスクスペースを占有することなく、オンザフライで自身を書き込む新しいプログラムが必要です。 (これを行うVerilog言語システムがあると思います。)フォークモデルはこれをサポートしていますが、通常、作成モデルはサポートしていません。
Fork()関数は親プロセスをコピーするだけでなく、プロセスが親または息子プロセスであることを示す値を返します。以下の画像は、fork()を親およびaとして使用する方法を説明しています息子:
プロセスが親である場合に示されているように、fork()は息子のプロセスID PID
を返します。それ以外の場合は0
を返します
たとえば、リクエストを受信するプロセス(Webサーバー)があり、各リクエストでson process
を作成してこのリクエストを処理する場合に、これを利用できます。ここでは、父親とその息子が異なるジョブを持っています。
つまり、プロセスのコピーを実行することは、fork()とまったく同じではありません。
I/Oリダイレクションは、forkの後、execの前に最も簡単に実装されます。子は、自分が子であることを認識し、ファイル記述子を閉じたり、新しい記述子を開いたり、dup()やdup2()でそれらを正しいfd番号に変更したりすることができます。それを実行した後、おそらく必要な環境変数を変更すると(親にも影響しません)、調整された環境で新しいプログラムを実行できます。