Appearance
代码: Wait, exit, 和 kill
sleep
和wakeup
可以用于多种等待。一个有趣的例子,在第1章中介绍过,是子进程的exit
和其父进程的wait
之间的交互。在子进程死亡时,父进程可能已经在wait
中休眠,或者可能在做其他事情;在后一种情况下,后续对wait
的调用必须观察到子进程的死亡,可能是在它调用exit
很久之后。xv6记录子进程死亡直到wait
观察到它的方式是让exit
将调用者置于ZOMBIE
状态,它一直保持在该状态,直到父进程的wait
注意到它,将子进程的状态更改为UNUSED
,复制子进程的退出状态,并将子进程的进程ID返回给父进程。如果父进程在子进程之前退出,父进程会将子进程交给init
进程,该进程会永久调用wait
;因此每个子进程都有一个父进程来为其清理。一个挑战是避免同时发生的父子进程的wait
和exit
以及同时发生的exit
和exit
之间的竞争和死锁。
wait
开始时获取wait_lock
,它充当条件锁,帮助确保wait
不会错过来自退出子进程的wakeup
。然后wait
扫描进程表。如果它找到一个处于ZOMBIE
状态的子进程,它会释放该子进程的资源及其proc
结构,将子进程的退出状态复制到提供给wait
的地址(如果不是0),并返回子进程的进程ID。如果wait
找到子进程但没有一个退出,它会调用sleep
来等待它们中的任何一个退出,然后再次扫描。wait
通常持有两个锁,wait_lock
和某个进程的pp->lock
;避免死锁的顺序是先wait_lock
然后是pp->lock
。
exit
记录退出状态,释放一些资源,调用reparent
将其子进程交给init
进程,唤醒父进程以防它在wait
中,将调用者标记为僵尸,并永久让出CPU。exit
在此序列中同时持有wait_lock
和p->lock
。exit
持有wait_lock
是因为它是wakeup(p->parent)
的条件锁,防止在wait
中的父进程丢失唤醒。exit
也必须持有p->lock
,以防止在wait
中的父进程在子进程最终调用swtch
之前看到子进程处于ZOMBIE
状态。exit
以与wait
相同的顺序获取这些锁以避免死锁。
exit
在将其状态设置为ZOMBIE
之前唤醒父进程可能看起来不正确,但这是安全的:虽然wakeup
可能会导致父进程运行,但wait
中的循环在子进程的p->lock
被scheduler
释放之前无法检查子进程,所以wait
在exit
将其状态设置为ZOMBIE
很久之后才能看到退出的进程。
虽然exit
允许一个进程自己终止,但kill
允许一个进程请求另一个进程终止。让kill
直接销毁受害者进程会过于复杂,因为受害者可能正在另一个CPU上执行,可能正在对内核数据结构进行敏感的更新序列。因此kill
做得很少:它只是设置受害者的p->killed
,如果它正在休眠,就唤醒它。最终受害者将进入或离开内核,此时usertrap
中的代码如果p->killed
被设置就会调用exit
(它通过调用killed
来检查)。如果受害者在用户空间运行,它很快就会通过进行系统调用或因为定时器(或其他设备)中断而进入内核。
如果受害者进程在sleep
中,kill
对wakeup
的调用将导致受害者从sleep
返回。这可能有潜在的危险,因为正在等待的条件可能不为真。然而,xv6对sleep
的调用总是被包装在一个while
循环中,该循环在sleep
返回后重新测试条件。一些对sleep
的调用也在循环中测试p->killed
,并在设置时放弃当前活动。这只有在放弃是正确的情况下才会这样做。例如,管道读写代码在killed标志被设置时返回;最终代码将返回到trap,trap将再次检查p->killed
并退出。
一些xv6的sleep
循环不检查p->killed
,因为代码正处于一个应该原子化的多步系统调用的中间。virtio驱动程序是一个例子:它不检查p->killed
,因为一个磁盘操作可能是一组写操作中的一个,而所有这些写操作都是为了让文件系统处于一个正确的状态所必需的。一个在等待磁盘I/O时被杀死的进程在完成当前系统调用并且usertrap
看到killed标志之前不会退出。