目录

1.什么是僵尸进程?产生条件

2.僵尸进程的解决方法

1.wait

2.waitpid

3.信号

4.两次fork

1.什么是僵尸进程?产生条件

当fork一个新进程的时候,子进程一般会和父进程同时运行。当子进程结束的时候,它与父进程的关联还会保持,直到父进程也正常终止或者wait,子进程才结束。因此,进程中代表子进程的表项不会立即释放。虽然子进程已经不能正常运行,但是它仍然存在于系统之中,因为它的退出码还要保存起来,以备父进程今后的wait调用使用。

这种情况我们称已经结束了但是还不能释放的子进程为僵尸进程。

当某一子进程结束、中断或恢复执行时,内核会发送SIGCHLD信号予其父进程。在默认情况下,父进程会以SIG_IGN函数忽略之。 某一子进程终止执行后,若其父进程未提前调用wait,则内核会持续保留子进程的退出状态等信息,以使父进程可以wait获取之 。而因为在这种情况下,子进程虽已终止,但仍在消耗系统资源,所以其亦称僵尸进程。wait相当于SIGCHLD信号的处理函数中。

首先我们来看一个产生僵尸进程的程序:

#include

# include

# include

# include

# include

# include

# include

int main()

{

int n = 0;

char *s = NULL;

pid_t pid = fork();

assert(pid>=0);

if(pid==0)

{

n=5;

s="child";

}

else

{

n=15;

s="parent";

}

int i=0;

for(;i

{

printf("s=%s\n",s);

sleep(1);

}

if(pid)

printf("parent end\n");

else

printf("child end\n");

exit(0);

}

上述程序子进程会先于父进程结束,当打印出child end的时候,子进程就已经不在执行了,我们通过ps查看它的状态:

可以看到进程4962为子进程,此时标识为defunct,标识它是一个僵尸进程。

2.僵尸进程的解决方法

1.wait

wait函数的形式为:

pid_t wait(int *stat_loc)

返回值为子进程的PID,如果stat_loc不为空,那么内核保存的子进程的状态信息将会写入它所指向的位置。

注意:wait函数会使父进程在函数调用处挂起(阻塞),当子进程结束之后,父进程才会继续执行wait之后的代码。

wait函数将父子进程分离开来,让本来并发执行的父子进程变成了异步执行。

#include

# include

# include

# include

# include

# include

# include

int main()

{

int n = 0;

char *s = NULL;

pid_t pid = fork();

assert(pid>=0);

if(pid==0)

{

n=5;

s="child";

}

else

{

n=15;

s="parent";

}

wait((int*)0);

int i=0;

for(;i

{

printf("s=%s\n",s);

sleep(1);

}

if(pid)

printf("parent end\n");

else

printf("child end\n");

exit(0);

}

执行结果如下,可以发现,在子进程结束之后,父进程才从wait处继续执行。

2.waitpid

首先看以下waitpid函数的形式:

pid-t waitpid(pid_t pid,int *stat_loc,int options);

返回值为清理掉的子进程的pid,如果为-1,标识没有子进程被清理。

stat_loc的含义和wait中一样。

pid的参数含义如下:

>0 回收指定ID的子进程

-1 回收任意子进程(相当于wait)

0 回收和当前调用waitpid一个组的所有子进程

< -1 回收指定进程组内的任意子进程

options选项经常使用的主要有以下两个:如果不想使用options,可以设置为0

WNOHANG

如果pid指定的子进程没有结束,则waitpid()函数立即返回0,而不是阻塞在这个函数上等待;如果结束了,则返回该子进程的进程号。

WUNTRACED

如果子进程进入暂停状态,则马上返回

waitpid函数相比于wait函数,除了能等待指定的进程,还能再等待指定进程的同时通过设置options选项为WNOHANG让父进程不会在waitpid函数中阻塞。

因此如果想让父进程周期性地检查某个特定的子进程是否结束,就可以使用waitpid。

#include

#include

#include

#include

int main(void)

{

pid_t pid, wpid;

pid = fork();

if(pid == -1){

perror("fork error");

exit(1);

} else if(pid == 0){ //son

printf("I'm process child, pid = %d\n", getpid());

sleep(5);

exit(0);

} else { //parent

do {

wpid = waitpid(pid, NULL, WNOHANG);

//wpid = wait(NULL);

printf("---wpid = %d\n", wpid);

if(wpid == 0){

printf("NO child exited\n");

sleep(1);

}

} while (wpid == 0); //子进程不可回收

if(wpid == pid) //回收了指定子进程

printf("I'm parent, I catched child process,"

"pid = %d\n", wpid);

return 0;

}

上述程序,在子进程结束之前,父进程一直在do while语句里面执行,并没有挂起。程序执行结果如下如所示:

3.信号

信号量说一种软中断机制。我们知道,任何一个子进程结束,内核都会给父进程发送一个SIGCHLD信号,如果我们在父进程定义一个信号处理函数,在函数中进行wait操作或者waitpid操作,当任何一个子进程结束的时候,都会触发signal函数,这样我们就可以通过signal+wait的方法,更加灵活的处理僵尸进程了。

如下所示:

#include

# include

# include

# include

# include

# include

# include

#include

void childHandle(int sig)

{

pid_t pid;

if(sig==SIGCHILD)

pid = wait(NULL);

printf("child process %d is over\n");

}

int main()

{

signal(SIGCHLD,childHandle);

int n = 0;

char *s = NULL;

pid_t pid = fork();

assert(pid>=0);

if(pid==0)

{

n=5;

s="child";

}

else

{

n=15;

s="parent";

}

int i=0;

for(;i

{

printf("s=%s\n",s);

sleep(1);

}

if(pid)

printf("parent end\n");

else

printf("child end\n");

exit(0);

}

程序打印结果如下:

可以看到在子进程结束之后,signal函数捕捉了SIGCHLD信号,并且对信号进行了childHandle处理操作。当然,处理函数中的wait方法的地方也可以使用waitpid的方法。

4.两次fork

首先我们了解一下孤儿进程,孤儿进程正好和僵尸进程相反,孤儿进程是父进程先于子进程结束,这时,子进程就会成为一个孤儿进程,孤儿进程会立即被init收养,完成善后操作,期间不需要人工的参与。

因此两次fork解决僵尸进程的思路如下:

当父进程fork以后,产生一个子进程,在子进程中再进行fork,会产生一个孙子进程,我们可以让子进程立即退出,然后在父进程里面wait保证子进程不会成为僵尸进程,然后我们的孙子进程失去了父亲,变成了一个孤儿进程,被init收养,init会处理孙子进程的善后操作。我们要执行的代码就可以放进孙子进程里面了。

按理来说,这种方法比上述三种方法都要繁琐,但是这种方法在服务器中经常会被用到。这是因为前几种方法在子进程结束的时候都会对父进程造成消耗。而两次fork的方法,只有在连接的时候造成消耗(子进程立即退出,和连接几乎同时发生),而后面的孙子进程完全脱离了服务器进程,不会再对服务器有任何负担。

另外:子进程的善后操作的处理方式为以上三种操作,甚至也可以两次fork,只要你不嫌麻烦。

代码实现:

#include

# include

# include

# include

# include

# include

# include

#include

void child_handle(int sig)

{

pid_t pid;

if(sig==SIGCHLD)

pid = wait(NULL);

printf("child process %d is over\n",pid);

}

int main()

{

signal(SIGCHLD,child_handle);

int n = 0;

char *s = NULL;

pid_t pid = fork();

assert(pid>=0);

if(pid==0)

{

pid_t son = fork();

if(son<0)

perror("fork error");

if(son>0)

{

exit(0);

}

n=5;

s="son";

}

else

{

n=15;

s="parent";

}

int i=0;

for(;i

{

printf("s=%s\n",s);

sleep(1);

}

printf("process %d is over\n",getpid());

exit(0);

}

执行结果:

可以看到父进程pid为6248,子进程pid为6249,孙子进程oid为6250,当孙子进程结束以后(父进程结束之前)ps一下:

从上图可以发现,并没有僵尸进程的出现。