引言: 在Linux/Unix系统编程时,会经常遇到僵尸进程(Zombie)这个概念。类似电影中的僵尸一样,僵尸进程指的是那些已经运行结束、却仍然占着一些内存资源,没有被彻底清理的进程。 一个进程结束之后,内核会释放该进程的资源,包括打开的文件、占用的内存的高等,此后它将成为一个僵尸进程,在它的父进程没有wait/waipid它之前,它将一直保持这个状态。它仍然保留一定的信息(包括PID、退出状态、运行时间等)。僵尸进程存在的意义是让父进程获取它的退出信息。
我们知道在Linux/Unix中每个进程(除init)都是通过其父进程创建的,然后子进程再创建新的子进程,如此周而复始。子进程的结束和父进程的运行是一个异步过程,也就是说,父进程永远无法知道子进程何时结束。当一个进程结束后,在没有被wait/waitpid的情况下,它将成为一个僵尸进程,在这种状态下,它通过两种途径被彻底杀死。1.父进程调用wait()/waitpid()获取它的退出信息后,它被彻底杀死。 2.它的父进程在结束,它作为一个孤儿进程被init进程收养(即init成为它的父进程),然后init进程再将它彻底杀死。不管通过哪种途径,一个进程要彻底从你的系统中被清除,都需要其父进程等待(wait/waitpid)它。 如上图中有个状态为Z的进程,表明该进程是僵尸进程。
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了,即此时子进程是一个僵尸进程。
僵尸进程的最显著特点是“占着茅坑不拉屎”,显然,它主要的危害就是占用着系统资源,却什么也不干,有点像我们在c中常犯的错误——内存泄漏。操作系统本身的进程当然不会犯这种错误了,如果由于我们人为的原因,产生大量的僵尸进程,而且并没有处理它们,那么这会严重影响操作系统的性能。
我觉得更准确的来说,应该是如何处理僵尸进程。在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/缺页中断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】