上一篇文章记录了fork的创建,但是创建的子进程并没有退出,这篇文章记录fork创建子进程的退出。
生成子进程的最重要的fork()我们已经有了,但这还不够,因为进程有出生就有死亡。让进程死亡的系统调用叫做exit(),直译作“退出”,其实它叫“自杀”更贴切,因为exit()通常是由要走的进程自己调用的,而且调用之后这个进程不是“退出”了,而是干脆消失了。
那么wait()是干什么的呢?如果你写过shell脚本的话,就很容易理解它的作用。我们执行一个程序之后,有时需要判断其返回值,这个返回值通常是通过 $? 得到。而你获取 $? 时,你执行的程序显然已经退出了(所以它才有返回值)。容易理解,这个返回值是我们执行的进程返回给shell的。换言之,是子进程返回给父进程的。父进程得到返回值的方法,就是执行一个wait()挂起,等子进程退出时,wait()调用方结束,并且父进程因此得到返回值。
如果用代码表示的话,那么应该是下面这个样子:
代码 kernel/main.c,Init。
void Init()
{
...
int pid = fork();
if (pid != 0) { /* parent process */
printf("parent is running, child pid:%d\n", pid);
int s;
int child = wait(&s);
printf("child (%d) exited with status: %d.\n", child, s);
} else { /* child process */
printf("child is running, pid:%d\n", getpid());
exit(123);
}
}
如果一切正常,子进程将会退出,而父进程将会得到返回值并打印出来。
跟fork()类似,exit()和wait()这两个函数是发送消息给MM,它们发送的消息分别是EXIT和WAIT。在MM中,这两个消息由do_exit()和do_wait()两个函数来处理。那么我们现在先来完成exit()和wait()两个函数的用户接口。
代码 lib/exit.c,exit(),这是新建的文件。
/*
* Terminate the current process.
*
* @param status The value returned to the parent.
*/
PUBLIC void exit(int status)
{
MESSAGE msg;
msg.type = EXIT;
msg.STATUS = status;
send_recv(BOTH, TASK_MM, &msg);
assert(msg.type == SYSCALL_RET);
}
代码 lib/wait.c,wait(),这是新建的文件。
/**
* Wait for the child process to terminiate.
*
* @param status The value returned from the child.
*
* @return PID of the terminated child.
*/
PUBLIC int wait(int * status)
{
MESSAGE msg;
msg.type = WAIT;
send_recv(BOTH, TASK_MM, &msg);
*status = msg.STATUS;
return (msg.PID == NO_TASK ? -1 : msg.PID);
}
然后在MM中添加EXIT和WAIT的消息处理。
代码 mm/main.c,EXIT和WAIT消息处理。
/* <Ring 1> The main loop of TASK MM. */
PUBLIC void task_mm()
{
...
case WAIT:
do_wait();
reply = 0;
break;
case EXIT:
do_exit(mm_msg.STATUS);
reply = 0;
break;
...
}
现在我们来完成处理EXIT和WAIT消息的两个函数:do_exit()和do_wait(),代码如下所示。
代码 mm/forkexit.c,do_exit()和do_wait()。
/**
* Perform the exit() syscall.
*
* if proc A calls exit(), the MM will do the following in this routine:
* <1> inform FS so that the fd-related things will be cleaned up
* <2> free A's memory
* <3> set A.exit_status, which is for the parent
* <4> depends on parent's status. if parent (sya P) is:
* (1) WAITING
* - clean P's WAITING bit, and
* - send P a message to unblock it
* - release A's proc_table[] slot
* (2) not WAITING
* - set A's HANGING bit
* <5> iterate proc_table[], if proc B is found as A's child, then:
* (1) make INIT the new parent of B, and
* (2) if INIT is WAITING and B is HANGING, thenL
* - clean INIT's WAITING bit, and
* - send INIT a messamge to unblock it
* - release B's proc_table[] slot
* else
* if INIT is WAITING but B is not HANGING, then
* - B will call exit()
* if B is HANGING but INIT is not WAITING, then
* - INIT will call wait()
*
* TERMs:
* - HANGING: everything except the proc_table entry has been cleaned up.
* - WAITING: a proc has at least one child, and it is waiting for the child(ren) to exit()
* - zombie: sya P has a child A, A will become a zombie if
* - A exit(), and
* - P does not wait(), neither does it exit(). that is to say, P just
* keeps running without terminating itself or its child
*
* @param status Exiting status for parent.
*/
PUBLIC void do_exit(int status)
{
int i;
int pid = mm_msg.source; /* PID of caller */
int parent_pid = proc_table[pid].p_parent;
PROCESS * p = &proc_table[pid];
/* tell FS, see fs_exit() */
MESSAGE msg2fs;
msg2fs.type = EXIT;
msg2fs.PID = pid;
send_recv(BOTH, TASK_FS, &msg2fs);
free_mem(pid);
p->exit_status = status;
if (proc_table[parent_pid].p_flags & WAITING) { /* parent is waiting */
proc_table[parent_pid].p_flags &= ~WAITING;
cleanup(&proc_table[pid]);
} else { /* parent is not waiting */
proc_table[pid].p_flags |= HANGING;
}
/* if the proc has any child, make INIT the new parent */
for (i = 0; i < NR_TASKS + NR_PROCS; i++) {
if (proc_table[i].p_parent == pid) { /* is a child */
proc_table[i].p_parent = INIT;
if ((proc_table[INIT].p_flags & WAITING) && (proc_table[i].p_flags & HANGING)) {
proc_table[INIT].p_flags &= ~WAITING;
cleanup(&proc_table[i]);
}
}
}
}
/**
* Do the last jobs to clean up a proc thoroughly:
* - Send proc's parent a message to unblock it, and
* - release proc's proc_table[] entry
*
* @param proc Process to clean up.
*/
PRIVATE void cleanup(PROCESS * proc)
{
MESSAGE msg2parent;
msg2parent.type = SYSCALL_RET;
msg2parent.PID = proc2pid(proc);
msg2parent.STATUS = proc->exit_status;
send_recv(SEND, proc->p_parent, &msg2parent);
proc->p_flags = FREE_SLOT;
}
/**
* Perform the wait() syscall.
*
* If proc P calls wait(), the MM will do the following in this routine:
* <1> iterate proc_table[],
* if proc A is found as P's child and it is HANGING
* - reply to P (cleanup() will send P a message to unblock it)
* - release A's proc_table[] entry
* - return (MM will go on with the next message loop)
* <2> if no child of P is HANGING
* - set P's WAITING bit
* <3> if P has no child at all
* - reply to P with error
* <4> return (MM will go on with the next message loop)
*/
PUBLIC void do_wait()
{
int pid = mm_msg.source;
int i;
int children = 0;
PROCESS * p_proc = proc_table;
for (i = 0; i < NR_TASKS + NR_PROCS; i++,p_proc++) {
if (p_proc->p_parent == pid) {
children++;
if (p_proc->p_flags & HANGING) {
cleanup(p_proc);
return;
}
}
}
if (children) {
/* has children, but no child is HANGING */
proc_table[pid].p_flags |= WAITING;
} else {
/* no child at all */
MESSAGE msg;
msg.type = SYSCALL_RET;
msg.PID = NO_TASK;
send_recv(SEND, pid, &msg);
}
}
代码 mm/main.c,free_mem。
/**
* Free a memory block. Because a memory block is corresponding with a PID, so
* we don't need to really `free' anything. In another word, a memory block is
* dedicated to one and only one PID, no matter what proc actually uses this
* PID.
*
* @param pid Whose memory is to be freed.
*
* @return Zero if success.
*/
PUBLIC int free_mem(int pid)
{
return 0;
}
想像得出,do_exit/do_wait跟msg_send/msg_receive这两对函数是有点类似的,它们最终都是实现一次“握手”。
假设进程P有子进程A。而A调用exit(),那么MM将会:
如果P调用wait(),那么MM将会:
为配合exit()和wait(),进程又多了两种状态:WAITING和HANGING。如果一个进程X被置了HANGING位,那么X的所有资源都已被释放,只剩一个进程表项还占着。为什么要占着进程表项不释放呢?因为这个进程表项里面有个新成员:exit_status,它记录了X的返回值。只有当X的父进程通过调用wait()取走了这个返回值,X的进程表项才被释放。
如果一个进程Y被置了WAITINT位,意味着Y至少有一个子进程,并且正在等待某个子进程退出。
此时你可能会想,如果一个子进程Z试图退出,但它的父进程却没有调用wait(),那Z的进程表项岂不一直占着得不到释放码?事情的确如此,而且有个专门用来称呼像Z这样的进程,叫做“僵尸进程(zombie)”。
如果一个进程Q有子进程,但它没有wait()就自己先exit()了,那么Q的子进程不会变成zombie,因为MM会把它们过继给Init,变成Init的子进程。也就是说,Init应该被设计成不停地调用wait(),以便让Q的子进程们退出并释放进程表项。
既然如此,让我们改造一下Init,在结尾处添加不停wait()的代码:
代码 kernel/main.c,Init。
void Init()
{
...
while (1) {
int s;
int child = wait(&s);
printf("child (%d) exited with status: %d.\n", child, s);
}
}
这样do_exit()和do_wait()做的事情我们就了解清楚了。不过事情还没完,因为FS接到MM的进程退出消息之后还要做些工作,见如下代码。
代码 fs/main.c,fs_exit()。
/**
* <Ring 1> The main loop of TASK FS.
*/
PUBLIC void task_fs()
{
...
case EXIT:
fs_msg.RETVAL = fs_exit();
break;
...
}
...
/**
* Perform the aspects of exit() that relate to files.
*
* @return Zero if success.
*/
PRIVATE int fs_exit()
{
int i;
PROCESS * p = &proc_table[fs_msg.PID];
for (i = 0; i < NR_FILES; i++) {
if (p->filp[i]) {
/* release the inode */
p->filp[i]->fd_inode->i_cnt--;
/* release the file desc slot */
if (--p->filp[i]->fd_cnt == 0) {
p->filp[i]->fd_inode = 0;
}
p->filp[i] = 0;
}
}
return 0;
}
完成了exit()和wait(),子进程的产生和消亡这个过程就都有了,我们接下来就可以编译运行一下看看,由于我们新增加了C文件,所以不要忘记更改Makefile。运行效果如下图所示。
我们看到了“child (9) exited with status: 123.”这一行输出,这是由父进程Init打印的,看上去一切良好。
欢迎关注我的公众号
?
公众号中对应文章附有当前文章代码下载说明。