7.8 代码:wait, exit和kill
Sleep
和wakeup
可用于多种等待。第一章介绍的一个有趣的例子是子进程exit
和父进程wait
之间的交互。在子进程死亡时,父进程可能已经在wait
中休眠,或者正在做其他事情;在后一种情况下,随后的wait
调用必须观察到子进程的死亡,可能是在子进程调用exit
后很久。xv6记录子进程终止直到wait
观察到它的方式是让exit
将调用方置于ZOMBIE
状态,在那里它一直保持到父进程的wait
注意到它,将子进程的状态更改为UNUSED
,复制子进程的exit
状态码,并将子进程ID返回给父进程。如果父进程在子进程之前退出,则父进程将子进程交给init
进程,init
进程将永久调用wait
;因此,每个子进程退出后都有一个父进程进行清理。主要的实现挑战是父级和子级wait
和exit
,以及exit
和exit
之间可能存在竞争和死锁。
Wait
使用调用进程的p->lock
作为条件锁,以避免丢失唤醒,并在开始时获取该锁(kernel/proc.c:398)。然后它扫描进程表。如果它发现一个子进程处于ZOMBIE
状态,它将释放该子进程的资源及其proc
结构体,将该子进程的退出状态码复制到提供给wait
的地址(如果不是0),并返回该子进程的进程ID。如果wait
找到子进程但没有子进程退出,它将调用sleep
以等待其中一个退出(kernel/proc.c:445),然后再次扫描。这里,sleep
中释放的条件锁是等待进程的p->lock
,这是上面提到的特例。注意,wait
通常持有两个锁:它在试图获得任何子进程的锁之前先获得自己的锁;因此,整个xv6都必须遵守相同的锁定顺序(父级,然后是子级),以避免死锁。
Wait
查看每个进程的np->parent
以查找其子进程。它使用np->parent
而不持有np->lock
,这违反了通常的规则,即共享变量必须受到锁的保护。np
可能是当前进程的祖先,在这种情况下,获取np->lock
可能会导致死锁,因为这将违反上述顺序。这种情况下无锁检查np->parent
似乎是安全的:进程的parent
字段仅由其父进程更改,因此如果np->parent==p
为true
,除非当前流程更改它,否则该值无法被更改,
Exit
(kernel/proc.c:333)记录退出状态码,释放一些资源,将所有子进程提供给init
进程,在父进程处于等待状态时唤醒父进程,将调用方标记为僵尸进程(zombie),并永久地让出CPU。最后的顺序有点棘手。退出进程必须在将其状态设置为ZOMBIE
并唤醒父进程时持有其父进程的锁,因为父进程的锁是防止在wait
中丢失唤醒的条件锁。子级还必须持有自己的p->lock
,否则父级可能会看到它处于ZOMBIE
状态,并在它仍运行时释放它。锁获取顺序对于避免死锁很重要:因为wait
先获取父锁再获取子锁,所以exit
必须使用相同的顺序。
Exit
调用一个专门的唤醒函数wakeup1
,该函数仅唤醒父进程,且父进程必须正在wait
中休眠(kernel/proc.c:598)。在将自身状态设置为ZOMBIE
之前,子进程唤醒父进程可能看起来不正确,但这是安全的:虽然wakeup1
可能会导致父进程运行,但wait
中的循环在scheduler
释放子进程的p->lock
之前无法检查子进程,所以wait
在exit
将其状态设置为ZOMBIE
(kernel/proc.c:386)之前不能查看退出进程。
exit
允许进程自行终止,而kill
(kernel/proc.c:611)允许一个进程请求另一个进程终止。对于kill
来说,直接销毁受害者进程(即要杀死的进程)太复杂了,因为受害者可能在另一个CPU上执行,也许是在更新内核数据结构的敏感序列中间。因此,kill
的工作量很小:它只是设置受害者的p->killed
,如果它正在睡眠,则唤醒它。受害者进程终将进入或离开内核,此时,如果设置了p->killed
,usertrap
中的代码将调用exit
。如果受害者在用户空间中运行,它将很快通过进行系统调用或由于计时器(或其他设备)中断而进入内核。
如果受害者进程在sleep
中,kill
对wakeup
的调用将导致受害者从sleep
中返回。这存在潜在的危险,因为等待的条件可能不为真。但是,xv6对sleep
的调用总是封装在while
循环中,该循环在sleep
返回后重新测试条件。一些对sleep
的调用还在循环中测试p->killed
,如果它被设置,则放弃当前活动。只有在这种放弃是正确的情况下才能这样做。例如,如果设置了killed
标志,则管道读写代码返回;最终代码将返回到陷阱,陷阱将再次检查标志并退出。
一些XV6的sleep
循环不检查p->killed
,因为代码在应该是原子操作的多步系统调用的中间。virtio驱动程序(kernel/virtio_disk.c:242)就是一个例子:它不检查p->killed
,因为一个磁盘操作可能是文件系统保持正确状态所需的一组写入操作之一。等待磁盘I/O时被杀死的进程将不会退出,直到它完成当前系统调用并且usertrap
看到killed
标志