fork(フォーク)とは
forkとは、
プロセスを複製する操作のことです。特に、
UNIXおよび
Unix系の
オペレーティングシステム(OS)において、
システムコールの一つとして重要な役割を果たします。fork()
システムコールは、呼び出した
プロセス(親
プロセス)のコピーである新しい
プロセス(子
プロセス)を生成します。
fork()の動作
fork()
システムコールが呼び出されると、以下の処理が行われます。
1.
子プロセスの生成: 親
プロセスの実行コード、データ、スタックなどの状態をコピーした子
プロセスが生成されます。
2.
戻り値: fork()
システムコールは、親
プロセスには子
プロセスの
プロセスIDを返し、子
プロセスには0を返します。エラーが発生した場合は、-1を返します。この戻り値によって、親
プロセスと子
プロセスは異なる処理を実行できます。
3.
アドレス空間: 子
プロセスは、親
プロセスの
アドレス空間のコピーを持ちますが、実際には物理メモリのコピーは遅延され、
コピーオンライト(COW)という仕組みによって、共有されたメモリ領域を一時的に使用します。これにより、fork()の実行効率が向上します。
Unixにおけるforkの重要性
Unix系OSでは、fork()は重要な概念であり、シェルによるパイプ処理など、多くの機能の基盤となっています。以下は、fork()がどのように利用されているかの例です。
パイプ処理
シェルでパイプを使用する場合、シェルはfork()を呼び出して複数の子
プロセスを生成します。これらの子
プロセスは、パイプを通じてデータのやり取りを行い、複雑な処理を連携して実行します。例えば、`find . -name ".cpp" | wc -l`というコマンドでは、findコマンドの出力がwcコマンドの入力にパイプで接続され、cppファイルの数が表示されます。
コマンド実行
シェルは、ユーザーがコマンドを入力すると、自分自身をfork()して子
プロセスを生成します。子
プロセスは、`exec()`ファミリの
システムコールを使って、実行したいプログラムで自身を上書きします。これにより、新たなプログラムを実行する
プロセスが生成されます。
プロセスの
アドレス空間は、実行ファイルに含まれるセグメントと呼ばれる領域に分割されています。各セグメントは異なる種類のデータを格納します。
`.text`: 実行可能なコード
`.bss`: 初期値がゼロのデータ
`.data`: 初期値のあるデータ
`.symtab`: シンボルテーブル(関数名、変数名など)
`.interp`: インタプリタの名前
これらのセグメントは、実行時にメモリにロードされ、
プロセスの
アドレス空間を構成します。
メモリのページング
メモリはページと呼ばれる固定サイズのブロックに分割されます。実行ファイルがメモリにロードされる際、実行ファイル内のデータは複数のページに分散して格納されます。これにより、メモリの効率的な利用が可能になります。
forkとページ共有
fork()
システムコールでは、子
プロセスは親
プロセスの全ページをコピーする必要があるように思えます。しかし、多くの場合、子
プロセスはすぐに`exec()`を呼び出して自身を上書きするか、終了します。このため、メモリのコピーは無駄になることがあります。
[コピーオンライト]技術では、fork()時に物理メモリのコピーは行わず、親
プロセスと子
プロセスでメモリページを共有します。どちらかの
プロセスが共有ページを書き込もうとした場合にのみ、そのページのコピーを作成します。これにより、fork()のコストを大幅に削減できます。
vforkとページ共有
vfork()はfork()に似た
システムコールですが、動作が異なります。
vfork()の動作
vfork()では、子
プロセスが終了するか、`exec()`ファミリの
システムコールで別のプログラムに切り替わるまで、親
プロセスが一時停止します。また、vfork()ではメモリの
コピーオンライトは行わず、親子間でメモリが共有されます。そのため、子
プロセスがメモリを更新すると、親
プロセスからもその変更が見えます。
vfork()の危険性
vfork()は高速ですが、誤った使い方をすると親
プロセスを破壊する危険性があります。例えば、子
プロセスでスタックを操作したり、共有メモリを不用意に書き換えると、親
プロセスが予期しない動作をすることがあります。そのため、vfork()の利用は推奨されず、特に
Linuxではvfork()の使用自体が推奨されていません。
MMUのないシステム
組み込みシステムなど、
メモリ管理ユニット(MMU)のないシステムでは、
コピーオンライトは実装できない場合があります。この場合、fork()の代わりにvfork()のみが実装されることがあります。MMUがない場合、
プロセスは単一の
アドレス空間を共有することがあり、
コンテキストスイッチにはメモリ全体のコピーが必要になることがあります。
Unix以外でのフォーク
UNIX系OSのfork()は、リニアなメモリ空間やページング機構など、特定のハードウェアの前提に依存しています。VMSなどのOSでは、
プロセスの状態がコピーされることでエラーが発生する可能性があるため、fork()の代わりに
プロセスのスポーンという概念が使われます。スポーンは、新しい
プロセスの
アドレス空間をゼロから構築するもので、より安全ですが、フォークよりも効率が劣ります。
fork を使ったコード例
以下に、C、Perl、Pythonでのfork()の使用例を示します。
Cの例
c
include
include
include
int main() {
pid_t pid = fork();
if (pid == 0) {
printf("子
プロセス: PID = %d
", getpid());
} else if (pid > 0) {
printf("親
プロセス: PID = %d, 子
プロセスのPID = %d
", getpid(), pid);
} else {
perror("fork failed");
return 1;
}
return 0;
}
Perlの例
perl
my $pid = fork();
if ($pid == 0) {
print "子
プロセス: PID = $$
";
} elsif ($pid > 0) {
print "親
プロセス: PID = $$", 子
プロセスのPID = $pid
";
} else {
die "fork failed: $!";
}
Pythonの例
python
import os
pid = os.fork()
if pid == 0:
print(f"子
プロセス: PID = {os.getpid()}")
elif pid > 0:
print(f"親
プロセス: PID = {os.getpid()}, 子
プロセスのPID = {pid}")
else:
print("fork failed")
Fork-
Execは、
UNIXで新たなプログラムを実行する一般的な方法です。`fork()`で子
プロセスを生成した後、`exec()`
システムコールを呼び出して子
プロセス自身を新たなプログラムで上書きします。これにより、独立した
プロセスでプログラムを実行できます。
親
プロセスは、子
プロセスの終了を`wait()`
システムコールで待ち合わせることができます。これにより、子
プロセスの終了コードを受け取り、ゾンビ
プロセス化を防ぎます。子
プロセスが`exec()`を呼び出すと、
アドレス空間は完全に新しいプログラムで上書きされますが、
ファイル記述子などは`close-on-exec`フラグが設定されていない限り、親
プロセスから継承されます。
Microsoft Windowsでは、
UNIXのような`fork()`
システムコールは提供されていません。代わりに、`spawn()`ファミリの関数が使用され、同様の
プロセス生成とプログラム実行を実現します。
まとめ
fork()は、
UNIX系OSにおける
プロセスの複製と並列処理の基礎となる重要なメカニズムです。
コピーオンライトやvfork()などの技術により、効率的な
プロセス生成を実現しています。fork-execモデルは、
UNIXシステムの柔軟性と強力さを支える重要な要素であり、様々なアプリケーションの開発に不可欠です。