本文从一个Golang案例学习僵尸进程:skull:的主要内容
一、案例
main进程创建一个bash子进程,子进程存活10s退出,main进程睡20s退出
1 | package main |
启动一个子进程,os包自带StartProcess创建子进程 API
1 | func StartProcess(name string, argv []string, attr *ProcAttr) (*Process, error) { |
我们在linux环境下运行该代码
二、结果分析
从上面的打印看出:主进程PID:23380,子进程PID:23385;子进程10s退出,但此时主进程是存活状态,所以该子进程是僵尸进程状态;主进程睡眠20s后清扫僵尸子进程(子进程彻底退出)
同时我们查看进程的相关信息,打印进程的状态、父进程、子进程以及运行命令。分析如下:
1、主进程与子进程同时存活
23380[主进程]状态是S(TASK_INTERRUPTIBLE) 可中断的睡眠状态,因为在睡眠,等到时钟信号量
23385[子进程]状态是S(TASK_INTERRUPTIBLE) 可中断的睡眠状态,因为在睡眠,等到时钟信号量
1 | [centos@wunaichi ~]$ ps -A ostat,ppid,pid,cmd|grep 23380 |
2、子进程退出
10s后子进程退出,再查看进程相关信息
23380[主进程]状态是S(TASK_INTERRUPTIBLE) 可中断的睡眠状态,因为在睡眠,等到时钟信号量
23385[子进程]状态是Z(TASK_DEAD - EXIT_ZOMBIE)退出状态,进程成为僵尸进程,描述符保存
1 | [centos@wunaichi ~]$ ps -A ostat,ppid,pid,cmd|grep 23380 |
查看机器所以的僵尸进程(查看机器所有的僵尸进程)
1 | [centos@wunaichi ~]$ ps aux |grep Z |
3、清扫僵尸进程
主进程23380和子进程23385全部退出
1 | [centos@wunaichi ~]$ ps -A ostat,ppid,pid,cmd|grep 23380 |
主进程清扫僵尸子进程,查看僵尸子进程已经不存在
1 | [centos@wunaichi ~]$ ps aux |grep Z |
三、僵尸进程
到这里,我想大家对僵尸进程已经有一定的概念。一个进程创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程,也就是僵尸进程。
在这个退出过程中,僵尸进程占有的所有资源将被回收,除了进程描述符、进程的退出码、以及一些统计信息以外[这些信息将存储在一个结构体中task_struct]。于是该进程几乎剩下这么个空壳,故称为僵尸。之所以还保留一些僵尸进程的基础信息是因为父进程很可能会关心这些信息。当然,内核也可以将这些信息保存在别的地方,而将task_struct结构释放掉,以节省一些空间。但是使用task_struct结构更为方便,因为在内核中已经建立了从pid到task_struct查找关系,还有进程间的父子关系。
另外一种情况是如果父进程在子进程结束之前退出,则子进程将由init接管。init将会以父进程的身份对僵尸状态的子进程进行处理。所以任何进程都会经过僵尸的这个状态
但是也会产生一些问题:如果进程不调用wait / waitpid的话, 那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程。
所以如果产生了大量的僵尸进程,我们可以将其父进程kill,这样的话僵尸进程都会交给init进程处理。当然,不是所有的进程都必须kill,这要根据你的业务而定了。
下一节我们来了解孤儿进程:stuck_out_tongue_closed_eyes: