僵尸进程是在类UNIX操作系统中,指已完成执行(通过exit系统调用,或运行时发生致命错误或收到终止信号所致),但在操作系统的进程表中仍然存在其进程控制块,处于"终止状态"的进程。这发生于子进程需要保留表项以允许其父进程读取子进程的退出状态:一旦退出态通过wait系统调用读取,僵尸进程条目就从进程表中删除,称之为"回收"(reaped)。正常情况下,进程直接被其父进程wait并由系统回收。进程长时间保持僵尸状态一般是错误的并导致资源泄漏。

形成原因

一个进程在调用exit命令结束自己的生命的时候,其实它并没有真正的被销毁,而是留下一个称为僵尸进程(Zombie)的数据结构(系统调用exit,它的作用是使进程退出,但也仅仅限于将一个正常的进程变成一个僵尸进程,并不能将其完全销毁)。在Linux进程的状态中,僵尸进程是非常特殊的一种,它已经放弃了几乎所有内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置,记载该进程的退出状态等信息供其他进程收集,除此之外,僵尸进程不再占有任何内存空间。它需要它的父进程来为它收尸,如果他的父进程没安装SIGCHLD信号处理函数调用wait或waitpid()等待子进程结束,又没有显式忽略该信号,那么它就一直保持僵尸状态,如果这时父进程结束了,那么init进程自动会接手这个子进程,为它收尸,它还是能被清除的。但是如果父进程是一个循环,不会结束,那么子进程就会一直保持僵尸状态,这就是为什么系统中有时会有很多的僵尸进程。

特征

unix系统中,一个进程结束了,但是他的父进程没有等待(调用wait / waitpid)他,那么他将变成一个僵尸进程。但是如果该进程的父进程已经先结束了,那么该进程就不会变成僵尸进程,因为每个进程结束的时候,系统都会扫描当前系统中所运行的所有进程,看有没有哪个进程是刚刚结束的这个进程的子进程,如果是的话,就由Init 来接管他,成为他的父进程。

危害

由于子进程的结束和父进程的运行是一个异步过程,即父进程永远无法预测子进程到底什么时候结束。不会。因为UNⅨ提供了一种机制可以保证只要父进程想知道子进程结束时的状态信息,就可以得到。这种机制就是:在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。但是仍然为其保留一定的信息(包括进程号the process ID,退出状态the termination status of the process,运行时间the amount of CPU 时间 taken by the process等)。直到父进程通过wait / waitpid来取时才释放。但这样就导致了问题,如果进程不调用wait / waitpid的话,那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程。此即为僵尸进程的危害,应当避免。

避免方法

方法一

父进程通过wait和waitpid等函数等待子进程结束,这会导致父进程挂起。

方法二

如果父进程很忙,那么可以用signal函数为SIGCHLD安装handler,因为子进程结束后,父进程会收到该信号,可以在handler中调用wait回收。

方法三

如果父进程不关心子进程什么时候结束,那么可以用signal(SIGCHLD,SIG_IGN)通知内核,自己对子进程的结束不感兴趣,那么子进程结束后,内核会回收,并不再给父进程发送信号。

方法四

还有些技巧,就是fork两次,父进程fork一个子进程,然后继续工作,子进程fork一个孙进程后退出,那么孙进程被init接管,孙进程结束后,init会回收。不过子进程的回收还要自己做。

查看方法

查看僵尸进程,利用命令ps,可以看到有标记为Z的进程就是僵尸进程。

假设

在fork()/execve()过程中,假设子进程结束时父进程仍存在,而父进程fork()之前既没安装SIGCHLD信号处理函数调用waitpid()等待子进程结束,又没有显式忽略该信号,则子进程成为僵尸进程,无法正常结束,此时即使是root身份kill -9也不能杀死僵尸进程。

补救办法

杀死僵尸进程的父进程(僵尸进程的父进程必然存在),僵尸进程成为"孤儿进程",过继给1号进程init,init始终会负责清理僵尸进程。

产生过程

