僵尸(Zombie)进程

news2025/7/18 20:39:53

文章目录

  • 1.僵尸进程
  • 2.产生僵尸进程的原因
  • 3.利用 wait 函数销毁僵尸进程
  • 4.使用 waitpid 函数销毁僵尸进程

1.僵尸进程

进程完成工作后(执行完 main 函数中的程序后)应被销毁,但有时这些进程将变成僵尸进程,占用系统中的重要资源。这种状态下的进程称作“僵尸进程”,这也是给系统带来负担的原因之一。我们应该消灭这种进程。当然应掌握正确的方法,否则它会死灰复燃。

从下图可以看到,父进程的 PID 是 1166,子进程的 PID 是 1167

在这里插入图片描述

如果我们使用 kill -9 1167 命令将子进程干掉,观察父进程会收到什么信号?

从下图可以看到,父进程收到了 SIGCHLD 信号,子进程变成了僵尸进程。

在这里插入图片描述

在这里插入图片描述

在 Unix 系统中,一个子进程结束了,但是他的父进程还活着,但该父进程没有调用 wait()/waitpid() 函数来进行额外的处置,那么这个子进程就会变成一个僵尸进程。

僵尸进程已经被终止,不干活了,但是依旧没有被内核丢弃掉,因为内核认为父进程可能还需要该子进程的一些信息。作为开发者,坚决不允许僵尸进程的存在。

如何干掉僵尸进程?

  • 重启电脑。
  • 手动地把僵尸进程的父进程干掉,僵尸进程就会自动消失。
  • 一个进程被终止或者停止时,SIGCHLD 信号会被发送给父进程,所以,对于源码中有 fork() 行为的进程,我们应该拦截并处理 SIGCHLD 信号。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>

// 信号处理函数
void sig_usr(int signo)
{
    int status;

    switch (signo)
    {
        case SIGUSR1:
            printf("收到了SIGUSR1信号,进程id=%d!\n", getpid());    
            break;
        
        case SIGCHLD:
            printf("收到了SIGCHLD信号,进程id=%d!\n", getpid());

            // waitpid()函数获取子进程的终止状态,这样子进程就不会成为僵尸进程了
            // 第一个参数为-1,表示等待任何子进程
            // 第二个参数保存子进程的状态信息
            // 第三个参数提供额外选项,WNOHANG表示不要阻塞,让这个waitpid()立即返回
            pid_t pid = waitpid(-1, &status, WNOHANG);

            if (pid == 0) return; // 子进程没结束,会立即返回这个数字
            if (pid == -1) return; // 这表示这个waitpid()调用有错误,有错误也立即返回出去
            return; // 走到这里,表示成功,那也return吧
            break;
    }
}

int main(int argc, char* const* argv)
{
    pid_t pid;

    printf("进程开始执行!\n");

    // 系统函数,第一个参数是个信号,第二个参数是个函数指针,代表一个针对该信号的捕捉处理函数
    if (signal(SIGUSR1, sig_usr) == SIG_ERR)
    {
        printf("无法捕捉SIGUSR1信号!\n");
        exit(1);
    }

    if (signal(SIGCHLD, sig_usr) == SIG_ERR)
    {
        printf("无法捕捉SIGCHLD信号!\n");
        exit(1);
    }

    pid = fork(); // 创建一个子进程

    // 要判断子进程是否创建成功
    if (pid < 0)
    {
        printf("子进程创建失败,很遗憾!\n");
        exit(1);
    }

    // 现在父进程和子进程同时开始运行了 
    for (;;)
    {
        sleep(1);
        printf("休息1秒,进程id=%d!\n", getpid());
    }

    printf("再见了!\n");

    return 0;
}

在这里插入图片描述

2.产生僵尸进程的原因

向 exit 函数传递的参数值和 main 函数的 return 语句返回的值都会传递给操作系统。而操作系统不会销毁子进程,直到把这些值传递给产生该子进程的父进程。处在这种状态下的进程就是僵尸进程。也就是说,将子进程变成僵尸进程的正是操作系统。

问:此僵尸进程何时被销毁呢?

答:应该向创建子进程的父进程传递子进程的 exit 参数值或 return 语句的返回值。

问:如何向父进程传递这些值呢?

答:操作系统不会主动把这些值传递给父进程。只有父进程主动发起请求(函数调用)时,操作系统才会传递该值。换言之,如果父进程未主动要求获得子进程的结束状态值,操作系统将一直保存,并让子进程长时间处于僵尸进程状态。也就是说,父母要负责收回自己生的孩子。

