linux处理僵尸进程
由来
在linux下,如果一个进程终止,内核会释放该进程使用的所有存储区,关闭所有文件句柄等,但是,内核会为每个终止子进程保留一定量的信息。这些信息至少包括进程ID,进程的终止状态,以及该进程使用的CPU时间。当终止子进程的父进程调用wait或waitpid时就可以得到这些信息
僵尸进程指:一个进程退出后,而其父进程并没有为它收尸(调用wait或waitpid来获得它的结束状态)的进程
任何一个子进程(init除外)在退出后并非马上就消失,而是留下一个称为僵尸进程的数据结构,等待父进程处理。这是每个子进程都必需经历的阶段。另外子进程退出的时候会向其父进程发送一个SIGCHLD信号
作用
设置僵死状态的目的是维护子进程的信息,以便父进程在以后某个时候获取。如果一个进程终止,而该进程有子进程处于僵尸状态,那么它的所有僵尸子进程的父进程ID将被重置为1(init进程)。init进程将清理它们(也就是说init进程将wait它们,从而去除它们的僵尸状态)
危害
内核保存僵尸进程信息,这就是占用了系统资源,而且僵尸进程的进程号一直会被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程
避免
1. 通过signal(SIGCHLD, SIG_IGN)通知内核对子进程的结束不关心,由内核回收。
2. 父进程调用wait/waitpid等函数等待子进程结束,如果尚无子进程退出,wait会导致父进程阻塞。waitpid可以通过传递WNOHANG使父进程不阻塞立即返回。
3. 如果父进程很忙可以用signal注册信号处理函数,在信号处理函数调用wait/waitpid等待子进程退出
4. 通过两次调用fork。父进程首先调用fork创建一个子进程然后waitpid等待子进程退出,子进程再fork一个孙进程后退出。这样子进程退出后会被父进程等待回收,而对于孙子进程其父进程已经退出所以孙进程成为一个孤儿进程,孤儿进程由init进程接管,孙进程结束后,init会等待回收。
第一种方法忽略SIGCHLD信号,这常用于并发服务器的性能的一个技巧因为并发服务器常常fork很多子进程,子进程终结之后需要服务器进程去wait清理资源。如果将此信号的处理方式设为忽略,可让内核把僵尸子进程转交给init进程去处理,省去了大量僵尸进程占用系统资源。
wait函数
pid_t wait(int *status);
进程一旦调用了wait,就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。
参数status用来保存被收集进程退出时的一些状态,它是一个指向int类型的指针。但如果我们对这个子进程是如何死掉的毫不在意,只想把这个僵尸进程消灭掉,(事实上绝大多数情况下,我们都会这样想),我们就可以设定这个参数为NULL
如果成功,wait会返回被收集的子进程的进程ID,如果调用进程没有子进程,调用就会失败,此时wait返回-1,同时errno被置为ECHILD。
- wait系统调用会使父进程暂停执行,直到它的一个子进程结束为止。
- 返回的是子进程的PID,它通常是结束的子进程
- 状态信息允许父进程判定子进程的退出状态,即从子进程的main函数返回的值或子进程中exit语句的退出码。
- 如果status不是一个空指针,状态信息将被写入它指向的位置
可以上述的一些宏判断子进程的退出情况:
waitpid函数
pid_t waitpid(pid_t pid, int *status, int options);
status:如果不是空,会把状态信息写到它指向的位置,与wait一样
options:允许改变waitpid的行为,最有用的一个选项是WNOHANG,它的作用是防止waitpid把调用者的执行挂起
The value of options is an OR of zero or more of the following con-
stants:
WNOHANG return immediately if no child has exited.
WUNTRACED also return if a child has stopped (but not traced via
ptrace(2)). Status for traced children which have stopped
is provided even if this option is not specified.
WCONTINUED (since Linux 2.6.10)
also return if a stopped child has been resumed by delivery
of SIGCONT.
返回值:如果成功返回等待子进程的ID,失败返回-1
对于waitpid的p i d参数的解释与其值有关:
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);
如果使用wait处理僵尸进程,如下程序:
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <errno.h>
#include <string.h>
int cnt = 0;
void waitpidexit(int sig)
{
while (waitpid(-1, NULL, WNOHANG) > 0)
{
cnt++;
}
}
void waitexit(int sig)
{
wait(NULL);
cnt++;
}
int main()
{
int pid;
printf("%d\n", getpid());
// signal(SIGCHLD, SIG_IGN);
signal(SIGCHLD, waitexit);
// signal(SIGCHLD, waitpidexit);
for (int i = 0; i < 500; ++i)
{
pid = fork();
if (pid == 0 || pid < 0)
{
break;
}
}
if (pid == 0)
{
exit(0);
}
else if (pid < 0)
{
fprintf(stderr, "fork error: %d, %s\n", errno, strerror(errno));
exit(0);
}
else
{
getchar();
printf("%d\n", cnt);
}
return 0;
}
运行之后,发现还是有很多僵尸进程:
这是因为 SIGCHLD 为不可靠非实时信号,不支持排队,可能会造成信号丢失。假如在某一时间段多个子进程退出后都会发出SIGCHLD信号,但父进程来不及一个一个地响应,所以最后父进程实际上只执行了一次信号处理函数。但执行一次信号处理函数只等待一个子进程退出,所以最后会有一些子进程依然是僵尸进程。
虽然这样但是有一点是明了的,就是收到SIGCHLD必然有子进程退出,而我们可以在信号处理函数里循环调用waitpid函数来等待所有的退出的子进程。至于为什么不用wait,主要原因是在wait在清理完所有僵尸进程后再次等待会阻塞。
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <errno.h>
#include <string.h>
int cnt = 0;
void waitpidexit(int sig)
{
while (waitpid(-1, NULL, WNOHANG) > 0)
{
cnt++;
}
}
void waitexit(int sig)
{
wait(NULL);
cnt++;
}
int main()
{
int pid;
printf("%d\n", getpid());
// signal(SIGCHLD, SIG_IGN);
// signal(SIGCHLD, waitexit);
signal(SIGCHLD, waitpidexit);
for (int i = 0; i < 500; ++i)
{
pid = fork();
if (pid == 0 || pid < 0)
{
break;
}
}
if (pid == 0)
{
exit(0);
}
else if (pid < 0)
{
fprintf(stderr, "fork error: %d, %s\n", errno, strerror(errno));
exit(0);
}
else
{
getchar();
printf("%d\n", cnt);
}
return 0;
}