专栏首页原创分享通过linux0.11理解僵尸进程

通过linux0.11理解僵尸进程

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

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的代码。

// 等待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函数的代码。

// 释放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函数,发现有这样一段代码。

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即等待任意一个子进程退出。

while (pid != wait(&i)

本文分享自微信公众号 - 编程杂技(theanarkh),作者:theanarkh

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-02-07

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 进程的执行和挂起

    进程是对逻辑的抽象,我们从操作系统的书籍中对进程有了很多的认识,但是对进程的实现可能不太了解,这篇文章尝试解释一下关于进程实现的大致原理。 进程的实现,...

    theanarkh
  • 理解进程和线程

    进程和线程是操作系统里很重要的概念,但是所有的东西都会落实到代码。看起来很复杂的进程线程,其实在操作系统的代码里。也只是一些数据结构和算法。只不过他比一...

    theanarkh
  • 理解进程的新建和执行过程

    本文以linux0.11版本为基础,分析进程的内存布局,现代版本已经发生比较大的变化,都是很多原理都是类似的。 系统维护了一个全局的数据结构叫GDT( Glob...

    theanarkh
  • 如何查看并杀死僵尸进程

    In UNIX System terminology, a process that has terminated,but whose parent has n...

    小小科
  • 如何查看并杀死僵尸进程

    In UNIX System terminology, a process that has terminated,but whose parent has n...

    小小科
  • 基础|进程和线程模型

    计算机中最重要的模型之一,莫过于进程模型和线程模型了,对于它们的深刻理解,直接关系到软件开发,算法设计等计算机细分方向。 01 — 进程模型 进程是指一个具有一...

    double
  • 进程

    以前的计算机一次只能执行一个程序,后来有了多道程序设计的电脑,可以宏观上执行多个程序。由此产生了进程的概念

    用户5426759
  • Linux进程基础

      计算机实际上可以做的事情实质上非常简单,比如计算两个数的和,再比如在内存中寻找到某个地址等等。这些最基础的计算机动作被称为指令(instruction)。所...

    用户6754675
  • 浅谈3A和进程

    3计帐(Accounting):记录用户对各种网络服务的用量,并提供给计费系统。整个系统在网络管理与安全问题中十分有效。

    py3study
  • linux进程管理

    查看进行使用的指令是 ps ,一般来说使用的参数是 ps -aux,ps -ef,正常与grep连用

    小小咸鱼YwY

扫码关注云+社区

领取腾讯云代金券