接下来的示例将创建僵尸进程。

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main(int argc, char *argv[])
{
	pid_t pid = fork();
	
	if (pid == 0)    // if Child Process
	{
		puts("Hi, I am a child process");
	}
	else
	{
		// 输出子进程ID。可以通过该值查看子进程状态(是否为僵尸进程)。
		printf("Child Process ID: %d\n", pid);
		
		// 父进程暂停30秒。如果父进程终止,处于僵尸状态的子进程将同时销毁。因此,延缓父进程的执行以验证僵尸进程。
		sleep(30);    // Sleep 30 sec.
	}

	if (pid == 0)
		puts("End child process");
	else
		puts("End parent process");
	
	return 0;
}

编译运行:

gcc zombie.c -o zombie
./zombie

在这里插入图片描述

程序开始运行后,将在如上所示状态暂停。跳出这种状态前(30秒内),应验证子进程是否为僵尸进程,该验证在其他控制台窗口进行。

在这里插入图片描述

可以看出,PID 为 1387 的进程状态为僵尸进程(Z+)。另外,经过 30 秒的等待时间后,PID 为 1386 的父进程和 PID 为 1387 的僵尸子进程同时销毁。

3.利用 wait 函数销毁僵尸进程

如前所述,为了销毁子进程,父进程应主动请求获取子进程的返回值。

#include <sys/wait.h>

pid_t wait(int *statloc);

// 成功时返回终止的子进程ID,失败时返回-1

调用此函数时如果已有子进程终止,那么子进程终止时传递的返回值(exit 函数的参数值、main 函数的 return 返回值)将保存到该函数的参数所指内存空间。但函数参数指向的单元中还包含其他信息,因此需要通过下列宏进行分离:

  • WIFEXITED:子进程正常终止时返回“真”(true)。
  • WEXITSTATUS:返回子进程的返回值。

也就是说,向 wait 函数传递变量 status 的地址时,调用 wait 函数后应编写如下代码:

if (WIFEXITED(status))    // 是正常终止的吗?
{
    puts("Normal termination!");
    printf("Child pass num: %d", WEXITSTATUS(status));    // 那么返回值是多少?
}

根据上述内容编写如下示例,在下面示例中,不会再让子进程变成僵尸进程。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main(int argc, char *argv[])
{
	int status;

	// 第11行创建的子进程将在第15行通过main函数中的return语句终止。
	pid_t pid = fork();
	
	if (pid == 0)
	{
		return 3;
	}
	else
	{
		printf("Child PID: %d\n", pid);

		// 第22行中创建的子进程将在第26行通过调用exit函数终止。
		pid = fork();
		
		if (pid == 0)
		{
			exit(7);
		}
		else
		{
			printf("Child PID: %d\n", pid);
			
			// 调用wait函数。之前终止的子进程相关信息将保存到status变量,同时相关子进程被完全销毁。
			wait(&status);

			// 第36行中通过WIFEXITED宏验证子进程是否正常终止。如果正常退出,则调用WEXITSTATUS宏输出子进程的返回值。
			if (WIFEXITED(status))
				printf("Child send one: %d\n", WEXITSTATUS(status));
			
			// 因为之前创建了2个进程,所以再次调用wait函数和宏。
			wait(&status);
			
			if (WIFEXITED(status))
				printf("Child send two: %d\n", WEXITSTATUS(status));
			
			// 为暂停父进程终止而插入的代码。此时可以查看子进程的状态。
			sleep(30); // Sleep 30 sec.
		}
	}

	return 0;
}

编译运行:

gcc wait.c -o wait
./wait

输出结果:

在这里插入图片描述

在这里插入图片描述

可以看出,此时系统中并没有 PID 为 1497 和 1498 的进程,这是因为调用了 wait 函数,完全销毁了该子进程。另外,两个子进程终止时返回的 3 3 3 7 7 7 传递到了父进程。

这就是通过调用 wait 函数消灭僵尸进程的方法。调用 wait 函数时,如果没有已终止的子进程,那么程序将阻塞(Blocking)直到有子进程终止,因此要谨慎调用该函数。

4.使用 waitpid 函数销毁僵尸进程

wait 函数会引起程序阻塞,还可以考虑调用 waitpid 函数。这是防止僵尸进程的第二种方法,也是防止阻塞的方法。

#include <sys/wait.h>

pid_t waitpid(pid_t pid, int *statloc, int options);

// 成功时返回终止的子进程ID(或0),失败时返回-1
// pid:等待终止的目标子进程的ID,若传递-1,则与wait函数相同,可以等待任意子进程终止
// statloc:与wait函数的statloc参数具有相同含义
// options:传递头文件sys/wait.h中声明的常量WNOHANG,即使没有终止的子进程也不会进入阻塞状态,而是返回0并退出函数

