🔥博客主页: 我要成为C++领域大神
🎥系列专栏:【C++核心编程】 【计算机网络】 【Linux编程】 【操作系统】
❤️感谢大家点赞👍收藏⭐评论✍️
本博客致力于知识分享,与更多的人进行学习交流
什么是僵尸进程?
僵尸进程(Zombie Process)是指一个已经完成执行(终止)的子进程,在父进程没有调用 wait()
或 waitpid()
来获取子进程的终止状态之前,该子进程的进程描述符仍然存在于系统中的状态。
僵尸进程产生的原因
当一个进程创建子进程,并且子进程先于父进程结束时,子进程的进程控制块(PCB)会继续存在,但是它不再具有执行状态(即进程不再运行)。但是,这时候父进程没有主动调用 wait()
或 waitpid()
函数来获取子进程的退出状态码,子进程的进PCB仍然会保留在系统中,这种状态的子进程就被称为僵尸进程。
我们可以通过下面的图示来理解僵尸进程产生的原因:
当子进程代码区执行完毕后,子进程先于父进程结束,调用exit
函数。exit
是用户层函数,会继续向下调用_EXIT
函数
_EXIT
会对用户层数据进行释放,但是对内核层的空间数据释放不完全,PCB不会释放,造成内存泄漏,就产生僵尸进程。
僵尸进程的危害
1、内存泄漏,进程结束后内存没有释放完毕,其他人使用此内存产生异常。
2、每个僵尸进程会持续占用一个PCB,影响新进程的创建与使用。
查看僵尸进程
下面我们将写一个产生僵尸进程的demo程序:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <string.h>
#include <sys/fcntl.h>
int main()
{
pid_t pid=fork();
if(pid>0){
printf("Parent PID:%d Running \n",getpid());
while(1)
sleep(1);//让父进程卡在这里不能退出
}else if(pid==0){
printf("Child PID:%d Running \n",getpid());
sleep(5);
exit(0);//让子进程休眠5s然后退出。
}else{
perror("fork call failed");
}
return 0;
}
打印出父子进程的PID
我们使用ps -aux命令查看进程状态,因为子进程先于父进程退出,所以子进程变成僵尸态
如何杀死僵尸进程
1、子进程成为僵尸进程,父进程退出,系统会回收僵尸进程。
2、父进程调用
wait()
或waitpid()
来回收子进程的资源。
wait函数
pid_t wait(int *status)
wait()
函数是一个阻塞函数,父进程执行后,阻塞等待子进程结束,子进程结束后,wait
回收成功,释放PCB资源,返回僵尸进程的PID。如果没有子进程却进行回收,返回-1。
参数:
status
:用于存储子进程退出状态的变量指针。
返回值:
如果成功等待到子进程退出,返回子进程的PID。
如果没有子进程(没有设置 WNOHANG
),返回-1,并设置 errno
。
下面我们写一个使用wait
函数杀死僵尸进程的demo程序:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <string.h>
#include <sys/fcntl.h>
#include <sys/wait.h>
int main()
{
pid_t pid=fork();
pid_t zpid;
if(pid>0){
printf("Parent PID:%d Running \n",getpid());
zpid=wait(NULL); //回收僵尸进程
printf("Zombie ZPID:%d delete sucess\n",zpid);
}else if(pid==0){
printf("Child PID:%d Running \n",getpid());
sleep(5);
exit(0);//让子进程休眠5s然后退出。
}else{
perror("fork call failed");
}
return 0;
}
成功杀死了僵尸进程。
wait
调用一次只回收一个僵尸,如果有多个僵尸进程,需要循环调用wait
为什么_EXIT不对进程进行彻底回收?
wait函数在释放PCB之前可以从PCB中提取子进程退出信息,父进程可以进行校验。通俗易懂地说”验尸操作“
wait函数的弊端
wait函数阻塞回收子进程(僵尸进程),主动回收方案,阻塞回收会影响父进程任务的执行,在等待过程中无法进行其它操作。如果子进程长期不退出,父进程将瘫痪。
waitpid函数
pid_t waitpid(pid_t pid, int *status, int options)
waitpid()
函数允许指定等待某个特定的子进程或者等待满足特定条件的任意子进程退出。
- 参数:
-
pid
:指定回收方式。
pid
>0,指定一个子进程pid
,回收这个子进程-1
:等待任意一个子进程。0
:等待与调用进程同组的任意子进程。只能回收掉与父进程同组的所有子进程-1000
:指定组id,实现跨组回收。
这里我们需要明确一下进程组的概念:进程组(Process Group)是一组相关联的进程的集合。
进程组中的每个进程都有一个相同的进程组ID(PGID),这个ID是整数类型的,用来标识进程组。通常,一个进程组由一个进程作为组长(Group Leader)来创建,并且组长进程的进程ID(PID)就是进程组的ID(PGID)。
-
status
:用于存储子进程退出状态的变量指针。options
:控制函数的行为,如是否非阻塞等。
- 返回值:
>0
,表示成功等待到子进程退出,返回子进程的PID。0
,子进程没有执行到结束(并且设置了WNOHANG
),非阻塞轮询,过一会再来查询子进程是否执行完。-1
,回收失败,并设置errno
。
下面我们写一个使用waitpid
函数设置非阻塞轮询检查僵尸进程的demo程序:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <string.h>
#include <sys/fcntl.h>
#include <sys/wait.h>
int main()
{
pid_t pid=fork();
pid_t zpid;
if(pid>0){
printf("Parent PID:%d Running \n",getpid());
//zpid=wait(NULL); //回收僵尸进程
while((zpid=waitpid(-1,NULL,WNOHANG))!=-1){
//使用waitpid设置非阻塞回收僵尸进程
if(zpid==0){
//父进程工作区
printf("Parent Working...\n");
usleep(400000);
}
else if(zpid>0){
printf("Zombie ZPID:%d\n",zpid);
break;
}
}
printf("Zombie ZPID:%d delete sucess\n",zpid);
}else if(pid==0){
printf("Child PID:%d Running \n",getpid());
sleep(5);
exit(0);//让子进程休眠5s然后退出。
}else{
perror("fork call failed");
}
return 0;
}
运行结果:
可以看到,当子进程没有退出的时候,父进程可以根据waitpid
的返回值来轮询执行自己的工作和检查子进程是否退出。当子进程结束的时候,成功杀死子进程。
status参数 “验尸”
通过查看waitpid函数的手册我们可以看到,有四个函数可以用于退出情况的判断。
下面我们写一个demo程序判断进程“死因”
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <string.h>
#include <sys/fcntl.h>
#include <sys/wait.h>
int main()
{
pid_t pid;
int i=0;
for(i=0;i<2;++i){
pid=fork();
if(pid==0) break;
}
if(pid>0){
printf("Parent Running PID:%d\n",getpid());
int status;//这里需要将status定义成整型而非指针,因为是输出参数。
pid_t zpid;
while((zpid=waitpid(-1,&status,WNOHANG))!=-1){
if(zpid==0) {
//AGAIN
}
else if(zpid>0){
if(WIFEXITED(status)){
printf("Zombie Process PID:%d 进程正常退出,退出码或返回值为%d\n"
,zpid,WEXITSTATUS(status));
}
if(WIFSIGNALED(status)){
printf("Zombie Process PID:%d 进程异常退出,信号编号:%d\n"
,zpid,WTERMSIG(status));
}
}
}
}else if(pid==0){
if(i==0){
printf("Child %d Running PID:%d\n",i,getpid());
sleep(5);
printf("Child %d Exiting PID:%d\n",i,getpid());
exit(8);
}
else if(i==1){
printf("Child %d Running PID:%d\n",i,getpid());
while(1)
sleep(1);
}
}else{
perror("fork call failed");
exit(0);
}
return 0;
}
通过exit正常退出的僵尸进程:成功打印出了退出码
通过信号异常退出的僵尸进程:打印出了信号编号