博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
19-孤儿进程与僵尸进程
阅读量:2044 次
发布时间:2019-04-28

本文共 3099 字,大约阅读时间需要 10 分钟。

1. 孤儿进程

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

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

2. 示例程序

#include 
#include
#include
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 
#include
#include
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进程进行回收资源。虽然这种方式能解决问题僵尸进程问题,但不够友好,我们需要一种更加好的办法。

你可能感兴趣的文章
Leetcode C++ 《第202场周赛》
查看>>
云原生 第十二章 可观测性:监控与日志
查看>>
Leetcode C++ 《第203场周赛》
查看>>
云原生 第十三章 Kubernetes网络概念及策略控制
查看>>
《redis设计与实现》 第一部分:数据结构与对象 || 读书笔记
查看>>
《redis设计与实现》 第二部分(第9-11章):单机数据库的实现
查看>>
Leetcode C++《热题 Hot 100-70》23.合并K个升序链表
查看>>
《redis设计与实现》第二部分 (第12章:事件)
查看>>
《redis设计与实现》第二部分 (第13章 客户端)
查看>>
《redis设计与实现》第二部分 (第14章 服务器)
查看>>
《redis设计与实现》第四部分 (第18章 发布与订阅)
查看>>
《redis设计与实现》第四部分 (第19章 事务)
查看>>
《redis设计与实现》第四部分 (第20章 Lua脚本)
查看>>
《redis设计与实现》第四部分 (第21章 排序)
查看>>
《redis设计与实现》第四部分 (第22章 二进制位数组)
查看>>
《redis设计与实现》第四部分 (第23章 慢查询日志)
查看>>
《redis设计与实现》第四部分 (第24章 监视器)
查看>>
《redis设计与实现》第三部分 (第15章 复制)
查看>>
《redis设计与实现》第三部分 (第16章 哨兵)
查看>>
故宫学系列之紫禁城:从皇宫到博物院
查看>>