下面介绍调用 waitpid 函数的示例。调用 waitpid 函数时,程序不会阻塞。

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main(int argc, char *argv[])
{
	int status;
	pid_t pid = fork();
	
	if (pid == 0)
	{
		// 调用sleep函数推迟子进程的执行。这会导致程序延迟15秒。
		sleep(15);
		return 24;
	}
	else
	{
		// while循环中调用waitpid函数。向第三个参数传递WNOHANG,因此,若之前没有终止的子进程将返回0。
		while (!waitpid(-1, &status, WNOHANG))
		{
			sleep(3);
			puts("sleep 3sec.");
		}

		if (WIFEXITED(status))
			printf("Child send %d\n", WEXITSTATUS(status));
	}

	return 0;
}

编译运行:

gcc waitpid.c -o waitpid
./waitpid

输出结果:

在这里插入图片描述

可以看出,第 22 22 22 行共执行了 5 5 5 次。另外,这也证明 waitpid 函数并未阻塞。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/396324.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

vue 监测数据改变的原理,添加属性

vue 监测数据改变的原理&#xff0c;添加属性 概况就是vue帮我们处理了&#xff0c;data的数据&#xff0c;加了get和set在生成虚拟dom模板之前&#xff0c; 开始做data数据的生成&#xff0c;get&#xff0c;set vue 提供的api : Vue.set(vm._data.student,‘key’,‘val’)…

C++复习笔记16

非类型的模板参数 类型形参即&#xff1a;出现在模板参数列表中&#xff0c;跟在class或者typename之后的参数类型名称。 非类型形参&#xff0c;就是用一个常量作为类(函数)模板的一个参数&#xff0c;在类(函数)模板中可将该参数当成常量来使用。 注意&#xff1a; 1. 浮点数…

分享一个可以早点下班的开发小技巧

这次来跟大家分享一下让大家早下班的工具。首先要声明一下&#xff0c;分享的工作&#xff0c;是为了大部分人&#xff0c;而不是“某一个人”&#xff0c;不喜勿喷哈&#xff01; 介绍的就是这两年很火的低代码平台&#xff0c;网上两种观点&#xff1a; 第一种人是很不屑&am…

等保2.0与1.0 测评要求的变化

No.1标准内容增加了 标准内容上最大的变化就是将安全要求分为了安全通用要求和扩展要求。首先&#xff0c;安全通用要求部分已对1.0标准的内容进行了优化&#xff0c;删除或修订了过时的要求项&#xff0c;新增了对新型网络攻击行为防护和个人信息保护等方面的新要求。其次&am…

SAP 更改物料基本计量单位

前言部分 在SAP中物料创建后&#xff0c;一旦发生业务&#xff0c;其基本计量单位便很难修改。由于单位无法满足业务要求&#xff0c;往往会要求新建一个物料替代旧物料。这时候除了要将旧物料上所有的未清业务删除外&#xff0c;还需要替换工艺与BOM中的旧物料。特别是当出现旧…

一文带你看懂:亿级大表垂直拆分的工程实践

伴随着不断扩张的业务量&#xff0c;在数据库层面一般会经历数据拆分。解决问题的第一步&#xff0c;就是重新评估DB表结构设计的合理性。我们开发者会对表结构和业务代码进行重构&#xff0c;在之前的文章《业务系统重构》我有提到过。大表问题我实际遇到的是怎么样的情况呢&a…

CAD指令框找不到了怎么调出来?CAD指令框调出方法

CAD制图过程中&#xff0c;为了提高设计师的绘图效率&#xff0c;经常会用到各种CAD命令快捷键&#xff0c;可是CAD指令框突然不见了&#xff0c;这就让人很头疼了。CAD指令框找不到了怎么调出来呢&#xff1f;本节内容小编以浩辰CAD软件为例来给大家分享一下CAD指令框调出方法…

网络协议(十二):HTTPS(SSL/TLS、TLS1.2的连接)

网络协议系列文章 网络协议(一)&#xff1a;基本概念、计算机之间的连接方式 网络协议(二)&#xff1a;MAC地址、IP地址、子网掩码、子网和超网 网络协议(三)&#xff1a;路由器原理及数据包传输过程 网络协议(四)&#xff1a;网络分类、ISP、上网方式、公网私网、NAT 网络…

mysql 数据库 tinyint 类型字段取数变成 true/false 的解决方案

mysql 数据库 tinyint 类型字段取数变成 true/false 的解决方案 灌水 问题描述&#xff1a; 在 mysql 数据库设定上&#xff0c;有个字段类型是 tinyint 类型&#xff0c;长度为 1&#xff0c; 设定如下所示&#xff1a; 常规 sql 取数&#xff0c; 取到润乾报表内的时候&…

