私は現在fork()
およびexecv()
について学習しており、組み合わせの効率性について質問しました。
次の標準コードが表示されました。
_pid = fork();
if(pid < 0){
//handle fork error
}
else if (pid == 0){
execv("son_prog", argv_son);
//do father code
_
fork()
はプロセス全体(ヒープ全体のコピーなど)を複製し、execv()
は現在のアドレス空間を新しいプログラムのアドレス空間に置き換えます。これを念頭に置いて、この組み合わせを使用することは非常に非効率になりませんか?プロセスのアドレス空間全体をコピーして、すぐに上書きします。
だから私の質問:
(他のソリューションの代わりに)このコンボを使用することで得られる利点は何ですか?
他の解決策の代わりにこのコンボを使用することで得られる利点は何ですか?
どういうわけか、新しいプロセスを作成する必要があります。ユーザースペースプログラムがそれを達成する方法はほとんどありません。 POSIXではvfork()
をfork()
の横に配置していましたが、一部のシステムには独自のメカニズム(Linux固有のclone()
など)がある場合がありますが、2008年以降、POSIXではfork()
およびposix_spawn()
ファミリー。 fork
+ exec
ルートはより伝統的で、よく理解されており、欠点がほとんどありません(以下を参照)。 posix_spawn
ファミリは、fork()
に困難をもたらすコンテキストで使用するための特殊目的の代替として設計されています。詳細は その仕様 の「根拠」セクションを参照してください。
vfork()
のLinuxマニュアルページからのこの抜粋は、次のようにわかります。
Linuxでは、
fork
(2)はコピーオンライトページを使用して実装されるため、fork
(2)によって発生する唯一のペナルティは時間と親のページテーブルを複製し、子の一意のタスク構造を作成するために必要なメモリ。ただし、古き良き時代には、通常fork
(3)が直後に実行されるため、exec
(2)は呼び出し側のデータスペースの完全なコピーを作成する必要があります。したがって、効率を高めるために、BSDはvfork
()システムコールを導入しました。これは、親プロセスのアドレススペースを完全にはコピーしませんでしたが、execve
(2)または終了が発生しました。子がリソースを使用している間、親プロセスは一時停止されました。vfork
()の使用には注意が必要でした。たとえば、親プロセスでデータを変更しないことは、どの変数がレジスターに保持されているかを知ることに依存していました。
(エンファシスを追加)
したがって、廃棄物に関する懸念は最近のシステム(Linuxに限定されない)には根拠がありませんが、これは確かに歴史的に問題であり、実際にそれを回避するように設計されたメカニズムがありました。最近では、それらのメカニズムのほとんどは時代遅れです。
別の答えは述べています:
ただし、古き良き時代には、通常直後にexec(3)が実行されるため、fork(2)は呼び出し元のデータスペースの完全なコピーを作成する必要があります。
明らかに、ある人の悪い昔は他の人が覚えているよりもずっと若いです。
元のUNIXシステムには、複数のプロセスを実行するためのメモリがなく、MMUを備えていなかったため、物理メモリ内の複数のプロセスを同じ論理アドレススペースですぐに実行できるようにしていたため、スワップされました。現在実行されていなかったプロセスをディスクに出力します。
Forkシステムコールは、現在のプロセスをディスクにスワップアウトすることとほぼ同じですが、戻り値とnotを除いて、残りのメモリ内コピーを別のプロセスでスワップすることで置き換えます。子を実行するためにとにかく親プロセスを交換する必要があったため、fork + execはオーバーヘッドを引き起こしませんでした。
Fork + execが厄介な期間があったことは事実です:論理アドレス空間と物理アドレス空間の間のマッピングを提供するMMUがあったが、ページフォールトがコピーオンライトと他の仮想の数を提供する十分な情報を保持していなかったとき-メモリ/デマンドページングスキームが実現可能でした。
この状況は、UNIXだけでなく、非常に苦痛でした。ハードウェアのページフォールト処理は、かなり高速に「再生可能」になるように調整されました。
もうありません。 COW
(コピーオンライト)と呼ばれるものがあります。2つのプロセス(親/子)の1つが共有データに書き込もうとした場合のみ、コピーされます。
過去:fork()
システムコールは、呼び出しプロセス(親)のアドレススペースをコピーして、新しいプロセス(子)を作成しました。親のアドレス空間を子にコピーすることは、fork()
操作の中で最もコストのかかる部分でした。
現在:
頻繁にfork()
を呼び出すと、子プロセスのexec()
がすぐに呼び出され、子のメモリが新しいプログラムに置き換えられます。たとえば、これはシェルが通常行うことです。この場合、子プロセスはexec()
を呼び出す前にそのメモリをほとんど使用しないため、親のアドレス空間のコピーに費やされた時間は、ほとんど無駄になります。
このため、Unixの以降のバージョンでは、仮想メモリハードウェアを利用して、プロセスの1つが実際に変更するまで、親と子がそれぞれのアドレススペースにマップされたメモリを共有できるようにしました。この手法はcopy-on-writeとして知られています。これを行うために、カーネルはfork()
で、マップされたページのコンテンツの代わりに親から子にアドレススペースマッピングをコピーし、同時に現在共有されているページを読み取り専用にマークします。 2つのプロセスの1つがこれらの共有ページの1つに書き込もうとすると、プロセスはページ不在になります。この時点で、Unixカーネルは、ページが実際には「仮想」または「コピーオンライト」コピーであることを認識しているため、障害のあるプロセスに対して、ページの新しいプライベートな書き込み可能なコピーを作成します。このように、個々のページのコンテンツは、実際に書き込まれるまで実際にはコピーされません。この最適化により、子のfork()
に続いてexec()
がはるかに安価になります。子は、exec()
。
プロセスに数ギガバイトの書き込み可能なRAMがある場合、これらのCOWページフォールトはすべて安くはありません。子供が長い間exec()
を呼び出していたとしても、それらはすべて一度エラーになります。 fork()
の子は、シングルスレッドの場合でもメモリを割り当てることができなくなったため(Appleに感謝))、vfork()/exec()
代わりに、今はほとんど難しくありません。
vfork()/exec()
モデルの本当の利点は、任意の現在のディレクトリ、任意の環境変数、および任意のfsハンドル(_stdin/stdout/stderr
_だけではない)、任意のシグナルマスク、および数年ごとにさらにいくつかの引数を取得する20引数のCreateProcess()
APIを使用しない、任意の共有メモリー(共有メモリーsyscallを使用)。
スレッド化の初期の頃からの「私がリークしたハンドルが別のスレッドによって開かれている」エラーは、_/proc
_のおかげでプロセス全体のロックなしでユーザー空間で修正可能であることがわかりました。同じことは、新しいOSバージョンがなく、すべての人に新しいAPIを呼び出すように説得する巨大なCreateProcess()
モデルにはありません。
だからあなたはそれを持っています。設計の事故は、直接設計された解決策よりもはるかに良くなった。
特にLinuxで見られるようなcopy-on-write fork
sを使用すると、(プロセスを直接生成するのに比べて)それほど高価ではありません。
POSIXにposix_spawn
これにより、fork/and-execを効果的に組み合わせることができます(おそらくfork
+ exec
よりも効率的です。より効率的である場合、通常は安価でありながら堅牢性の低い方法で実装されます。 fork(clone
/vfork
)に続いてexec)、しかしそれが#2を達成する方法は、比較的厄介なオプションのトンによるものです。新しいプロセスイメージが読み込まれる直前に任意のコードを実行します。
Exec()などによって作成されたプロセスは、親プロセス(stdin、stdout、stderrを含む)からファイルハンドルを継承します。親がfork()を呼び出した後、exec()を呼び出す前にこれらを変更すると、子の標準ストリームを制御できます。