《ORANGE’S:一个操作系统的实现》读书笔记(三十四)内存管理(二)

发布时间:2024年01月19日

上一篇文章记录了fork的创建,但是创建的子进程并没有退出,这篇文章记录fork创建子进程的退出。

exit和wait

生成子进程的最重要的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将会:

  1. 告诉FS:A退出,请做相应处理
  2. 释放A占用的内存
  3. 判断P是否正在WAITING
    • 如果是:
      • 清除P的WAITING位
      • 向P发送消息以解除阻塞(到此P的wait()函数结束)
      • 释放A的进程表项(到此A的exit()函数结束)
    • 如果否:
      • 设置A的HANGING位
  4. 遍历proc_table[],如果发现A有子进程B,那么
    • 将Init进程设置为B的父进程(换言之,将B过继给Init)
    • 判断是否满足Init正在WAITING且B正在HANGING
      • 如果是:
        • 清除Init的WAITING位
        • 向Init发送消息以解除阻塞(到此Init的wait()函数结束)
        • 释放B的进程表项(到此B的exit()函数结束)
      • 如果否:
        • 如果Init正在WAITING但B并没有HANGING,那么“握手”会在将来B调用exit()时发生
        • 如果B正在HANGING但Init并没有WAITING,那么“握手”会在将来Init调用wait()时发生

如果P调用wait(),那么MM将会:

  1. 遍历proc_table[],如果发现A是P的子进程,并且它正在HANGING,那么
    • 向P发送消息以解除阻塞(到此P的wait()函数结束)
    • 释放A的进程表项(到此A的exit()函数结束)
  2. 如果P的子进程没有一个在HANGING,则
    • 设P的WAITING位
  3. 如果P压根儿没有子进程,则
    • 向P发送消息,消息携带一个表示出错的返回值(到此P的wait()函数结束)

为配合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打印的,看上去一切良好。

欢迎关注我的公众号


?

公众号中对应文章附有当前文章代码下载说明。

文章来源:https://blog.csdn.net/zorro_z/article/details/135691472
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。