- 什么是僵尸进程?
首先内核会释放终止进程(调用了exit系统调用)所使用的所有存储区,关闭所有打开的文件等,但内核为每一个终止子进程保存了一定量的信息。这些信息至少包括进程ID,进程的终止状态,以及该进程使用的CPU时间,所以当终止子进程的父进程调用wait或waitpid时就可以得到这些信息。
而僵尸进程就是指:一个进程执行了exit系统调用退出,而其父进程并没有为它收尸(调用wait或waitpid来获得它的结束状态)的进程。
任何一个子进程(init除外)在exit后并非马上就消失,而是留下一个称外僵尸进程的数据结构,等待父进程处理。这是每个子进程都必需经历的阶段。另外子进程退出的时候会向其父进程发送一个SIGCHLD信号。
- 僵尸进程的目的?
设置僵尸状态的目的是维护子进程的信息,以便父进程在以后某个时候获取。这些信息至少包括进程ID,进程的终止状态,以及该进程使用的CPU时间,所以当终止子进程的父进程调用wait或waitpid时就可以得到这些信息。如果一个进程终止,而该进程有子进程处于僵尸状态,那么它的所有僵尸子进程的父进程ID将被重置为1(init进程)。继承这些子进程的init进程将清理它们(也就是说init进程将wait它们,从而去除它们的僵尸状态)。
- 僵尸进程的危害:
- 僵尸进程的PID还占据着,意味着海量的子进程会占据满进程表项,会使后来的进程无法fork.
- 僵尸进程的内核栈无法被释放掉,为啥会留着它的内核栈,因为在栈的最低端,有着thread_info结构,它包含着 struct_task 结构,这里面包含着一些退出信息
- 僵死进程与孤儿进程的区别?
回答这个问题很简单, 父进程和子进程终止关系有两种:父进程先于子进程终止和子进程先于父进程终止。
孤儿进程——父进程先于子进程终止
终止进程的子进程的父进程更改为init进程,也就是父进程ID更改为1。当一个进程终止时,内核会检查所有的活动进程,找出正要终止进程的子进程并将其父进程更改为init进程。
僵尸进程———子进程先于父进程终止
内核会为每个终止子进程保留一定量的信息,父进程就可以通过调用wait函数来获取这些信息。如果父进程没有调用wait函数的话,则该资源就会一直被占用。
- 如何避免僵尸进程?
1.wait和waitpid函数:父进程调用wait/waitpid等函数等待子进程结束,如果尚无子进程退出wait会导致父进程阻塞。waitpid可以通过传递WNOHANG使父进程不阻塞立即返回。
2.sigaction信号处理函数(交给内核处理):如果父进程很忙可以用sigaction注册信号处理函数,在信号处理函数调用wait/waitpid等待子进程退出。(sigaction函数类似于signal函数,而且完全可以代替后者,也更稳定)
3.signal忽略SIGCHLD信号(交给内核处理) :通过signal(SIGCHLD, SIG_IGN)通知内核对子进程的结束不关心,由内核回收。如果不想让父进程挂起,可以在父进程中加入一条语句:signal(SIGCHLD,SIG_IGN);表示父进程忽略SIGCHLD信号,该信号是子进程退出的时候向父进程发送的。
4.fork两次:通过两次调用fork。父进程首先调用fork创建一个子进程然后waitpid等待子进程退出,子进程再fork一个孙进程后退出。这样子进程退出后会被父进程等待回收,而对于孙子进程其父进程已经退出所以孙进程成为一个孤儿进程,孤儿进程由init进程接管,孙进程结束后,init会等待回收。
第三种方法忽略SIGCHLD信号,这常用于并发服务器的性能的一个技巧因为并发服务器常常fork很多子进程,子进程终结之后需要服务器进程去wait清理资源。如果将此信号的处理方式设为忽略,可让内核把僵尸子进程转交给init进程去处理,省去了大量僵尸进程占用系统资源
1 wait()函数
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
进程一旦调用了wait,就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。
参数status用来保存被收集进程退出时的一些状态,它是一个指向int类型的指针。但如果我们对这个子进程是如何死掉的毫不在意,只想把这个僵尸进程消灭掉,(事实上绝大多数情况下,我们都会这样想),我们就可以设定这个参数为NULL,就象下面这样:
pid = wait(NULL);
如果成功,wait会返回被收集的子进程的进程ID,如果调用进程没有子进程,调用就会失败,此时wait返回-1,同时errno被置为ECHILD。
- wait系统调用会使父进程暂停执行,直到它的一个子进程结束为止。
- 返回的是子进程的PID,它通常是结束的子进程
- 状态信息允许父进程判定子进程的退出状态,即从子进程的main函数返回的值或子进程中exit语句的退出码。
- 如果status不是一个空指针,状态信息将被写入它指向的位置
可以上述的一些宏判断子进程的退出情况:
2 waitpid()函数
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
参数:
status:如果不是空,会把状态信息写到它指向的位置,与wait一样
options:允许改变waitpid的行为,最有用的一个选项是WNOHANG,它的作用是防止waitpid把调用者的执行挂起。
返回值:如果成功返回等待子进程的ID,失败返回-1
对于waitpid的p i d参数的解释与其值有关:
pid==-1 等待任一子进程。此种情况下,waitpid等效于wait。 pid>0 等待进程ID为pid的子进程。 pid==0 等待调用者进程组内的任一子进程。 pid<-1 等待组ID等于|pid|的任一子进程
参数options是以下各标志的按位或运算或为0。
常量 说明 WCONTINUED 等待一进城,它以前曾被停止,此后又继续,但状态尚未报告。 WEXITED 等待已退出的进程。 WNOHANG 如无可用的子进程退出状态,立即返回而非阻塞。 WNOWAIT 不破坏子进程退出状态。该子进程退出状态可由后续的wait、waitid或waitpid调用获取。 WSTOPPED 等待一进程,它已经停止,但其状态尚未报告。
wait与waitpid区别:
- 在一个子进程终止前, wait 使其调用者阻塞,而waitpid 有一选择项WNOHANG,可使调用者不阻塞。
- waitpid并不等待第一个终止的子进程—它有若干个选择项,可以控制它所等待的特定进程。
- 实际上wait函数是waitpid函数的一个特例。waitpid(-1, &status, 0)
示例:
如以下代码会创建100个子进程,但是父进程并未等待它们结束,所以在父进程退出前会有100个僵尸进程。
#include <stdio.h>
#include <unistd.h>
int main() {
int i;
pid_t pid;
for(i=0; i<100; i++) {
pid = fork();
if(pid == 0)
break;
}
if(pid>0) {
printf("press Enter to exit...");
getchar();
}
return 0;
}
其中一个解决方法即是编写一个SIGCHLD信号处理程序来调用wait/waitpid来等待子进程返回。
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
void wait4children(int signo) {
int status;
wait(&status);
}
int main() {
int i;
pid_t pid;
signal(SIGCHLD, wait4children);
for(i=0; i<100; i++) {
pid = fork();
if(pid == 0)
break;
}
if(pid>0) {
printf("press Enter to exit...");
getchar();
}
return 0;
}
但是通过运行程序发现还是会有僵尸进程,而且每次僵尸进程的数量都不定。这是为什么呢?其实主要是因为Linux的信号机制是不排队的,假如在某一时间段多个子进程退出后都会发出SIGCHLD信号,但父进程来不及一个一个地响应,所以最后父进程实际上只执行了一次信号处理函数。但执行一次信号处理函数只等待一个子进程退出,所以最后会有一些子进程依然是僵尸进程。
虽然这样但是有一点是明了的,就是收到SIGCHLD必然有子进程退出,而我们可以在信号处理函数里循环调用waitpid函数来等待所有的退出的子进程。至于为什么不用wait,主要原因是在wait在清理完所有僵尸进程后再次等待会阻塞。
技巧:利用信号处理技术消灭僵尸进程
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
void read_childproc(int sig)
{
int status;
pid_t id=waitpid(-1,&status,WNOHANG); //调用waitpid()函数销毁子进程
if(WIFEXITED(status)){
printf("Removed proc id: %d \n",id);
printf("Child send: %d \n",WEXITSTATUS(status));
}
}
int main(){
pid_t pid;
struct sigaction act;
act.sa_handler=read_childproc;
sigemptyset(&act.sa_mask);
act.sa_flags=0;
sigaction(SIGCHLD,&act,0); //注册信号,当子进程终止时产生SIGCHILD信号
pid=fork();
if(pid==0) { //子进程1
puts("Hi! I'm child one process");
sleep(10);
return 12;
}
else
{
printf("Child proc id: %d \n",pid);
pid=fork(); //子进程2
if(pid==0){
puts("Hi!I'm child two process");
sleep(10);
exit(24);
}
else
{
int i;
printf("Child proc id: %d \n",pid);
for(i=0;i<5;++i){
puts("wait...");
sleep(5);
}
}
}
return 0;
}
部分参考jessica的 http://www.cnblogs.com/wuchanming/p/4020463.html
部分参考尹圣雨《TCP/TP 网络编程》