fork関数がどうやってプロセスを分割しているか

はじめてのOSコードリーディング ~UNIX V6で学ぶカーネルのしくみ
という本を読んでいます。

この中で、fork関数がどうやって子プロセスを作り、
親子かを識別して別の値を返しているのかが解説されており、
とても興味深かったです。

以下にその概要をまとめました。

fork関数

Cではfork関数を利用することで、子プロセスを作成することが出来ます。
コードとしてはこんな感じですね。

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
  pid_t pid = fork();
  if (pid == 0) {
    sleep(1);
    printf("child!\n");
    return 0;
  }

  printf("parent!\n");

  int status;
  waitpid(pid, &status, 0);

  printf("parent end\n");
  return 0;
}

子プロセスは親プロセスのデータをそのままコピーするため、変数などは全て同じ状態になります。
ですが、fork関数は親プロセスの場合は子プロセスのIDを、子プロセスでは0を返すため、
ユーザはfork関数の戻り値を見て、自身が親なのか子なのかを区別できるようになっています。

では、fork関数の中ではどのようにして、親プロセスか子プロセスかを判断し、
別の値を返しているのでしょうか。

これは(UNIX V6では)switch関数の仕様を上手く使った実装により実現されていました。

fork関数がプロセスの親子を区別する仕組み

親による子プロセスの作成

ライブラリのfork関数(source/s4/fork.s)を実行すると、
システムコールによってカーネルのfork関数(sys/ken/sys1.c)が実行されます。

fork()
{
  register struct proc *p1, *p2;

  p1 = u.u_procp;
  for(p2 = &proc[0]; p2 < &proc[NPROC]; p2++)
  if(p2->p_stat == NULL)
    goto found;
  u.u_error = EAGAIN;
  goto out;

found:
  if(newproc()) {
    u.u_ar0[R0] = p1->p_pid;
    u.u_cstime[0] = 0;
    u.u_cstime[1] = 0;
    u.u_stime = 0;
    u.u_cutime[0] = 0;
    u.u_cutime[1] = 0;
    u.u_utime = 0;
    return;
  }
  u.u_ar0[R0] = p2->p_pid;

out:
  u.u_ar0[R7] =+ 2;
}

このカーネルのfork関数内でnewproc関数(sys/ken/slp.c)を呼び出し、子プロセスを作成しています。
その後、newproc関数は0を返すため、カーネルのfork関数で0で帰ってきた場合に、
作成した子プロセスのIDをレジスタに乗せ、ライブラリのfork関数で返すようにしています。

作成された子プロセス側の処理

子プロセスは作成された後、実行順番が回ってきたタイミングでswitch関数(sys/ken/slp.c)により再開します。
この関数内では保存されたデータを復元し、最後にsavu関数を実行した関数の呼び出し元に、return 1で戻ります。

カーネルのfork関数で呼び出しているnewproc関数(sys/ken/slp.c)内では、
savu関数が実行されてから子プロセスがコピーされるため、
switch関数はnewprocの呼び出し元であるfork関数に1で戻ります。

これにより、カーネルのfork関数内で呼び出しているnewproc関数は、
親プロセスの場合は0が、子プロセスの場合は1が返るようになり、
その値を見て自身が親なのか子なのかを判断でき、別々の戻り値を返せるようになっています。

まとめ

  • 親プロセスはforkで子プロセスを作成してそのまま処理を継続
  • 子プロセスは実行順番が回ってきたタイミングで処理を開始
    • switch関数で復帰した際に、通常とは別の戻り値が返るため親子を区別可能