避免僵尸进程的方法
何为僵尸进程
- 如果父进程先退出,子进程自动被 init 进程收养,不会产生僵尸进程。
- 如果子进程先退出。父进程 wait() 处理(即父进程调用wait/waitpid方法来处理),则僵尸进程会被父进程清理;如果父进程不用 wait() 处理,则僵尸进程会在父进程退出之前一直存在。当然,父进程退出后,僵尸子进程会被 init 收养,init 进程会自动调用 wait() 处理。但是对于处理网络请求的服务器进程来说,父进程可能会一直存在,子进程处理完任务就退出,这种情况下会产生很多僵尸进程,这种场景就需要对僵尸进程的处理提高警惕了。
避免产生僵尸进程的方法(每种方法都有自己适用的场景)
- fock twice。使用场景:一个进程要创建一个进程,两个进程同时处理任务,谁也不耽误谁。如果直接用子进程充当第二个进程的角色,那么问题是这样的:如果父进程处理时间长,子进程处理时间短,那么如果父进程不 wait() 处理的话,子进程就会成为僵尸进程,但如果父进程 wait() 子进程的话,父进程就会阻塞,所有有个方法就是让自己尽快推出,任务让子进程的子进程来处理。
/*
* Avoid zombie processes by calling fork twice.
* APUE-2e 程序清单8-5
*/
#include <unistd.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
pid_t pid;
if( (pid = fork()) < 0 )
{
printf("fork error.\n");
exit(-1);
}
else if(pid == 0) /* first child */
{
if( (pid = fork()) < 0 )
{
printf("fork error.\n");
exit(-1);
}
else if(pid > 0)
{
exit(0);
}
/* We're the second child; our parent becomes init as soon as our real parent exits. */
printf("second child, parent pid = %d\n", getppid());
/* ---------------handle tasks--------------- */
exit(0);
}
if(waitpid(pid, NULL, 0) != pid) /* wait for first child */
{
printf("waitpid error.\n");
exit(1);
}
printf("parent, first child pid = %d\n", pid);
/* ---------------handle tasks--------------- */
exit(0);
}
- 父进程调用wait()方法,但是会使父进程阻塞
- signal(sigchld,sig_ign),并不是所有系统都兼容。通过signal(SIGCHLD, SIG_IGN)通知内核对子进程的结束不关心,由内核回收,即可让内核把僵尸子进程转交给init进程去处理,省去了大量僵尸进程占用系统资源。
- sigaction+sa_nocldwait,并不是所有系统兼容
- 在signal handler函数中调用 waitpid (下面的例子能说明用 waitpid 而不用 wait的原因:我们必须指定WOHOANG option——这告诉waitpid不要阻止是否有正在运行的子节点),这样父进程不用阻塞。适用场景: one-request-one-process 的网络服务器程序
- 让僵尸进程变成孤儿进程,由init回收,就是让父亲先死
void sig_chld(int signo)
{
pid_t pid;
int stat;
while ( (pid = waitpid(-1, &stat, WNOHANG)) > 0 )
{
printf("child %d terminated\n", pid);
}
return;
}
建立信号处理程序并从该处理程序调用wait不足以防止僵尸。 问题是如果在执行信号处理程序之前生成五个信号,则信号处理程序只执行一次,因为Unix信号通常不排队(这种情况下只会产生一次“SIGCHLD”信号)。在这种情况下,信号处理程序执行一次,留下四个僵尸。故正确的解决方案是调用waitpid而不是wait,并且我们必须指定WOHOANGoption:这告诉waitpid不要阻止是否有正在运行的子节点还没有终止。
wait与waitpid的区别
pid_t wait(int *status);
pid_t waitpd(pid_t pid, int *status, int options);
-
wait
进程一旦调用了wait,就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。
参数status用来保存被收集进程退出时的一些状态,它是一个指向int类型的指针。但如果我们对这个子进程是如何死掉的毫不在意,只想把这个僵尸进程消灭掉,(事实上绝大多数情况下,我们都会这样想),我们就可以设定这个参数为NULL,就象下面这样:pid = wait(NULL);
返回值:
- 如果成功,wait会返回被收集的子进程的进程ID
-
如果调用进程没有子进程,调用就会失败,此时wait返回-1,同时errno被置为ECHILD。
-
waitpid
从本质上讲,系统调用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中只支持WNOHANG和WUNTRACED两个选项,这是两个常数,可以用"|"运算符把它们连接起来使用,如果我们不想使用它们,也可以把options设为0。
返回值——waitpid的返回值比wait稍微复杂一些,一共有3种情况:
- 当正常返回的时候,waitpid返回收集到的子进程的进程ID;
- 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
- 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;当pid所指示的子进程不存在,或此进程存在,但不是调用进程的子进程,waitpid就会出错返回,这时errno被设置为ECHILD
wait与waitpid的区别:waitpid提供了wait不能实现的功能
- waitpid等待特定的子进程, 而wait则返回任一终止状态的子进程;
- waitpid提供了一个wait的非阻塞版本;
- waitpid支持作业控制(以WUNTRACED选项). 用于检查wait和waitpid两个函数返回终止状态的宏: 这两个函数返回的子进程状态都保存在status指针中, 用以下3个宏可以检查该状态:
- WIFEXITED(status): 若为正常终止, 则为真. 此时可执行 WEXITSTATUS(status): 取子进程传送给exit或_exit参数的低8位.
- WIFSIGNALED(status): 若为异常终止, 则为真.此时可执行 WTERMSIG(status): 取使子进程终止的信号编号
- WIFSTOPPED(status): 若为当前暂停子进程, 则为真. 此时可执行 WSTOPSIG(status): 取使子进程暂停的信号编号