はじめに
xv6でのfork関数について、親プロセスと子プロセスとのパラメタ設定の比較を意識してまとめてみた。
なお、context switch(struct context
が関係してる)やsyscall, iret(struct trapframe
が関係してる)の詳細は省くので、switching編を参考にしてください(あんまりまとまってないけど)
Reference
fork
user側でforkが実行されたとき、sys_forkが実行される。sys_forkは即fork()
を実行する。fork()
は
https://github.com/mit-pdos/xv6-public/blob/xv6-rev11/proc.c#L181
の実装である。
上記のコードに沿って、parent processとchild processの設定を比較したものが次のようになる:
process\item | struct proc |
struct trapframe |
fork後の%eip | fork後の%esp | 戻り値 | kernel stack |
---|---|---|---|---|---|---|
parent | curproc |
curproc->tf |
curproc->tf->eip(変化なし) | curproc->tf->esp(変化なし) | return pid; | curproc->kstack(変化なし) |
child | np |
*np->tf=*curproc->tf |
forkret => trapret => parentの%eip | np->tf->espで変化しない(pagingで対応) | np->tf->eax = 0 | allocproc => p->kstack = kalloc() |
子プロセスと親プロセスとでfork後の%esp(virtual memory address)は同じであるが、copyuvmで異なるpagingにしているため、指し示すphysical memory addressの場所が異なる。
親プロセスと子プロセスは以下のような時間経過をたどる1:
やや詳細に書くと以下のような感じ:
- [parent process]
int $T_SYSCALL
を実行し、sys_forkが呼ばれる。 - [parent process] sys_fork実行することで、
np
(子プロセスのdescriptor)を構成する。このとき、forkret関数やtrapret関数がstack上に格納される。trapframe自体はそのまま複製されるので、trapframeのeipメンバの値は親プロセスのものと同じである。 - [parent process]
np->state = RUNNABLE;
と設定する。 - [parent process] sys_forkがreturnし、trapretのiretの実行で、user landに戻る。このプロセスは特に何もいじってないので、そのまま進行する。
- [parent process] sched() -> swtchが呼ばれる(context switch)。例えば、local timerなどで定期的に発生したinterruptでyield() -> sched()が呼ばれるなど。
- [kernel process(swapper)] swtichkvmから再開する。
- [kernel process(swapper)] p->state == RUNNABLEのprocessが選出される。このとき、3で設定したprocessが選ばれるとしよう(このタイミングでなくてもいずれ選ばれるので)
- [kernel process(swapper)]
swtch
関数が実行される(context switch) - [child process] context switchによって、stack上のcontext->eipのaddressにジャンプする。これによりforkret関数が実行される
- [child process] forkretが終わったらtrapretが実行される
- [child process] trapretのiretの実行でuser landに戻る。レジスタはtrapframeに保存されているので復元される。このとき、trapframeのeipメンバは親プロセスのものと同じだったから、後は親プロセスと同じ変遷をたどることになる。
Note) xv6では子供は親プロセスの後に開始するが、実はlinux kernelでは、子プロセスが親よりも先に実行される。また、xv6はcontext switchで必ずswapper processを経由するが、linux kernelの場合はprocessから他のprocessに直接遷移する。これらの詳細は次の機会としたい。
Note) sys_execはforkと親戚のような関数だが、forkよりも単純なsyscallだ:
xv6ではsys_exec呼ばれるとkernel stack(=not user stack)上で作業するから
— Kenta Nakajima (@knknkn26918) August 6, 2019
+ 今のuser space完全に無視して、下のように新しいプログラム用のuser spaceとそれ用のpagingをゼロから作成
+ 今のprocess descriptorに%eip, %esp, pagingを紐付けて、cr3を再ロード
だけやな。forkよりもロジックは簡単 pic.twitter.com/zoOazBbC9h
ELFファイルの構造については、リンカ・ローダ実践開発テクニックのch.2が詳しいのでそちらを読んでください。