Skip to content

代码: Wait, exit, 和 kill

sleepwakeup可以用于多种等待。一个有趣的例子,在第1章中介绍过,是子进程的exit和其父进程的wait之间的交互。在子进程死亡时,父进程可能已经在wait中休眠,或者可能在做其他事情;在后一种情况下,后续对wait的调用必须观察到子进程的死亡,可能是在它调用exit很久之后。xv6记录子进程死亡直到wait观察到它的方式是让exit将调用者置于ZOMBIE状态,它一直保持在该状态,直到父进程的wait注意到它,将子进程的状态更改为UNUSED,复制子进程的退出状态,并将子进程的进程ID返回给父进程。如果父进程在子进程之前退出,父进程会将子进程交给init进程,该进程会永久调用wait;因此每个子进程都有一个父进程来为其清理。一个挑战是避免同时发生的父子进程的waitexit以及同时发生的exitexit之间的竞争和死锁。

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_lockp->lockexit持有wait_lock是因为它是wakeup(p->parent)的条件锁,防止在wait中的父进程丢失唤醒。exit也必须持有p->lock,以防止在wait中的父进程在子进程最终调用swtch之前看到子进程处于ZOMBIE状态。exit以与wait相同的顺序获取这些锁以避免死锁。

exit在将其状态设置为ZOMBIE之前唤醒父进程可能看起来不正确,但这是安全的:虽然wakeup可能会导致父进程运行,但wait中的循环在子进程的p->lockscheduler释放之前无法检查子进程,所以waitexit将其状态设置为ZOMBIE很久之后才能看到退出的进程。

虽然exit允许一个进程自己终止,但kill允许一个进程请求另一个进程终止。让kill直接销毁受害者进程会过于复杂,因为受害者可能正在另一个CPU上执行,可能正在对内核数据结构进行敏感的更新序列。因此kill做得很少:它只是设置受害者的p->killed,如果它正在休眠,就唤醒它。最终受害者将进入或离开内核,此时usertrap中的代码如果p->killed被设置就会调用exit(它通过调用killed来检查)。如果受害者在用户空间运行,它很快就会通过进行系统调用或因为定时器(或其他设备)中断而进入内核。

如果受害者进程在sleep中,killwakeup的调用将导致受害者从sleep返回。这可能有潜在的危险,因为正在等待的条件可能不为真。然而,xv6对sleep的调用总是被包装在一个while循环中,该循环在sleep返回后重新测试条件。一些对sleep的调用也在循环中测试p->killed,并在设置时放弃当前活动。这只有在放弃是正确的情况下才会这样做。例如,管道读写代码在killed标志被设置时返回;最终代码将返回到trap,trap将再次检查p->killed并退出。

一些xv6的sleep循环不检查p->killed,因为代码正处于一个应该原子化的多步系统调用的中间。virtio驱动程序是一个例子:它不检查p->killed,因为一个磁盘操作可能是一组写操作中的一个,而所有这些写操作都是为了让文件系统处于一个正确的状态所必需的。一个在等待磁盘I/O时被杀死的进程在完成当前系统调用并且usertrap看到killed标志之前不会退出。