通过Unix的exit和wait系统调用的说明,我们可以知道如果一个进程exit了,并且它的父进程调用了wait系统调用,父进程的wait会返回。wait函数的返回表明当前进程的一个子进程退出了。所以接下来我们看一下wait系统调用的实现。
它里面包含了一个大的循环。当一个进程调用了wait系统调用,它会扫描进程表单,找到父进程是自己且状态是ZOMBIE的进程。从上一节可以知道,这些进程已经在exit函数中几乎要执行完了。之后由父进程调用的freeproc函数,来完成释放进程资源的最后几个步骤。我们看一下freeproc的实现,
这是关闭一个进程的最后一些步骤。如果由正在退出的进程自己在exit函数中执行这些步骤,将会非常奇怪。这里释放了trapframe,释放了page table。如果我们需要释放进程内核栈,那么也应该在这里释放。但是因为内核栈的guard page,我们没有必要再释放一次内核栈。不管怎样,当进程还在exit函数中运行时,任何这些资源在exit函数中释放都会很难受,所以这些资源都是由父进程释放的。
wait不仅是为了父进程方便的知道子进程退出,wait实际上也是进程退出的一个重要组成部分。在Unix中,对于每一个退出的进程,都需要有一个对应的wait系统调用,这就是为什么当一个进程退出时,它的子进程需要变成init进程的子进程。init进程的工作就是在一个循环中不停调用wait,因为每个进程都需要对应一个wait,这样它的父进程才能调用freeproc函数,并清理进程的资源。
当父进程完成了清理进程的所有资源,子进程的状态会被设置成UNUSED。之后,fork系统调用才能重用进程在进程表单的位置。
学生提问:在exit系统调用中,为什么需要在重新设置父进程之前,先获取当前进程的父进程?
Robert教授:这里其实就是在防止一个进程和它的父进程同时退出。通常情况下,一个进程exit,它的父进程正在wait,一切都正常。但是也可能一个进程和它的父进程同时exit。所以当子进程尝试唤醒父进程,并告诉它自己退出了时,父进程也在退出。这些代码我一年前还记得是干嘛的,现在已经记不太清了。它应该是处理这种父进程和子进程同时退出的情况。如果不是这种情况的话,一切都会非常直观,子进程会在后面通过wakeup函数唤醒父进程。
学生提问:为什么我们在唤醒父进程之后才将进程的状态设置为ZOMBIE?难道我们不应该在之前就设置吗?
Robert教授:正在退出的进程会先获取自己进程的锁,同时,因为父进程的wait系统调用中也需要获取子进程的锁,所以父进程并不能查看正在执行exit函数的进程的状态。这意味着,正在退出的进程获取自己的锁到它调用sched进入到调度器线程之间(注,因为调度器线程会释放进程的锁),父进程并不能看到这之间代码引起的中间状态。所以这之间的代码顺序并不重要。大部分时候,如果没有持有锁,exit中任何代码顺序都不能工作。因为有了锁,代码的顺序就不再重要,因为父进程也看不到进程状态。
这里我想要强调的是,直到子进程exit的最后,它都没有释放所有的资源,因为它还在运行的过程中,所以不能释放这些资源。相应的其他的进程,也就是父进程,释放了运行子进程代码所需要的资源。这样的设计可以让我们极大的精简exit的实现。