Forkfork(フォーク)とは、プロセスのコピーを生成するものである。UNIXおよびUnix系OSではシステムコールのひとつで、新たに作り出されたプロセスを子プロセス、 forkが呼び出されると、子プロセスのためのアドレス空間が新たに作成される。子プロセスのアドレス空間には親プロセスが持っていた全セグメントのコピーがあるが、コピーオンライト機能によって実際の物理メモリの確保は遅延される(すなわち、一時的に同じ物理メモリセグメント群を親子で共有する)。親プロセスと子プロセスは同じコードセグメントを持つが、独立して実行される。 Unixにおけるforkの重要性Unixにとってforkは重要な機構概念であり、フィルタの開発を奨励している設計哲学の重要な部分を担っている。
Unixでのフィルタは標準入力を入力とし標準出力を出力とする(通常小さな)プログラムである。シェルがフィルタをパイプで連結することで、複雑な処理を実現できる。例えば次のように $ find . -name "*.cpp" -print | wc -l
このコマンド行を入力すると、シェルは自分自身をforkし、プロセス間通信の1つであるパイプを使って より一般的に、シェルはユーザーがコマンド行を入力するたびにforkを行っている。子プロセスはシェルがforkを行うことで生成され、子プロセスが プロセスのアドレス空間実行ファイルを実行しようとすると、プロセスが生成される。実行ファイルはセグメントと呼ばれるブロックにグループ化されたバイナリコードを含んでいる。各セグメントは特定の種類のデータを格納するのに使われる。典型的なELF形式の実行ファイルには、以下のようなセグメントが存在する。
forkとページ共有
そのため、コピーオンライト (COW) という技法が使われる。COWでは、fork時に親プロセスのページ群を子プロセスにコピーしない。その代わりページ群は親プロセスと子プロセスの間で共有される。親子いずれかのプロセスがページの内容を更新しようとしたとき、そのページだけコピーを作成し、書き込もうとしたプロセスの当該ページだけが更新される。書き込んだプロセスはその後その新たにコピーしたページを使用する。もう一方のプロセス(共有ページに書き込まなかったプロセス)は、コピー元のページを使用し続ける(共有状態は解消される)。何らかのプロセスが書き込もうとしたときにページがコピーされるので、この技法をコピーオンライトと呼ぶ。 vforkとページ共有
実装によっては
直後に
MMUのないシステム組み込みシステムではメモリ管理ユニット (MMU) が存在しない場合があり、 全プロセスが単一のアドレス空間を共有している場合、例えば全メモリページをスワップしてコンテキストスイッチするという Unix以外でのフォークUnix系やLinuxでのfork機構は、基盤となっているハードウェアにある種の前提を課している。リニアなメモリ空間とページング機構を持ち、連続なアドレス範囲のメモリコピーを効率的に行えるという前提である。VMS(現在のOpenVMS)の当初の設計では、コピー操作後に少数の具体的なアドレスの内容書き換えを行うフォークは危険だとみなされていた。現在のプロセス状態におけるエラーが子プロセスにコピーされるかもしれない。そこでプロセスのスポーン(産卵)というメタファーが使われた。すなわち、新しいプロセスのメモリレイアウトの各コンポーネントを一から新たに構築する。ソフトウェア工学的観点からすれば後者(スポーン)の手法の方がきれいで安全だが、フォークの方が効率的なのでよく使われている。スポーン方式は後にマイクロソフトのOSで採用された。 fork を使ったコード例C の例#include <stdio.h> /* printf, stderr, fprintf */
#include <sys/types.h> /* pid_t */
#include <unistd.h> /* _exit, fork */
#include <stdlib.h> /* exit */
#include <errno.h> /* errno */
int main(void)
{
pid_t pid;
/* 子プロセスと親プロセスの両方の出力が
* 標準出力に書かれる。
* 両者は同時に動作する。
*/
pid = fork();
if (pid == -1)
{
/* エラー:
* fork()が-1を返す場合、エラーが起きたことを示す。
* 例えばプロセス数が制限に達した場合など。
*/
fprintf(stderr, "can't fork, error %d\n", errno);
exit(EXIT_FAILURE);
}
if (pid == 0)
{
/* 子プロセス:
* fork()が0を返す場合、子プロセスである。
* 1秒に1ずつ、10まで数える。
*/
int j;
for (j = 0; j < 10; j++)
{
printf("child: %d\n", j);
sleep(1);
}
_exit(0); /* exit() を使わない点に注意 */
}
else
{
/* fork() が正の数を返す場合、親プロセスである。
* その値は生成した子プロセスのPIDである。
* ここでも10まで数える。
*/
int i;
for (i = 0; i < 10; i++)
{
printf("parent: %d\n", i);
sleep(1);
}
exit(0);
}
return 0;
}
Perl の例#!/usr/bin/env perl -w
use strict;
if (fork) { # 親プロセス
foreach my $i (0 .. 9) {
print "Parent: $i\n";
sleep 1;
}
}
else { # 子プロセス
foreach my $i (0 .. 9) {
print "Child: $i\n";
sleep 1;
}
exit(0); # forkした子プロセスの終了
}
exit(0); # 親プロセスの終了
Python の例#!/usr/bin/env python3
import os
import sys
import time
def doTask():
"This function create a task that will be a daemon"
with open("/tmp/tarefa.log", "w") as log_file:
while True:
log_file.write("{}\n".format(time.ctime()))
log_file.flush()
time.sleep(2)
def createDaemon():
"This function create a service/Daemon that will execute a det. task"
try:
pid = os.fork()
if pid > 0:
print("PID: {}".format(pid))
sys.exit()
except OSError as error:
print("Unable to fork. Error: {} ({})".format(error.errno, error.strerror))
sys.exit(1)
else:
doTask()
if __name__ == "__main__":
createDaemon()
Fork-ExecFork–Execは、UNIXで一般的に使われる手法であり、新たなプログラムをプロセスとして実行する。fork()は親プロセスを2つの同一内容のプロセスに(フォークの先のように)分岐させるシステムコールである。fork()によって子プロセスが親プロセスのコピーとして生成され、子プロセスがexec()システムコールを呼び出すことで(子プロセス)自身の内容を置き換える。 親プロセスが子プロセスの終了を待ち合わせる場合、子プロセスの終了コード (exit code) を受け取ることができる。子プロセスがゾンビプロセスとなるのを防ぐには、親プロセスがwaitシステムコールを使用する必要があり、そのタイミングは定期的でもよいし、SIGCHLDシグナルを受け取った際でもよい。 子プロセスがexec()を呼び出すと、そのアドレス空間の内容は全て失われ、指定されたプログラムを実行するためのアドレス空間のマッピングが新しく設定される。これをオーバーレイと呼ぶ。アドレス空間は全て置き換えられるが、オープン済みファイルのファイル記述子群は close-on-exec が指定されたときだけ exec()時に自動的にクローズされる。この特徴を利用して、fork()を呼び出す前にパイプを作成しておき、exec()で指定された新しいプログラムとの通信を行うというUNIX特有の手法が実現されている。 なお、Microsoft Windows では fork() 単独に相当するシステムコールがなく、fork-exec モデルを採用していない。代わりにspawn()ファミリ関数が process.h で定義されており、fork-exec に相当する働きをする。 脚注
参考文献
関連項目外部リンク
|