僵尸进程和孤儿进程
🖋️ 1、孤儿进程
孤儿进程,顾名思义,和现实生活中的孤儿有点类似,当一个进程的父进程结束时,但是它自己还没有结束,那么这个进程将会成为孤儿进程。最后孤儿进程将会被init进程(进程号为1)的进程收养,当然在子进程结束时也会由init进程完成对它的状态收集工作,因此一般来说,孤儿进程并不会有什么危害。
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
int main(){
pid_t pid;
pid = fork(); //创建一个进程
if (pid < 0){ //创建失败
perror("fork error:");
exit(1);
}
//子进程
if (pid == 0){
printf("I am the child process.\n");
//输出进程ID和父进程ID
printf("pid: %d\tppid:%d\n",getpid(),getppid());
printf("I will sleep five seconds.\n");
sleep(5); //睡眠5s,保证父进程先退出
printf("pid: %d\tppid:%d\n",getpid(),getppid());
printf("child process is exited.\n");
}else{ //父进程
printf("I am father process.\n");
sleep(1); //父进程睡眠1s,保证子进程输出进程id
printf("father process is exited.\n");
}
return 0;
}
🖋️ 2、僵尸进程
僵尸进程是指:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的某些信息如进程描述符仍然保存在系统中。这种进程称之为僵尸进程。
任何一个子进程(init除外)在exit()之后,并非马上就消失掉,而是留下一个称为僵尸进程(Zombie)的数据结构,等待父进程处理。这是每个子进程在结束时都要经过的阶段。如果子进程在exit()之后,父进程没有来得及处理,这时用ps命令就能看到子进程的状态是“Z”。如果父进程能及时 处理,可能用ps命令就来不及看到子进程的僵尸状态,但这并不等于子进程不经过僵尸状态。 如果父进程在子进程结束之前退出,则子进程将由init接管。init将会以父进程的身份对僵尸状态的子进程进行处理。
🐹 2.1、僵尸进程的危害
僵尸进程会在系统中保留其某些信息如进程描述符、进程id等等。以进程id为例,系统中可用的pid是有限的,如果由于系统中大量的僵尸进程占用pid,就会导致因为没有可用的pid系统不能产生新的进程。

🐹 2.2、僵尸进程解决办法
🍈 2.2.1、通过信号机制
子进程退出时向父进程发送SIGCHILD信号,父进程处理SIGCHILD信号。在信号处理函数中调用waitpid进行处理僵尸进程。

需要注意的是,捕获SIGCHLD信号并且调用wait来清理退出的进程,不能彻底避免产生僵尸进程.
来看一种特殊的情况:
假设有一个client/server的程序,对于每一个连接过来的client,server都启动一个新的进程去处理来自这个client的请求。然后有一个client进程,在这个进程内,发起了多个到server的请求(假设5个),则server会fork 5个子进程来读取client输入并处理(同时,当客户端关闭套接字的时候,每个子进程都退出);当我们终止这个client进程的时候 ,内核将自动关闭所有由这个client进程打开的套接字,那么由这个client进程发起的5个连接基本在同一时刻终止。这就引发了5个FIN,每个连接一个。server端接受到这5个FIN的时候,5个子进程基本在同一时刻终止。这就又导致差不多在同一时刻递交5个SIGCHLD信号给父进程。
首先运行服务器程序,然后运行客户端程序,运用ps命令看以看到服务器fork了5个子进程,如图:

然后Ctrl+C终止客户端进程,在我机器上边测试,可以看到信号处理函数运行了3次,还剩下2个僵尸进程,如图:

通过上边这个实验我们可以看出,建立信号处理函数并在其中调用wait并不足以防止出现僵尸进程,其原因在于:所有5个信号都在信号处理函数执行之前产生,而信号处理函数只执行一次,因为Unix信号一般是不排队的。 更为严重的是,本问题是不确定的,依赖于客户FIN到达服务器主机的时机,信号处理函数执行的次数并不确定。
正确的解决办法是调用waitpid而不是wait,这个办法的方法为:信号处理函数中,在一个循环内调用waitpid,以获取所有已终止子进程的状态。我们必须指定WNOHANG选项,他告知waitpid在有尚未终止的子进程在运行时不要阻塞。(我们不能在循环内调用wait,因为没有办法防止 wait在尚有未终止的子进程在运行时阻塞,wait将会阻塞到现有的子进程中第一个终止为止),下边的程序分别给出了这两种处理办法 (func_wait,func_waitpid)。
🍈 2.2.2、 fork两次
fork两次原理是将子进程成为孤儿进程,从而其的父进程变为init进程,通过init进程可以处理僵尸进程。

Last updated
Was this helpful?