通过linux0.11理解僵尸进程

2020-02-17 11:43:54 浏览数 (1)

首先僵尸进程产生的原因是子进程退出了,但是父进程没有回收他的资源(pcb),所以我们从源头开始分析这个过程。那就是子进程退出的时候。进程是通过exit系统调用退出的。 我们看一下exit函数的代码。

代码语言:javascript复制
int do_exit(long code)
{
    int i;
    // 释放代码段和数据段页表,页目录,物理地址
    free_page_tables(get_base(current->ldt[1]),get_limit(0x0f));
    free_page_tables(get_base(current->ldt[2]),get_limit(0x17));
    for (i=0 ; i<NR_TASKS ; i  )
        // 找出当前进程的子进程
        if (task[i] && task[i]->father == current->pid) {
            // 子进程的新父进程是进程id为1的进程
            task[i]->father = 1;
            /*
             父进程没有调wait,子进程退出了,然后父进程也退出了。没人回收子进程的pcb,给init进程发
            */
            if (task[i]->state == TASK_ZOMBIE)
                /* assumption task[1] is always init */
                (void) send_sig(SIGCHLD, task[1], 1);
        }
    // 关闭文件
    for (i=0 ; i<NR_OPEN ; i  )
        if (current->filp[i])
            sys_close(i);
    // 回写inode到硬盘
    iput(current->pwd);
    current->pwd=NULL;
    iput(current->root);
    current->root=NULL;
    iput(current->executable);
    current->executable=NULL;
    // 是会话首进程并打开了终端
    if (current->leader && current->tty >= 0)
        tty_table[current->tty].pgrp = 0;
    if (last_task_used_math == current)
        last_task_used_math = NULL;
    // 是会话首进程,则通知会话里的所有进程会话结束
    if (current->leader)
        kill_session();
    // 更新状态
    current->state = TASK_ZOMBIE;
    current->exit_code = code;
    // 通知父进程
    tell_father(current->father);
    // 重新调度进程(tell_father里已经调度过了)
    schedule();
    return (-1);    /* just to suppress warnings */
}

上面的代码主要做了几个事情。 1 修改当前进程的子进程的新父进程为init进程。如果子进程已经退出了,则通知init进程。否则init无法回收该子进程的资源。 2 释放一系列资源。 3 修改进程状态和退出码。 4 给父进程发SIGCHLD信号。 5 重新调度。因为自己退出了。

我们可以知道,一个进程调用exit的时候,他就已经成为僵尸进程了。这时候如果父进程不处理这个事情,则退出的子进程会一直占据pcb。父进程有两种方式可以处理子进程的退出。 1 调waitpid系列函数。 2 处理SIGCHLD信号,在信号处理函数里执行waitpid系列函数。 我们看看waitpid的代码。

代码语言:javascript复制
// 等待pid进程退出,并且把退出码写到stat_addr变量
int sys_waitpid(pid_t pid,unsigned long * stat_addr, int options)
{
    int flag, code;
    struct task_struct ** p;

    verify_area(stat_addr,4);
repeat:
    flag=0;
    for(p = &LAST_TASK ; p > &FIRST_TASK ; --p) {
        // 过滤不符合条件的
        if (!*p || *p == current)
            continue;
        // 不是当前进程的子进程则跳过
        if ((*p)->father != current->pid)
            continue;
        // pid大于0说明等待某一个子进程
        if (pid>0) {
            // 不是等待的子进程则跳过
            if ((*p)->pid != pid)
                continue;
        } else if (!pid) {
            // pid等于0则等待进程组中的进程,不是当前进程组的进程则跳过
            if ((*p)->pgrp != current->pgrp)
                continue;
        } else if (pid != -1) {
            // 不等于-1说明是等待某一个组的,但不是当前进程的组,组id是-pid的组,不是该组则跳过
            if ((*p)->pgrp != -pid)
                continue;
        } 
        // else {
        //  等待所有进程
        // }
        // 找到了一个符合条件的进程
        switch ((*p)->state) {
            // 子进程已经退出,这个版本没有这个状态
            case TASK_STOPPED:
                if (!(options & WUNTRACED))
                    continue;
                put_fs_long(0x7f,stat_addr);
                return (*p)->pid;
            case TASK_ZOMBIE:
                // 子进程已经退出,则返回父进程
                current->cutime  = (*p)->utime;
                current->cstime  = (*p)->stime;
                flag = (*p)->pid;
                code = (*p)->exit_code;
                release(*p);
                put_fs_long(code,stat_addr);
                return flag;
            default:
                // flag等于1说明子进程还没有退出
                flag=1;
                continue;
        }
    }
    // 还没有退出的进程
    if (flag) {
        // 设置了非阻塞则返回
        if (options & WNOHANG)
            return 0;
        // 否则父进程挂起
        current->state=TASK_INTERRUPTIBLE;
        // 重新调度
        schedule();
        /*
            在schedule函数里,如果当前进程收到了信号,会变成running状态,
            如果current->signal &= ~(1<<(SIGCHLD-1)))为0,即...0000000100000... & ...111111110111111...
            说明当前需要处理的信号是SIGCHLD,因为signal不可能为全0,否则进程不可能被唤醒,
            即有子进程退出,跳到repeat找到该退出的进程,否则说明是其他信号导致了进程变成可执行状态,
            阻塞的进程被信号唤醒,返回EINTR
        */
        if (!(current->signal &= ~(1<<(SIGCHLD-1))))
            goto repeat;
        else
            return -EINTR;
    }
    return -ECHILD;
}

代码比较多,我们主要关注switch的TASK_ZOMBIE分支就行。我们看到如果父进程调用waitpid函数,会遍历子进程,当子进程是TASK_ZOMBIE状态(即退出了),父进程会保存子进程的退出码和把子进程运行的时间累加到自己的时间里。最后调用release函数。下面看看release函数的代码。

代码语言:javascript复制
// 释放pcb的一页内存,重新调度进程
void release(struct task_struct * p)
{
    int i;

    if (!p)
        return;
    for (i=1 ; i<NR_TASKS ; i  )
        if (task[i]==p) {
            task[i]=NULL;
            free_page((long)p);
            schedule();
            return;
        }
}

释放pcb然后重新调用。讲到这里,我们看到,如果按照这个流程。那子进程退出的时候,就不会成为真正的僵尸进程。因为他的全部资源会得到释放。否则子进程一直处于TASK_ZOMBIE状态,pcb得不到释放。成为真正的僵尸进程。这就是僵尸进程产生的原因和解决方案。

下面再来看一下另一个问题。如果一个子进程的父进程先退出会怎样?我们回到exit函数,发现有这样一段代码。

代码语言:javascript复制
for (i=0 ; i<NR_TASKS ; i  )
    // 找出当前进程的子进程
    if (task[i] && task[i]->father == current->pid) {
        // 子进程的新父进程是进程id为1的进程
        task[i]->father = 1;
        /*
         父进程没有调wait,子进程退出了,然后父进程也退出了。没人回收子进程的pcb,给init进程发
        */
        if (task[i]->state == TASK_ZOMBIE)
            /* assumption task[1] is always init */
            (void) send_sig(SIGCHLD, task[1], 1);
    }

如果一个父进程比子进程先退出,则系统会把当前进程的所有子进程的父进程修改为进程为1的进程(init进程)。如果这时候当前进程的某些进程已经退出了(当前进程没有调waitpid处理子进程的退出),则给init进程发送SIGCHLD信号。这时候init会回收这个子进程的pcb。下面是init进程的逻辑。wait即等待任意一个子进程退出。

代码语言:javascript复制
while (pid != wait(&i)

0 人点赞