女神节告白代码

今天是女神节&#xff0c;送给所有女神们一句话&#xff1a; 爱自己是终生浪漫的开始&#xff0c;无论何时都要好好爱自己 目录 1. 请求动画帧填充 2.点类 3.粒子类 ​编辑 4.ParticlePool 池类 5.创建和填充 6.处理循环队列 7.更新活动粒子 8.移除非活性粒子 9.绘制有…

MQTT协议-CONNECT报文介绍

MQTT协议-CONNECT报文介绍 参考MQTT协议中文笔记&#xff1a;https://mcxiaoke.gitbooks.io/mqtt-cn/content/mqtt/01-Introduction.html Connect报文主要用于客户端连接服务器的&#xff0c;未涉及具体数据的传输&#xff0c;可以使用网络调试助手来连接阿里云平台&#xff…

什么是档案级光盘?它的寿命是多少年?

我们经常会听到有人在说&#xff1a;CD、DVD光盘的寿命多少多少年&#xff0c;蓝光光盘的寿命多少多少年。实际上这个说法是不对的&#xff0c;至少是不准确的&#xff0c;因为同样是CD、DVD光盘或者蓝光光盘&#xff0c;也分等级&#xff0c;而不同等级的光盘的寿命是不一样的…

ENVI_Classic:快速入门_菜单栏常见功能的基本介绍

说明&#xff1a;由于实验要求&#xff0c;所以并没有对各个功能进行详尽的解释&#xff0c;大多点到为止&#xff0c;少部分实验内容是实验要求所以步骤详尽。当然由于经验不足&#xff0c;有一些可能存在错误恳请指正.1. 实验目的通过ENVI Classic对自行下载的遥感图像进行一…

JavaScript Math 算数对象实例集合

文章目录JavaScript Math 算数对象实例集合使用 round() 对数字进行舍入使用 random() 来返回 0 到 1 之间的随机数使用 max() 来返回两个给定的数中的较大的数使用 min() 来返回两个给定的数中的较小的数摄氏度与华氏转换JavaScript Math 算数对象实例集合 注意&#xff1a; 了…

MySQL基础篇2

第一章 SQL语句之DQL 语法&#xff1a;查询不会对数据库中的数据进行修改&#xff0c;根据指定的方式来呈现数据。 语法格式&#xff1a; select * | 列名,列名 from 表名 [where 条件表达式] select 是查询指令&#xff0c;可以读 1 ~ n 行数据&#xff1b; 列名换成 * 号&a…

网络:TCP与UDP相关知识(详细)

目录&#xff1a;1、UDP 和 TCP 的特点与区别2、UDP 、TCP 首部格式3、TCP 的三次握手和四次挥手4、TCP 的三次握手&#xff08;为什么三次&#xff1f;&#xff09;5、TCP 的四次挥手&#xff08;为什么四次&#xff1f;&#xff09;6、TCP 长连接和短连接的区别7、TCP粘包、拆…

Caddy2学习笔记——Caddy2的安装、部署和编译小白教程

个人环境概述 本人拥有一个国内云服务商的云主机和一个备案好的域名&#xff0c;希望通过caddy2来作为web服务器。我的云主机是公网ip&#xff0c;地址为&#xff1a;43.126.100.78&#xff1b;我备案好的域名是&#xff1a;hotgirl.com。后面的文章都以上述的ip和域名来进行讲…

什么是jvm?

说明&#xff1a;做java开发的几乎都知道jvm这个名词&#xff0c;但是由于jvm对实际的简单开发的来说关联的还是不多&#xff0c;一般工作个一两年&#xff08;当然不包括爱学习的及专门做性能优化的什么的&#xff09;&#xff0c;很少有人能很好的去学习及理解什么是jvm&…

跨源资源共享(CORS)-亲测理解,以及对http的状态,参数的理解和使用,对预检请求的触发和解决

跨源资源共享&#xff08;CORS&#xff09;-亲测理解&#xff0c;以及对http的状态&#xff0c;参数的理解和使用 跨源资源共享&#xff08;CORS&#xff0c;或通俗地译为跨域资源共享&#xff09;是一种基于HTTP 头的机制&#xff0c;该机制通过允许服务器标示除了它自己以外的…

Python 的IDE——PyCharm

IDE介绍与安装 介绍 集成开发环境&#xff08;IDE&#xff09; 集成开发环境(IDE,integrated Development Environment) —— 集成开发软件需要的所有工具&#xff0c;一般包括以下工具&#xff1a; 图形用户界面 代码编辑器(支持代码补全、自动缩进) 编译器/解释器 调试器…