引言:
在Linux/Unix系统编程时,会经常遇到僵尸进程(Zombie)这个概念。类似电影中的僵尸一样,僵尸进程指的是那些已经运行结束、却仍然占着一些内存资源,没有被彻底清理的进程。
一个进程结束之后,内核会释放该进程的资源,包括打开的文件、占用的内存的高等,此后它将成为一个僵尸进程,在它的父进程没有wait
/waipid
它之前,它将一直保持这个状态。它仍然保留一定的信息(包括PID、退出状态、运行时间等)。僵尸进程存在的意义是让父进程获取它的退出信息。
1.僵尸进程产生的原因
我们知道在Linux/Unix中每个进程(除init
)都是通过其父进程创建的,然后子进程再创建新的子进程,如此周而复始。子进程的结束和父进程的运行是一个异步过程,也就是说,父进程永远无法知道子进程何时结束。当一个进程结束后,在没有被wait/waitpid
的情况下,它将成为一个僵尸进程,在这种状态下,它通过两种途径被彻底杀死。1.父进程调用wait()
/waitpid()
获取它的退出信息后,它被彻底杀死。 2.它的父进程在结束,它作为一个孤儿进程被init
进程收养(即init
成为它的父进程),然后init
进程再将它彻底杀死。不管通过哪种途径,一个进程要彻底从你的系统中被清除,都需要其父进程等待(wait
/waitpid
)它。
如上图中有个状态为Z
的进程,表明该进程是僵尸进程。
2.模拟创建一个僵尸进程
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
pid_t pid;
pid = fork();//创建一个子进程
if(pid < 0)
{
perror("fork()");
exit(0);
}
//通过返回的PID做不同的事情
if(0 == pid)
{
printf("I am a child process.\n");
sleep(2);
}
else
{
printf("I am a father process.\n");
sleep(5);
}
//输出进程的信息
system("ps -o pid,ppid,stat,tty,command | grep zombie");
printf("exit!\n");
return 0;
}
fork()
函数通过系统调用创建一个与原来进程几乎完全相同的进程。调用fork()之后,操作系统会复制一个全新的task_struct
结构体,这个结构体除了ID
号不一样外,其余的都完全一样。这意味着,两个进程的内存空间也是映射到相同的地址(也就是说这两个进程共享同一片内存空间)。fork()
的进程采用copy on write
的机制,当某一个进程试图去修改其共享的数据时,操作系统会产生”缺页中断”为该进程分配新的内存空间。
pid_t fork()
函数与我们平常的函数不同之处在于,它仅被调用一次却能返回两个值,它有三种返回值:
1. 在父进程中fork()
返回子进程的PID
。
2. 在子进程中fork()
返回0。
3. 如果出现错误,则返回一个负值。
在fork()
函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork()
函数返回0,在父进程中,
fork()
返回新创建子进程的进程ID。我们可以通过fork()
返回的值来判断当前进程是子进程还是父进程。
所以在调用fork()
函数之前,只有一个进程执行这段代码,在这个函数之后,不出意外的话就有两个进程在同时执行了。
该代码的执行结果如下:
我们先fork()
一个子进程,然后父进程和子进程同时执行,状态都为sleep
,紧接着让父进程睡眠5s后,此时子进程已经执行结束,再次查看他们的状态,发现子进程状态已经成为Z
了,即此时子进程是一个僵尸进程。
3.僵尸进程有什么危害
僵尸进程的最显著特点是“占着茅坑不拉屎”,显然,它主要的危害就是占用着系统资源,却什么也不干,有点像我们在c中常犯的错误——内存泄漏。操作系统本身的进程当然不会犯这种错误了,如果由于我们人为的原因,产生大量的僵尸进程,而且并没有处理它们,那么这会严重影响操作系统的性能。
4.如何处理僵尸进程
我觉得更准确的来说,应该是如何处理僵尸进程。在Linux/Unix系统中沦为僵尸进程是一个进程发展的必经阶段,我们无法避免,就如同动物在死亡时其尸体不会直接凭空消失一样,我们只能想办法在一个进程死亡后,迅速给它收尸,不让它长时间的“占着茅坑”。
我们可以通过下面几个方法处理僵尸进程:
- 父进程调用
wait()
/waitpid()
函数,获取完退出信息后,子进程被彻底清理。- 让该进程成为孤儿进程,
init
收养它,然后交给init
处理它。- 调用fork()两次。
对于方法1
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t pid;
pid = fork();//创建一个子进程
if(pid < 0)
{
perror("fork()");
exit(0);
}
//通过返回的PID做不同的事情
if(0 == pid)
{
printf("I am a child process.\n");
sleep(2);
}
else
{
printf("I am a father process.\n");
sleep(5);
wait(0); //重点!!!父进程wait子进程。
}
//输出进程的信息
system("ps -o pid,ppid,stat,tty,command | grep zombie");
printf("exit!\n");
return 0;
}
函数:pid_t wait (int * status);
说明:wait()会暂时停止目前进程的执行, 直到有信号来到或子进程结束. 如果在调用wait()时子进程已经结束, 则wait()会立即返回子进程结束状态值. 子进程的结束状态值会由参数status 返回, 而子进程的进程识别码也会一起返回. 如果不在意结束状态值, 则参数 status 可以设成NULL. 子进程的结束状态值请参考waitpid().
当子进程结束后,我们在父进程成中调用wait()
函数,处理子进程遗留下的问题。
它的运行结果如下:
显然此时子进程已经被彻底杀死。
对于第二种情况,我们修改代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t pid;
pid = fork();//创建一个子进程
if(pid < 0)
{
perror("fork()");
exit(0);
}
//通过返回的PID做不同的事情
if(0 == pid)
{
printf("I am a child process.\n");
sleep(5);
}
else
{
printf("I am a father process.\n");
}
//输出进程的信息
system("ps -o pid,ppid,stat,tty,command | grep zombie");
printf("exit!\n");
return 0;
}
先让父进程退出,然后子进程将作为孤儿进程被init
(ID为1的进程)收养。
运行结果如下:
我们发现父进程比子进程早退出后,子进程已经被init
进程收养(它的PPID为1),那么随后子进程的收尾工作将交给init
进程完成。
第三种fork()
两次的意义在于,我们在父进程的孙子进程上工作,当工作完毕之后,将父进程的子进程杀死,这样的话,孙子进程就作为孤儿进程被init
收养,与2的原理类似。
参考内容:
- fork(). http://blog.csdn.net/hyfcomeon/article/details/906023
- 缺页中断. http://baike.baidu.com/item/%E7%BC%BA%E9%A1%B5%E4%B8%AD%E6%96%AD
- wait(). http://c.biancheng.net/cpp/html/289.html
- 孤儿进程:http://baike.baidu.com/link?url=1gwzcjNRTO0OSiOhmUhJzDl_6Oxkk040fpVP3R29Re5VsuyW9CvArYZj85D78R6B-xGzY1HtRBbICAl5RqMgSqnxKBza_ytnHAmG5-D175hGiBtouKCopQdoidxUIIIR
【作者:果冻 http://blog.csdn.net/jelly_9】