目录
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一下: 从上图可以发现,并没有僵尸进程的出现。