基本概念:
我们知道在unix/linux中,正常情况下,子进程是通过父进程创建的,子进程再创建新的进程。子进程的结束和父进程的运行是一个异步过程,即父进程永远无法预测子进程 到底什么时候结束。 当一个 进程完成它的工作终止之后,它的父进程需要调用wait()或者waitpid()系统调用取得子进程的终止状态。
孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。
僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。
重要概念:
一个进程在调用exit命令结束自己生命的时候,其实它并没有真正的被销毁,而是留下一个称为僵尸进程(Zombie)的数据结构。系统调用exit的作用是使进程退出,但也仅仅限于将一个正常的进程变成一个僵尸进程,并不能将其完全销毁。
在Linux进程的状态中,僵尸进程是非常特殊的一种,它已经放弃了几乎所有的空间,没有任何可执行代码,也不能被调度,仅仅在进程的列表中保留一个位置,记载该进程的退出状态等信息供其他进程收集。除此之外,僵尸进程不再占有任何内存空间。
它需要他的父进程来为他收尸,如果他的父进程没有安装SIGCHLD信息处理函数调用wait或waitpid等待子进程的结束,又没有显示忽略该信息,那么它就一直保持僵尸状态,如果这时候父进程结束了,那么init进程自动会接手这个子进程,为它收尸,它还是能被清除的。但是如果父进程是一个循环,不会结束,那么子进程交一直保持僵尸状态,这就是为什么系统中有时会有很多僵尸进程。
问题及危害:
unix提供了一种机制可以保证只要父进程想知道子进程结束时的状态信息, 就可以得到。这种机制就是: 在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。 但是仍然为其保留一定的信息(包括进程号the process ID,退出状态the termination status of the process,运行时间the amount of CPU time taken by the process等)。直到父进程通过wait / waitpid来取时才释放。 但这样就导致了问题,如果进程不调用wait / waitpid的话, 那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程. 此即为僵尸进程的危害,应当避免。
僵尸进程危害场景:
例如有个进程,它定期的产 生一个子进程,这个子进程需要做的事情很少,做完它该做的事情之后就退出了,因此这个子进程的生命周期很短,但是,父进程只管生成新的子进程,至于子进程 退出之后的事情,则一概不闻不问,这样,系统运行上一段时间之后,系统中就会存在很多的僵死进程,倘若用ps命令查看的话,就会看到很多状态为Z的进程。 严格地来说,僵死进程并不是问题的根源,罪魁祸首是产生出大量僵死进程的那个父进程。因此,当我们寻求如何消灭系统中大量的僵死进程时,答案就是把产生大 量僵死进程的那个元凶枪毙掉(也就是通过kill发送SIGTERM或者SIGKILL信号啦)。枪毙了元凶进程之后,它产生的僵死进程就变成了孤儿进 程,这些孤儿进程会被init进程接管,init进程会wait()这些孤儿进程,释放它们占用的系统进程表中的资源,这样,这些已经僵死的孤儿进程 就能瞑目而去了。
四种僵尸进程避免方式:
1.wait和waitpid函数
2.signal安装处理函数(交给内核处理)
3.signal忽略SIGCHLD信号(交给内核处理)
4.fork两次
书231页
wait和waitpid:
1.
wait系统调用在Linux函数库中的原型是:
pid_t wait (int *status )
进程一旦调用了wait,就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。(wait第一个结束的进程)
参数status用来保存被收集进程退出时的一些状态,它是一个指向int类型的指针。但如果我们对这个子进程是如何死掉的毫不在意,只想把这个僵尸进程消灭掉,(事实上绝大多数情况下,我们都会这样想),我们就可以设定这个参数为NULL,就象下面这样:pid = wait(NULL);
如果成功,wait会返回被收集的子进程的进程ID,如果调用进程没有子进程,调用就会失败,此时wait返回-1,同时errno被置为ECHILD。
2.
waitpid系统调用在Linux函数库中的原型是:
#include <sys/types.h> /* 提供类型pid_t的定义 */
#include <sys/wait.h>
pid_t waitpid(pid_t pid,int *status,int options)
从本质上讲,系统调用waitpid和wait的作用是完全相同的,但waitpid多出了两个可由用户控制的参数pid和options,从而为我们编程提供了另一种更灵活的方式。下面我们就来详细介绍一下这两个参数:
pid:从参数的名字pid和类型pid_t中就可以看出,这里需要的是一个进程ID。但当pid取不同的值时,在这里有不同的意义。
pid>0时,只等待进程ID等于pid的子进程,不管其它已经有多少子进程运行结束退出了,只要指定的子进程还没有结束,waitpid就会一直等下去。
pid=-1时,等待任何一个子进程退出,没有任何限制,此时waitpid和wait的作用一模一样。
pid=0时,等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid不会对它做任何理睬。
pid<-1时,等待一个指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值。
options:options提供了一些额外的选项来控制waitpid,目前在Linux中只支持0(阻塞),WNOHANG(非阻塞)和WUNTRACED两个选项,这是两个常数,可以用”|”运算符把它们连接起来使用。
比如:
ret = waitpid(-1,NULL,WNOHANG | WUNTRACED);
使用了WNOHANG参数调用waitpid,即使没有子进程退出,它也会立即返回,不会像wait那样永远等下去。
而WUNTRACED参数,由于涉及到一些跟踪调试方面的知识,加之极少用到,这里就不多费笔墨了,有兴趣的读者可以自行查阅相关材料。
看到这里,聪明的读者可能已经看出端倪了:wait不就是经过包装的waitpid吗?没错,察看<内核源码目录>/include/unistd.h文件349-352行就会发现以下
例子:
/******************************************
waitpid函数控制阻塞非阻塞wait模式
******************************************/
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
/***********************
错误处理函数
***********************/
void die(const char *msg)
{
perror(msg);
exit(1 );
}
/**************************************
孙进程执行函数
***************************************/
void child2_do()
{
printf("In child2: execute 'date'\n" );
sleep(5 );
if (execlp("date" , "date" , NULL) < 0 )
{
perror("child2 execlp" );
}
}
/**************************************
子进程执行函数
**************************************/
void child1_do(pid_t child2, char *argv)
{
pid_t pw;
do { /**************************
控制waitpid模式,等孙进程
***************************/
if (*argv == '1' )
{
pw = waitpid(child2, NULL, 0 );
}
else
{
pw = waitpid(child2, NULL, WNOHANG);
}
if (pw == 0 )
{
printf("In child1 process:\nThe child2 process has not exited!\n" );
sleep(1 );
}
}while (pw == 0 );
if (pw == child2)
{
printf("Get child2 %d.\n" , pw);
sleep(5 );
if (execlp("pwd" , "pwd" , NULL) < 0 )
{
perror("child1 execlp" );
}
}
else
{
printf("error occured!\n" );
}
}
void father_do(pid_t child1, char *argv)
{
pid_t pw;
do {
if (*argv == '1' ) {
pw = waitpid(child1, NULL, 0 );
}
else {
pw = waitpid(child1, NULL, WNOHANG);
}
if (pw == 0 ) {
printf("In father process:\nThe child1 process has not exited.\n" );
sleep(1 );
}
}while (pw == 0 );
if (pw == child1) {
printf("Get child1 %d.\n" , pw);
if (execlp("ls" , "ls" , "-l" , NULL) < 0 ) {
perror("father execlp" );
}
}
else {
printf("error occured!\n" );
}
}
int main(int argc, char *argv[])
{
pid_t child1, child2;
if (argc < 3 ) {
printf("Usage: waitpid [0 1] [0 1]\n" );
exit(1 );
}
child1 = fork();
if (child1 < 0 ) {
die("child1 fork" );
}
else if (child1 == 0 ) {
child2 = fork();
if (child2 < 0 ) {
die("child2 fork" );
}
else if (child2 == 0 ) {
child2_do();
}
else {
child1_do(child2, argv[1 ]);
}
}
else {
father_do(child1, argv[2 ]);
}
return 0 ;
}