テキストの60ページ
sleepとwakeupは、条件が整うまで待つ事が必要な、様々な状況で使用され得る。
第0章で見たように、親プロセスは、子プロセスが終了するのを待つためにwait関数を呼ぶことが出来る。
xv6では、子プロセスが終了(exit)したとき、すぐには終了(die)しない。
その代わり、親プロセスが終了出来る状態になるためにwaitを呼ぶまで、プロセスの状態をZOMBIEに変更する。
親プロセスには、そのプロセスに割り当てられたメモリを解放し、再利用のためにproc構造体を準備する責任がある。
それぞれのプロセスのproc構造体は、p->parentとして親プロセスを指すポインタを保持する。
子プロセスより先に親プロセスが終了した場合、一番最初のプロセスであるinitがその子プロセスを受け継ぎ、その終了を待つ。
このステップは、いくつかのプロセスが子プロセスが終了した後にクリーンアップする事を確実にするために必要である。
すべてのプロセスのproc構造体は、ptable.lockによって保護される。
waitは、ptable.lockの獲得から始める。
そしたら、そのプロセスの子プロセスを捜すためにプロセステーブルを走査する。
もしwaitが、現在のプロセスがまだ終了してない子プロセスを持つということを見つけた場合、その子プロセスが終了するのを待つためにsleepを呼び、それを繰り返す。
そして、sleep中にptable.lockが解放される。
それは以前の節で見た特別な場合に相当する。(特別 = 同じロックの獲得と解放を別のプロセスで行うパターンということ?)
proc.cのwait関数
// Wait for a child process to exit and return its pid.
// Return -1 if this process has no children.
int
wait(void)
{
struct proc *p;
int havekids, pid;
acquire(&ptable.lock);
for(;;){
// Scan through table looking for zombie children.
havekids = 0;
for(p = ptable.proc; p < &ptable.proc[NPROC]; p++){
if(p->parent != proc)
continue;
havekids = 1;
if(p->state == ZOMBIE){
// Found one.
pid = p->pid;
kfree(p->kstack);
p->kstack = 0;
freevm(p->pgdir);
p->state = UNUSED;
p->pid = 0;
p->parent = 0;
p->name[0] = 0;
p->killed = 0;
release(&ptable.lock);
return pid;
}
}
// No point waiting if we don't have any children.
if(!havekids || proc->killed){
release(&ptable.lock);
return -1;
}
// Wait for children to exit. (See wakeup1 call in proc_exit.)
sleep(proc, &ptable.lock); //DOC: wait-sleep
}
}
exitは、ptable.lockを獲得し、それから現在のプロセスの親プロセスを起こす。
exit関数が現在のプロセスをZOMBIEとしてマークする前の段階で、親プロセスを起こすのは時期尚早に見えるだろう。
しかしこれは安全である。
親プロセスはその時点でRUNNABLEとしてマークされてるとは言え、exitがスケジューラに処理を移すためにschedを呼ぶ事によってptable.lockを解放するまで、waitの中のループは実行できないので、ZOMBIE状態になる前に終了中のプロセスをwaitで操作してしまうような事態は起きない。
exitはスケジューラを呼ぶ前に、終了中の現在のプロセスの子プロセスのすべての親プロセスをinitprocへ変更する。
最後にexitはCPUを手放すためにschedを呼ぶ。
proc.cのexit関数
// Exit the current process. Does not return.
// An exited process remains in the zombie state
// until its parent calls wait() to find out it exited.
void
exit(void)
{
struct proc *p;
int fd;
if(proc == initproc)
panic("init exiting");
// Close all open files.
for(fd = 0; fd < NOFILE; fd++){
if(proc->ofile[fd]){
fileclose(proc->ofile[fd]);
proc->ofile[fd] = 0;
}
}
iput(proc->cwd);
proc->cwd = 0;
acquire(&ptable.lock);
// Parent might be sleeping in wait().
wakeup1(proc->parent);
// Pass abandoned children to init.
for(p = ptable.proc; p < &ptable.proc[NPROC]; p++){
if(p->parent == proc){
p->parent = initproc;
if(p->state == ZOMBIE)
wakeup1(initproc);
}
}
// Jump into the scheduler, never to return.
proc->state = ZOMBIE;
sched();
panic("zombie exit");
}
そしたらスケジューラは、終了中のプロセスの親プロセス(waitのsleepの呼び出しによってスリープしている)を実行するために選択することが出来る。
sleepの呼び出しから戻るとき、ptable.lockを保持した状態になってるので、waitはプロセステーブルを再捜査し、state == ZOMBIEとなっている終了した子プロセスを捜すことが出来る。
そしたらその子プロセスのpidを記録し、そしてそのproc構造体をクリーンアップし、そのプロセスに割り当てられたメモリを解放する。(ここはwait関数のif(p−>state == ZOMBIE){の中の話)
これで子プロセスはexitにおけるクリーンアップ処理の大部分を完了したことになるが、子プロセスのp->kstackやp->pgdirは親プロセスが解放しなければならないというのが重要な点である。
子プロセスがexitを実行するとき、そのスタックはp->kstackとして割り当てられたメモリに置かれ、そのプロセス自身のページテーブルとして使う。
それらは、子プロセスが終了した後(exit内でsched経由でswtchが呼ばれた後)でないと解放できない。
これはスケジューラの手続きが、schedと呼ばれるスレッドのスタック上ではなく、それ自身のスタック上で動作するひとつの理由である。
waitとexitのコードの説明ですが、ちゃんと理解するためにはプロセスの仕組みについての総合的な知識が必要です。
自分の知識の一つ上ぐらいの事ならまだあーだこーだ言える余地があるんですが、この節は二・三段ぐらい上な感じで、なんとか理解は出来るけど受け身で「へぇそうなんだ」と思うことしかできませんでした。
親プロセスのp->parentはどうなってるのかというと、それはシェルから起動されたプログラムの場合は、親はシェルになってるはずです。(シェルはforkを使ってプログラムを実行するので)
ではシェルはどうなのかというと、ソースを全部読んだわけではないので、間に何かはさまってるかもしれませんが、initprocの子になってるはずです。
initprocがすべてのプロセスの最終的な親になるので、exit関数はinitprocで実行されるとpanicするようになってますね。
最近のコメント
たかたむ
はじめまして。初リアルフォース(R3ですが)で,同…
nokiyameego
ZFS poolのデバイスラベル破損で悩んていたと…
名前
しゅごい
Jane Doe
FYI Avoid Annoying Unexpe…
Jane Doe
ご存じとは思いますが、whileには、”~の間”と…
花粉症対策2019 – 日曜研究室
[…] 花粉症対策についてはこれまで次の記事を書いてきました。https://…
花粉症対策2019 – 日曜研究室
[…] 花粉症対策についてはこれまで次の記事を書いてきました。https://…
花粉症対策2019 – 日曜研究室
[…] 花粉症対策についてはこれまで次の記事を書いてきました。https://…
花粉症対策2019 – 日曜研究室
[…] 花粉症対策についてはこれまで次の記事を書いてきました。https://…
花粉症対策2019 – 日曜研究室
[…] 花粉症対策についてはこれまで次の記事を書いてきました。https://…