19-孤儿进程与僵尸进程

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_35733751/article/details/82454359

1. 孤儿进程

   简单来说,当父进程先于子进程结束,子进程就会成为孤儿进程,通常会被init进程领养,并由init进程进行回收孤儿进程。

  在前面的学习中我们说过,父进程调用fork成功创建子进程的时候,父进程和子进程谁先执行是不确定的。假设这么一种情况:父进程调用fork后立马执行,而子进程由于某种原因一直在循环执行,迟迟不结束,当父进程先运行结束时,子进程就成了孤儿进行。


2. 示例程序

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void) {
    pid_t pid;
    pid = fork();
    //子进程
    if (pid == 0) {
        //死循环
        while (1) {
            printf("I am child, my parent pid = %d\n", getppid());
            //每次间隔1秒
            sleep(1);
        }
    //父进程
    } else if (pid > 0) {
        printf("I am parent, my pid is = %d\n", getpid());
        //间隔9秒
        sleep(9);
        //提示父进程即将执行结束
        printf("parent is die\n");
        //父进程结束
    } else {
        perror("fork");
        return 1;
    }
    return 0;
}



程序执行结果:

这里写图片描述

  从上图可以看出./orphan一执行,首先父进程打印自己的pid,随后子进程每隔1秒打印一次父进程的pid,当父进程打印father to die后就结束退出了,再次打印子进程的父进程pid就变成了1868。那么问题来了,1868进程到底是哪个进程呢?



于是执行ps -aux命令查看,1868号进程是属于用户的init进程。
这里写图片描述

  由此我们可以确定当父进程比子进程先结束时,子进程就变成孤儿进程了,这时候会由init进程领养这些孤儿进程,从上图来看子进程被划归到用户init进程下了。需要注意的是这些孤儿进程有可能会划归到系统init进程下,也有可能划归到用户的init进程下,这个并不确定。


3. 进程控制块PCB

  在学习文件IO和文件目录时,简单提过进程控制块PCB。

  当一个进程运行起来,系统内核会为每一个进程维护一个进程控制块PCB(进程描述符)这样的数据结构。

  操作系统用PCB来描述进程的信息和相关资源,利用PCB来控制和管理进程,换句话说,进程控制块PCB是系统感知进程存在的唯一标志,那么我们可以理解为,进程在linux系统中实际上是一个PCB结构体,即task_struct结构体(这可能有点不太准确)。



task_struct结构体其内部成员有将近300多个,下面是一些比较重要和常见的成员定义:

task_struct{
    进程id
    进程的状态
    描述虚拟地址空间的信息
    当前工作目录
    umask掩码
    文件描述符表
    和信号相关的信息
    用户id和组id
    会话(Session)和进程组
    ......
}

   比如大家在使用windows系统时打开的程序越多,在任务管理器中的进程数,cpu,内存等资源占用就会越高,当你关闭某些程序时,就会释放一些系统资源。

这里写图片描述

   在linux中也是一样的道理,由于linux系统会对每一个运行的进程分配一个task_struct结构体空间,当进程结束时,linux系统就会释放这个结构体空间。如果不释放的话,每个运行的进程都会占用系统资源,当运行的进程越来越多时,占用的系统资源也会增多,最后可能会导致linux系统资源占用过高,使linux系统崩溃重启。

  下面我们要讲的僵尸进程就属于这种情况,是一种比较特殊的进程,僵尸进程太多也会导致系统资源占用过高,是系统最终崩溃的问题。


4. 僵尸进程

   子进程结束,但是部分资源(PCB)还未释放,残留在内核中,父进程又尚未回收,于是子进程就变成了僵尸(Zombie)进程。

   僵尸进程产生的原因在于:一般系统是根据PCB进程控制块中的信息来感知进程的运行状态。僵尸进程就属于进程实际上已经结束了,也就是子进程整个用户空间上面所有资源都释放了,但是PCB还未释放,父进程又尚未回收,于是内核将子进程归为僵尸进程了。


5. 程序示例

#include <unistd.h>
#include <stdio.h>
#include <string.h>

int main() {
  printf("before fork\n");
  pid_t pid;
  int n = 5;

  // 父进程生出 5 个子进程
  while(n--) {
    pid = fork();
    if (pid == 0) break;
    else if (pid < 0) {
      perror("fork");
      return 1;
    }   
  }

  // 子进程
  if (pid == 0) {
    printf("child = %d ; father = %d\n", getpid(), getppid());
    return 0;  //子进程已经结束了
  }
    /*
     父进程一直死循环,没有回收子进程
      但是父进程如果一直不结束,那么子进程就一直处于僵尸进程状态
      不能被init进程回收。
    */
  while(1) {
    sleep(3);
    printf("father = %d\n", getpid());
  }
  return 0;
}

编译并执行:

gcc -o zoom_process zoom_process.c
./zoom_process

程序执行结果:
这里写图片描述

  通过上图发现父进程创建了5个子进程后,5个子进程都先于父进程结束,然后父进程一直在循环。



  执行ps -aux命令查看发现创建的5个子进程都处于,defunct表示已死亡,Z+就表示僵尸进程状态。

这里写图片描述

  在这种情况下,如果想要完全结束僵尸进程应该要怎么做呢,有同学可能会想到kill -9命令,但是在执行kill -9命令后再次查看的时候并没有结束子进程,所以使用kill命令是行不通的。

  kill命令本质上是发送了SIGINT信号杀死一个进程,因为子进程本来就已经死亡变成了僵尸进程,再次使用kill -9命令杀死子进程毫无意义。实际上,这也是僵尸进程名字的由来,源于unix系统对电影情节的效仿——无法通过信号杀死僵尸进程,即便是SIGKILL。

  正常来说每一个子进程结束后,父进程需要知道子进程已经死亡然后对子进程进行资源回收,所以在父进程回收前,每个子进程都有一个时间段是处于僵尸进程状态,等待着回收。

  而在第一节讲的孤儿进程产生的原因是父进程比子进程先结束,然后子进程划归到init进程,当子进程结束时由init进程进行回收资源。虽然这种方式能解决问题僵尸进程问题,但不够友好,我们需要一种更加好的办法。

猜你喜欢

转载自blog.csdn.net/qq_35733751/article/details/82454359