Fork

Unixにおけるforkの重要性

Unixにとってforkは重要であり、フィルタの開発を奨励している設計哲学の重要な部分を担っている。Unixでのフィルタは標準入力を入力とし標準出力を出力とする（通常小さな）プログラムである。シェルがフィルタをパイプで連結することで、複雑な処理を実現できる。例えば次のようにfind(1)コマンドの出力をwc(1)コマンドの入力に連結すると、拡張子が ".cpp" のファイルをカレントディレクトリ配下で探し、見つかったファイル数を表示できる。
$ find . -name "*.cpp" -print | wc -l
このコマンド行を入力すると、シェルは自分自身をforkし、プロセス間通信の1つであるパイプを使って find コマンドの出力を wc コマンドの入力に結びつける。パイプは2つの新たなファイル識別子を生成し、2つの子プロセスを生成する（それぞれ find と wc に対応）。2つの子プロセスはまず dup2(2) で対応するパイプのファイル識別子を複製して標準入力と標準出力に置き換える。そしてそれぞれの子プロセスがexec(3)ファミリのシステムコールを使って、実行すべきコマンドのプログラムで自身をオーバーレイする。
より一般的に、シェルはユーザーがコマンド行を入力するたびにforkを行っている。子プロセスはシェルがforkを行うことで生成され、子プロセスがexecでオーバーレイを行い、実行すべきプログラムのコードをマッピングする。

forkとページ共有

fork()システムコールを実行すると、本来ならばOSが子プロセスのために親プロセスの持つ全ページを物理メモリ上の別の位置にコピーすることになる。しかし、場合によってはその必要はない。子プロセスがfork()直後に "exec" システムコール（実行ファイルを実行するときに使用する）を実行する場合や終了する場合である。親プロセスが何らかのコマンドを実行するためだけに子プロセスを生成した場合、子プロセスのアドレス空間は実行すべきコマンドですぐに置換されるので、親プロセスのページ群をコピーする必要はない。
そのため、コピーオンライト (COW) という技法が使われる。COWでは、fork時に親プロセスのページ群を子プロセスにコピーしない。その代わりページ群は親プロセスと子プロセスの間で共有される。親子いずれかのプロセスがページの内容を更新しようとしたとき、そのページだけコピーを作成し、書き込もうとしたプロセスの当該ページだけが更新される。書き込んだプロセスはその後その新たにコピーしたページを使用する。もう一方のプロセス（共有ページに書き込まなかったプロセス）は、コピー元のページを使用し続ける（共有状態は解消される）。何らかのプロセスが書き込もうとしたときにページがコピーされるので、この技法をコピーオンライトと呼ぶ。

vforkとページ共有

vfork はもう1つのプロセス生成用UNIXシステムコールである。vfork()システムコールで子プロセスを生成すると、子プロセスが終了するかexecve()ファミリのシステムコールで新たな実行イメージに切り換えるまでそのシステムコールの延長上で親プロセスが待ち合わせる。vforkでも親プロセスと子プロセスでページ群を共有するが、コピーオンライトは必要としない。子プロセスが共有ページに更新を加えてもコピーは作成せず、親プロセスからもその更新が見える。ページを全くコピーしないので、子プロセスでコマンドを実行する場合には非常に効率的である。
実装によっては vfork() は fork() と同じである。
vfork()とfork()の唯一の違いは、親プロセスと子プロセスがコードとデータを共有できる点である。これによって複製は劇的に高速化されるが、使い方を誤ると親プロセスの一貫性が壊れる危険性がある。
直後に exec または _exit() を呼び出す以外のvfork()の使用は推奨されない。特にLinuxのmanページではvforkそのものの使用が推奨されていない。
Linuxが過去からこの幽霊を蘇らせたことはやや不幸である。BSDのmanページには「このシステムコールは妥当なシステム共有機構が実装された場合には削除される。ユーザは vfork() のメモリ共有機能に依存するべきではない。何故ならば、このシステムコールが削除された場合には、それは fork(2) の同義語とされるからである」と記されている。
vfork()で生成した子プロセスがvfork()を呼び出したルーチンからさらに外側に復帰した場合、親プロセスのコールスタックを書き変えてしまうので、vfork()から親プロセスが戻ったときの動作を保証できない。また、execせずに子プロセスを終了する場合、_exit()ではなくexit()を使用すると、標準I/Oチャネルをフラッシュしてクローズするため親プロセスの標準I/O構造にダメージを与える危険性がある。なお、fork()の場合でも子プロセスがexit()を呼び出すのは危険である。
vfork()後に子プロセスでシグナルハンドラが呼び出された場合、子プロセスの他のコードと同じ規則に従う必要がある。

MMUのないシステム

組み込みシステムではメモリ管理ユニット (MMU) が存在しない場合があり、fork() でのコピーオンライトの実装ができないことがある。システムがプロセス毎のアドレス空間を他の何らかの機構で（例えばセグメント方式で）サポートする場合、プロセスの使用する全メモリをコピーすれば実質的には同じである。しかしそれは非常に時間がかかるし、多くの場合すぐに別の実行ファイルのイメージで書き換えられるので無駄である。
全プロセスが単一のアドレス空間を共有している場合、例えば全メモリページをスワップしてコンテキストスイッチするというfork()の実装も考えられる。μClinuxベースのOSなどの組み込みOSが採用しているのは、fork() を廃して vfork() だけを実装するという方法である。そのため、forkを使っているところを全てvforkで動作できるように書き換えている。

Unix以外でのフォーク

Unix系やLinuxでのfork機構は、基盤となっているハードウェアにある種の前提を課している。リニアなメモリ空間とページング機構を持ち、連続なアドレス範囲のメモリコピーを効率的に行えるという前提である。VMS（現在のOpenVMS）の当初の設計では、コピー操作後に少数の具体的なアドレスの内容書き換えを行うフォークは危険だとみなされていた。現在のプロセス状態におけるエラーが子プロセスにコピーされるかもしれない。そこでプロセスのスポーン（産卵）というメタファーが使われた。すなわち、新しいプロセスのメモリレイアウトの各コンポーネントを一から新たに構築する。ソフトウェア工学的観点からすれば後者（スポーン）の手法の方がきれいで安全だが、フォークの方が効率的なのでよく使われている。スポーン方式は後にマイクロソフトのOSで採用された。

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 -wuse 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 pythonimport os, time, sysdef createDaemon():
  "This function create a service/Daemon that will execute a det. task"
   
  try:
    # forkしたPIDを格納
    pid = os.fork()

    if pid > 0:
      print 'PID: %d' % pid 
      sys.exit()

  except OSError as error:
    print 'Unable to fork. Error: %d (%s)' % (error.errno, error.strerror)
    sys.exit(1)

  doTask()def doTask():
  "This function create a task that will be a daemon"

  # ライトモードでファイルをオープン
  log_file = open('/tmp/tarefa.log', 'w')

 
  while True:
    print >> log_file, time.ctime()
    log_file.flush()
    time.sleep(2)

  # ファイルをクローズ
  log_file.close()
  
if __name__ == '__main__':

  # デーモンを生成
  createDaemon()

Fork-Exec

Fork-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 に相当する働きをする。