1、什么是僵尸进程?
首先内核会释放终止进程(调用了 exit 系统调用)所使用的所有存储区,关闭所有打开的文件 等,但内核为每一个终止子进程保存了一定量的信息。这些信息至少包括进程 ID,进程的终止状 态,以及该进程使用的 CPU 时间,所以当终止子进程的父进程调用 wait 或 waitpid 时就可以得 到这些信息。
而僵尸进程就是指:一个进程执行了 exit 系统调用退出,而其父进程并没有为它收尸(调用 wait 或 waitpid 来获得它的结束状态)的进程。 任何一个子进程(init 除外)在 exit 后并非马上就消失,而是留下一个称外僵尸进程的数据结构, 等待父进程处理。这是每个子进程都必需经历的阶段。另外子进程退出的时候会向其父进程发送一 个 SIGCHLD 信号。
2、僵尸进程的目的
设置僵死状态的目的是维护子进程的信息,以便父进程在以后某个时候获取。这些信息至少包 括进程 ID,进程的终止状态,以及该进程使用的 CPU 时间,所以当终止子进程的父进程调用 wait 或 waitpid 时就可以得到这些信息。如果一个进程终止,而该进程有子进程处于僵尸状态,那么 它的所有僵尸子进程的父进程 ID 将被重置为 1(init 进程)。继承这些子进程的 init 进程将清 理它们(也就是说 init 进程将 wait 它们,从而去除它们的僵尸状态)。
3、怎么避免僵尸进程?
1.通过 signal(SIGCHLD, SIG_IGN)通知内核对子进程的结束不关心,由内核回收。如果不 想让父进程挂起,可以在父进程中加入一条语句:signal(SIGCHLD,SIG_IGN);表示父进程忽略 SIGCHLD 信号,该信号是子进程退出的时候向父进程发送的。
2.父进程调用 wait/waitpid 等函数等待子进程结束,如果尚无子进程退出 wait 会导致父进 程阻塞。waitpid 可以通过传递 WNOHANG 使父进程不阻塞立即返回。
3.如果父进程很忙可以用 signal 注册信号处理函数,在信号处理函数调用 wait/waitpid 等待子进程退出。
4.通过两次调用 fork。父进程首先调用 fork 创建一个子进程然后 waitpid 等待子进程退出, 子进程再 fork 一个孙进程后退出。这样子进程退出后会被父进程等待回收,而对于孙子进程其父 进程已经退出所以孙进程成为一个孤儿进程,孤儿进程由 init 进程接管,孙进程结束后,init 会等待回收。
第一种方法忽略 SIGCHLD 信号,这常用于并发服务器的性能的一个技巧因为并发服务器常常 fork 很多子进程,子进程终结之后需要服务器进程去 wait 清理资源。如果将此信号的处理方式 设为忽略,可让内核把僵尸子进程转交给 init 进程去处理,省去了大量僵尸进程占用系统资源。
4、僵尸进程的处理方法
4.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 不是一个空指针,状态信息将被写入它指向的位置
4.2 waitpid()函数
#include #include pid_t waitpid(pid_t pid, int *status, int options);
参数:
status:如果不是空,会把状态信息写到它指向的位置,与 wait 一样
options:允许改变 waitpid 的行为,最有用的一个选项是 WNOHANG,它的作用是防止 waitpid 把调用者的执行挂起
返回值:如果成功返回等待子进程的 ID,失败返回-1
对于 waitpid 的 pid 参数的解释与其值有关:
pid == -1 等待任一子进程。于是在这一功能方面 waitpid 与 wait 等效。
pid > 0 等待其进程 I D 与 p i d 相等的子进程。
pid == 0 等待其组 I D 等于调用进程的组 I D 的任一子进程。换句话说是与调用者进程同在一个组的进程。
pid < -1 等待其组 I D 等于 p i d 的绝对值的任一子进程 wait 与 waitpid 区别:
在一个子进程终止前, wait 使其调用者阻塞,而 waitpid 有一选择项,可使调用者不阻塞。
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; }
所以最佳的方案如下:
#include <stdio.h> #include <unistd.h> #include <signal.h> #include <errno.h> #include <sys/types.h> #include <sys/wait.h> void wait4children(int signo) { int status; pid_t pid; while(pid=waitpid(-1, &status, WNOHANG) > 0){ //if (WIFEXITED(status)) // printf("------------child %d exit %d\n", pid, WEXITSTATUS(status)); // else if (WIFSIGNALED(status)) // printf("child %d cancel signal %d\n", pid, WTERMSIG(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; }
这里使用 waitpid 而不是使用 wait 的原因在于:我们在一个循环内调用 waitpid,以获取 所有已终止子进程的状态。我们必须指定 WNOHANG 选项,它告诉 waitpid 在有尚未终止的子进程 在运行时不要阻塞。我们不能在循环内调用 wait,因为没有办法防止 wait 在正运行的子进程尚 有未终止时阻塞。