一个进程在调用exit命令结束自己的生命的时候,其实它并没有真正的被销毁,而是留下一个称为僵尸进程(Zombie)的数据结构(系统调用exit,它的作用是使进程退出,但也仅仅限于将一个正常的进程变成一个僵尸进程,并不能将其完全销毁)。在Linux进程的状态中,僵尸进程是非常特殊的一种,它已经放弃了几乎所有内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置,记载该进程的退出状态等信息供其他进程收集,除此之外,僵尸进程不再占有任何内存空间。它需要它的父进程来为它收尸,如果他的父进程没安装SIGCHLD信号处理函数调用wait或waitpid()等待子进程结束,又没有显式忽略该信号,那么它就一直保持僵尸状态,如果这时父进程结束了,那么init进程自动会接手这个子进程,为它收尸,它还是能被清除的。但是如果父进程是一个循环,不会结束,那么子进程就会一直保持僵尸状态,这就是为什么系统中有时会有很多的僵尸进程。

进程处理

它需要它的父进程来为它收尸,如果它的父进程没安装SIGCHLD信号处理函数调用wait或waitpid()等待子进程结束,又没有显式忽略该信号,那么它就一直保持僵尸状态;存在的问题:如果父进程是一个循环,不会结束,那么子进程就会一直保持僵尸状态,这就是为什么系统中有时会有很多的僵尸进程,系统的性能可能会受到影响。如果这时父进程结束了,那么init进程会自动接手这个子进程,为它收尸,它还是能被清除的。4、子进程结束后为什么要进入僵尸状态?因为父进程可能要取得子进程的退出状态等信息。5、僵尸状态是每个子进程必经的状态吗?是的。任何一个子进程(init除外)在exit()之后,并非马上就消失掉,而是留下一个称为僵尸进程(Zombie)的数据结构,等待父进程处理。这是每个子进程在结束时都要经过的阶段。如果子进程在exit()之后,父进程没有来得及处理,这时用ps命令就能看到子进程的状态是“Z”。如果父进程能及时处理,可能用ps命令就来不及看到子进程的僵尸状态,但这并不等于子进程不经过僵尸状态。* 如果父进程在子进程结束之前退出,则子进程将由init接管。init将会以父进程的身份对僵尸状态的子进程进行处理。

示例

Example Recall our discussion in Section 8.5 about zombie processes. If we want to write a process so that it forks a child but we don't want to wait for the child to complete and we don't want the child to become a zombie until we terminate,the trick is to call fork twice. The program in Figure 8.8 does this. We call sleep in the second child to ensure that the first child terminates before printing the parent process ID. After a fork,either the parent or the child can continue executing; we never know which will resume execution first. If we didn't put the second child to sleep,and if it resumed execution after the fork before its parent,the parent process ID that it printed would be that of its parent,not process ID 1. Executing the program in Figure 8.8 gives us $ ./a.out $ second child,parent pid = 1 Note that the shell prints its prompt when the original process terminates,which is before the second child prints its parent process ID. Figure 8.8. Avoid zombie processes by calling fork twice\n#include "apue.h"\n#include \u003csys/wait.h\u003e\nint main(void) ...{\npid_t pid;\nif ((pid = fork()) \u003c 0)\n{\nerr_sys("fork error");\n} else if (pid == 0) { /* first child */\nif ((pid = fork()) \u003c 0)\nerr_sys("fork error");\nelse if (pid \u003e 0)\nexit(0); /* parent from second fork == first child */\n/ * We're the second child; our parent becomes init as soonas our real parent calls exit() in the statement above. Here's where we'd continue executing,knowing that whenwe're done,init will reap our status.*/\nsleep⑵;\nprintf("second child,parent pid = %d ",getppid());\nexit(0);\n}\nif (waitpid(pid,NULL,0) != pid) /* wait for first child */\nerr_sys("waitpid error");\n/ * We're the parent (the original process); we continue executing,knowing that we're not the parent of the second child.*/\nexit(0);\n}

小结

子进程成为 defunct 直到父进程wait(),除非父进程忽略了 SIGCLD。更进一步,父进程没有 wait() 就消亡(仍假设父进程没有忽略 SIGCLD)的子进程(活动的或者 defunct)成为 init 的子进程,init 用重手法处理它们。

参考资料

Linux系统僵尸进程详解.博客园.2024-09-18

Linux系统下的僵尸进程(概念、产生、危害、避免).CSDN博客.2024-09-18

僵尸进程:Linux系统中的隐秘威胁|如何查看并清理僵尸进程.人言兑.2024-09-18