Linux僵死(尸)进程(详解)||概念||产生条件||解决方法(wait(waitpid)、信号、两次fork)
本文详细介绍了进程结束的处理机制、僵尸进程以及僵尸进程的四(三)种处理方法,同时涉及到孤儿进程的概念和处理机制。
目录
1.什么是僵尸进程?产生条件
当fork一个新进程的时候,子进程一般会和父进程同时运行。当子进程结束的时候,它与父进程的关联还会保持,直到父进程也正常终止或者wait,子进程才结束。因此,进程中代表子进程的表项不会立即释放。虽然子进程已经不能正常运行,但是它仍然存在于系统之中,因为它的退出码还要保存起来,以备父进程今后的wait调用使用。
这种情况我们称已经结束了但是还不能释放的子进程为僵尸进程。
当某一子进程结束、中断或恢复执行时,内核会发送SIGCHLD信号予其父进程。在默认情况下,父进程会以SIG_IGN函数忽略之。 某一子进程终止执行后,若其父进程未提前调用wait,则内核会持续保留子进程的退出状态等信息,以使父进程可以wait获取之 。而因为在这种情况下,子进程虽已终止,但仍在消耗系统资源,所以其亦称僵尸进程。wait相当于SIGCHLD信号的处理函数中。
首先我们来看一个产生僵尸进程的程序:
#include <stdio.h>
# include<stdlib.h>
# include<string.h>
# include<unistd.h>
# include<assert.h>
# include<sys/wait.h>
# include<sys/types.h>
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<n;++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 <stdio.h>
# include<stdlib.h>
# include<string.h>
# include<unistd.h>
# include<assert.h>
# include<sys/wait.h>
# include<sys/types.h>
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<n;++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 <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/wait.h>
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 <stdio.h>
# include<stdlib.h>
# include<string.h>
# include<unistd.h>
# include<assert.h>
# include<sys/wait.h>
# include<sys/types.h>
#include<signal.h>
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<n;++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 <stdio.h>
# include<stdlib.h>
# include<string.h>
# include<unistd.h>
# include<assert.h>
# include<sys/wait.h>
# include<sys/types.h>
#include<signal.h>
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<n;++i)
{
printf("s=%s\n",s);
sleep(1);
}
printf("process %d is over\n",getpid());
exit(0);
}
执行结果:
可以看到父进程pid为6248,子进程pid为6249,孙子进程oid为6250,当孙子进程结束以后(父进程结束之前)ps一下:
从上图可以发现,并没有僵尸进程的出现。
更多推荐
所有评论(0)