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関数で復帰した際に、通常とは別の戻り値が返るため親子を区別可能