可重入函数 volatile SIGCHLD

news2024/12/26 20:26:35

目录

  • 1. 可重入函数
  • 2. volatile
  • 3. SIGCHLD 信号

1. 可重入函数

在这里插入图片描述

场景:当我们在全局区定义一个链表(不带头结点),然后对链表做头插结点的操作,即插入 node1 结点(如上图所示)。在插入 node1 时需要执行两条语句,一条是修改 node1 的 next 指针的指向,一条是将头结点更新为 node1,而此时进程刚执行完 p->next = head; ,正准备更新头结点的指向时,进程发送了信号中断,进程头也不回,转而执行信号捕捉了,刚好信号处理方法也要执行插入结点(node2),因此就先把 node2 结点头插到链表中了,最后再返回完成 node1 插入时剩下的语句。

此时同一个方法 insert,在调用插入 node1 结点时是 main 函数执行流,后面进程发生中断,在信号捕捉时调用的 insert 插入 node2 结点是 sighandler 执行流。也即,insert 这个方法在 main 函数执行流还没结束时,又被 sighandler 方法重复进入,这种现象称为函数被重复进入,简称函数被重入。

而当进程中断执行信号捕捉,把 node2 结点头插到链表之后,回到 main 函数,依旧要继续执行没有执行完的 node1 的头插,即 node1=head。问题就来了,本来 head 指针指向的是 node2的,现在转而指向 node1 了,没有指针指向 node2,即 node2 结点丢失,进而导致内存泄漏!

对于该场景下的 insert 函数,被不同执行流重复进入、并且可能发生错误(或已经发生)的情况下,称为不可重入函数!反之称为可重入函数。

对于上述场景,main 函数和 sighandler 方法是两个执行流,其实是一种 “假结论”。因为当没有进程没有收到信号时,sighandler 方法并不会被执行,但是 main 函数是一定会执行的。换言之,sighandler 方法的执行与 main 函数没有任何关系,在操作系统的设计上,当进程收到信号了,要转而执行 sighandler 方法时,main 函数是被暂停的,所以这两股执行流并不是同时执行的。

如果一个函数符合以下条件之一,则是不可重入的:

  • 调用了 malloc 或 free,因为 malloc 也是用全局链表来管理堆的。
  • 调用了标准 I/O 库函数。标准 I/O 库的很多实现都以不可重入的方式使用全局数据结构。

2. volatile

int flag = 0;
void handler(int signo)
{
    cout << "process get a signal: " << signo << endl;
	flag = 1;	// 跳出while循环
}
int main()
{
	signal(2, handler);
	while(!flag);
	cout << "process quit! pid: " << getpid() << endl;
	return 0;
}

在这段代码中,while(!flag); 的条件一直为真。当进程运行起来,我们向进程发送一个 2 号信号,由于代码中对 2 号做了信号捕捉,所以 handler 自定义处理方法被调用,然后 flag 被置为 1,while 循环的条件为假,跳出循环,最后进程运行完毕退出。

在这里插入图片描述

在这里,while(!flag) 条件判断的本质也是一种计算,计算分为算数运算和逻辑运算,我们对 flag 取反,就是为了让 CPU 将 while 的条件当作运算去执行。当 CPU 进行逻辑运算时,就注定了需要先将 flag 从内存读取到 CPU 的寄存器中,才能够被 CPU 执行运算。而编译器在编译这份代码时发现 flag 这个变量并不会被修改(while 循环也只是一直在读取 flag 变量的内容,并没有对 flag 做写入操作,而因为 hander 方法和 main 函数是属于两个不同的执行流,因此 handler 方法内对 flag 变量做修改,main 执行流并无法感知,所以编译器认为 flag 不会被修改)。结合这两点,因此编译器在编译时可能会对 flag 变量做优化,将 flag 的内容拷贝到 CPU 的寄存器中,让 CPU 在读取 flag 变量时不需要再进行访存,而是直接从寄存器中直接读取。

编译器在编译时,可以加上 -O 选项来进行优化。O0 代表没有优化,O1 - O3 优化层度递增。

g++ test.c -o test -O3 -std=c++11

在这里插入图片描述

编译时带上优化后,我们的进程不会退出,即便捕捉信号的处理方法内对 flag 置 1 了。这就涉及到我们刚说的,flag 变量的内容拷贝到了 CPU 的寄存器中,CPU 往后在读取 flag 变量的内容时,不会再进行访存,而是直接从寄存器中读。

而为什么捕捉信号的处理方法内对 flag 置 1 了,进程不会退出呢?

我们需要清楚一点的是,不管编译器如何优化,把变量的内容拷贝到寄存器也好,这个变量的本体都必须在内存都要存在,即这个被优化的变量依旧需要存储在内存中。而 flag 的内容被优化到寄存器这件事,是在编译时就决定的事情 ,进程收到信号,执行 handler 方法,将 flag 变量由 0 置 1,这是进程运行之后才发生的事情。问题就出在,一开始 flag 的内容就已经被拷贝到寄存器了,所以从此以后 CPU 都不在关注内存中 flag 的内容(代码层面上对 flag 做写入,是对内存中的flag 做的写入),只读取寄存器的 flag。所以即便后来内存中的 flag 被改了,但不好意思,CPU 不知道这件事,对于 CPU 来说,flag 一直都是 0,因为寄存器中存储的 flag 的值为 0,这就是为什么当代码被优化之后,flag 即便被修改为 1 了,进程也无法退出的原因。

对于 flag 被优化到寄存器中,可以理解为这个优化导致了内存不可见(对于 CPU 而言)。

而当我们给 flag 带上 volatile 关键词修饰时 volatile int flag = 0; ,进程又能够退出了。

在这里插入图片描述

所以我们可以理解为,volatile 关键字起到了防止编译器过度优化的作用,保持内存的可见性! 当 volatile 修饰一个变量时,即向编译器示意,不要将变量优化到寄存器中了,后续 CPU 对变量做检测时,都需要进行访存读取。


3. SIGCHLD 信号

在 进程等待 这篇文章中,我们曾说过,子进程退出后,会陷入僵尸状态,直到父进程对其进行回收。而进程等待的作用就是为了回收子进程,防止系统中的进程僵尸无人回收,导致内存泄漏。但由于父进程并不知道子进程何时会退出,所以父进程就需要一直通过 wait / waitpid 检测子进程的退出情况(采用阻塞式等待或者非阻塞轮询的方式)。

但是,当一个子进程退出时,它并不是 “静悄悄” 地退出走人的,而是会向父进程发送 SIGCHLD(17) 信号,这也是父进程等待检测子进程状态的重要标准,当父进程收到了来自子进程的 SIGCHLD 信号,便知道子进程退出了,然后对其进行回收。

对于所有的普通信号,只有 9 和 19 号信号不可被捕捉,因此我们可以通过捕捉 SIGCHLD(17) 信号 + 自定义信号处理动作,来证明子进程退出时,会向父进程发送 SIGCHLD(17) 信号。

void handler(int signo)
{
    cout << "process(" << getpid() << ") get a signal: " << signo << endl;
}

int main()
{
	signal(17, handler);
	pid_t id = fork();
	if(id == 0)
	{
		cout << "I am a child process, pid: " << getpid() << ", ppid: " << getppid() << endl;
		sleep(1);		// 子进程一秒后退出。
		cout << "child process quit!\n";
		exit(0);	
	}
	while(1)
	{
		cout << "I am a father process, pid: " << getpid() << endl;
		sleep(1);
	}
	return 0;
}

在这里插入图片描述

所以,现在我们就知道了,父进程在等待子进程时,可以采用基于信号的方式进行等待。但是还是需要使用 wait / waitpid 对子进程进行回收、释放资源等后续工作。

等待子进程的作用:

  • 获取子进程的退出状态,释放子进程的僵尸(如果基于信号等待,而不调用 wait / waitpid,那么子进程就不会被回收,处于僵尸状态)
  • 虽然无法得知父子进程谁先允许,但是一定是父进程最后退出

因此,基于信号的方式对子进程进行等待时,可以将 wait / waitpid 作为信号处理的一环,回收子进程。

void handler(int signo)
{
	pid_t rid = waitpid(-1, nullptr, 0);
    cout << "process(" << getpid() << ") get a signal: " << signo << endl;
}

对于 waitpid(-1, nullptr, 0); -1 是等待任意一个子进程。现在的问题是,假如父进程创建了 10 个子进程,并且它们同时退出呢?10 个子进程同时退出,同时向父进程发送 SIGCHLD(17) 信号,但是在 信号处理与捕捉 中,我们就已经说过了,当进程处于信号处理期间,是会自动屏蔽正在处理的那种信号的。那这样就导致了剩下的 9 个进程向父进程发送的信号就都被屏蔽了,最终导致只回收了一个进程(cpu速度快的话,最多收到两信号,回收两进程),那么剩下的那些僵尸进程该如何处理呢??

对于多个子进程同时退出的问题,可以采用循环的方式对子进程进行等待;考虑到可能并不是全部的子进程一起同时退出,而是随时退出,可能连着两三个,也可能一个一个退。因此在对子进程等待时应采用非阻塞轮询的方式,等不到子进程退出了,父进程就先返回,直到下一个子进程退出,向父进程发送信号,父进程再次捕捉信号、回收子进程。(如果采用阻塞式一直等待,那么当子进程随机退出时,父进程因等不到全部子进程退出而一直阻塞在信号处理 handler 方法中)

void handler(int signo)
{
	sleep(5);
	pid_t rid;
	while((rid = waitpid(-1, nullptr, WNOHANG)) > 0)	// 多进程同时退出也适用
    	cout << "process(" << getpid() << ") get a signal: " << signo << ", child process quit: " << rid << endl;
}

int main()
{
	srand(time(nullptr));
	signal(17, handler);
	for(int i = 0; i < 10; ++i)		// 创建 10 个子进程
	{
		pid_t id = fork();
		if(id == 0) { ... }
		sleep(rand() % 5 + 3);	// 模拟子进程随时退出
	}
	....
	return 0;
}

其实,子进程在终止时会给父进程发 SIGCHLD 信号,操作系统对该信号的默认处理动作是忽略。同时,父进程也可以自定义 SIGCHLD 信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程终止时会通知父进程,父进程在信号处理函数中调用 wait / waitpid 清理子进程即可。

事实上,由于 UNIX 的历史原因,,想不产生僵尸进程还有另外一种办法,即父进程调用 sigaction 将 SIGCHLD 信号的处理动作置为SIG_IGN,即 signal(17, SIG_IGN),这样 fork 创建出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用 sigaction 函数自定义的忽略通常是没有区别的,但这是一个特例(此方法对于Linux可用,但不保证在其它 UNIX 系统上都可用)。

在这里插入图片描述

现象:一次性创建10个子进程,当子进程退出时,并没有呈现僵尸昨天,而是直接被父进程自动回收。

查看 man 手册对 signal 的描述中提及了,SIGCHLD 的默认处理动作确实为 IGN(即忽略)。也即,以前在对子进程退出,即便父进程收到信号这件事,父进程对该信号的默认处理动作就是忽略(什么都不会做)。

在这里插入图片描述

但是,在介绍 进程等待 的时候,我们并不知道子进程退出时会给父进程发送信号这一回事,那时候对父进程收到的信号也没捕捉,但这并没有出现任何问题。所以问题就是: 以前没有对 SIGCHLD 信号做捕捉处理的时候,操作系统对它的处理就是忽略啊,那为什么那种忽略会导致进程僵尸了呢??而我们显式的对信号捕捉,处理动作依旧是 IGN signal(17, SIG_IGN),那为什么进程又不会僵尸了。所以都是 IGN ,为什么结果不一样呢??

其实,官方手册所描述的 SIGCHLD 信号的处理动作默认是忽略的,展开即是,它的处理动作依旧是 SIG_DFL,只不过这个默认动作里面,什么都不会做(即忽略处理)的意思。而我们显式的对信号捕捉,并将处理动作设置为 IGN,那它就是真正意义上直接跳过这个信号的处理。


如果感觉该篇文章给你带来了收获,可以 点赞👍 + 收藏⭐️ + 关注➕ 支持一下!

感谢各位观看!

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

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

相关文章

『VUE』20. 组件嵌套关系page(详细图文注释)

目录 VUE的自带组件结构新建文件搭建结构app与Main Header Aside结构App.vueHeader.vueMain.vueAside.vue Main 与Article.Aside与Item结构Article.vueItem.vue 总结 欢迎关注 『VUE』 专栏&#xff0c;持续更新中 欢迎关注 『VUE』 专栏&#xff0c;持续更新中 因为前面已经有…

【Hive sql面试题】找出连续活跃3天及以上的用户

表数据如下&#xff1a; 要求&#xff1a;求出连续活跃三天及以上的用户 建表语句和插入数据如下&#xff1a; create table t_useractive(uid string,dt string );insert into t_useractive values(A,2023-10-01 10:10:20),(A,2023-10-02 10:10:20),(A,2023-10-03 10:16…

【英特尔IA-32架构软件开发者开发手册第3卷:系统编程指南】2001年版翻译,2-16

文件下载与邀请翻译者 学习英特尔开发手册&#xff0c;最好手里这个手册文件。原版是PDF文件。点击下方链接了解下载方法。 讲解下载英特尔开发手册的文章 翻译英特尔开发手册&#xff0c;会是一件耗时费力的工作。如果有愿意和我一起来做这件事的&#xff0c;那么&#xff…

Vulnhub靶机——DC-3

#环境准备 dc-3 虚拟机&#xff1a;网卡NAT模式 kali攻击机&#xff1a;网卡NAT模式 #信息收集 nmap轻车熟路扫一下dc3的地址&#xff0c;识别出joomla系统 面向百度渗透&#xff0c;得知有一个joomla的扫描器&#xff0c;直接安排上&#xff0c;这下有版本号和后台登录地址…

数据结构:七种排序及总结

文章目录 排序一插入排序1直接插入排序2希尔排序二选择排序3直接选择排序4堆排序三 交换排序5冒泡排序6快速排序四 归并排序7归并排序源码 排序 我们数据结构常见的排序有四大种&#xff0c;四大种又分为七小种&#xff0c;如图所示 排序&#xff1a;所谓排序&#xff0c;就是…

基于STM32H7XX的Bootloader启动与FOTA

1. Bootloader是如何工作的: 2.正常情况下,程序从flash启动时的启动流程,如下图所示: 首先程序从flash启动,根据中断向量表找到复位中断处理函数的地址(0x0800 0004处是中断向量表的起始地址,记录了复位中断处理函数的地址)。执行复位中断处理函数,初始化系统环境之后…

语音 AI 迎来爆发期,也仍然隐藏着被低估的机会丨RTE2024 音频技术和 Voice AI 专场

在人工智能快速发展的今天&#xff0c;语音交互技术正经历一场革命性的变革。从语音识别到语音合成&#xff0c;再到端到端的语音对话系统&#xff0c;这一领域的创新正以前所未有的速度推进。这些进步不仅提升了技术指标&#xff0c;更为实时翻译、虚拟数字人、智能客服等实时…

【自学笔记】神经网络(1)

文章目录 介绍模型结构层&#xff08;Layer&#xff09;神经元 前向传播反向传播Q1: 为什么要用向量Q2: 不用激活函数会发生什么 介绍 我们已经学习了简单的分类任务和回归任务&#xff0c;也认识了逻辑回归和正则化等技巧&#xff0c;已经可以搭建一个简单的神经网络模型了。 …

在内蒙考驾照需要注意什么呢?

一、报名条件 年满18周岁&#xff0c;具有完全民事行为能力的中国公民。持有有效的身份证明文件。身体健康&#xff0c;无妨碍驾驶机动车的疾病&#xff0c;并需要通过体检。 二、选择驾校 可以先向身边已经拿到驾照的朋友咨询&#xff0c;了解驾校的距离、位置、口碑等信息。…

C++builder中的人工智能(8)什么是神经网络中的恒等激活函数?

在这篇文章中&#xff0c;我们将回答以下问题&#xff1a; 你想了解神经网络中最简单的激活函数是什么吗&#xff1f;什么是恒等函数&#xff1f;作为AI术语&#xff0c;我们需要了解激活函数和转移函数的哪些知识&#xff1f;激活函数与净输入函数是一回事吗&#xff1f;是否…

django图书管理系统-计算机毕业设计源码00648

摘要 图书管理系统在数字化阅读趋势、图书馆自动化管理、用户体验需求和信息技术应用等方面具有重要的研究意义。图书馆自动化管理系统的引入和应用提高了图书借阅过程的效率和准确性&#xff0c;减少了对手工操作和纸质记录的需求。用户对系统的易用性、查询速度、借还流程有更…

文件系统和日志管理

文件系统 文件系统&#xff1a; 文件系统提供了一个接口&#xff0c;用户用来访问硬件设备硬件设备上对文件的管理 存储单位 文件存储在硬盘上&#xff0c;硬盘最小的存储单位是512字节 扇区&#xff0c;文件在硬盘上的最小存储单位&#xff1a;块block&#xff0c;一个块的…

【代码转换】如何用 GPT 将 Python2代码 转换为 Python3代码 :实战教程

文章目录 1. 为什么要将 Python 2 代码迁移到 Python 3&#xff1f;2. 使用 ChatGPT 进行代码转换的步骤步骤1&#xff1a;打开CodeMoss步骤2&#xff1a;在输入框输入符号&#xff0c;选择代码转换步骤3&#xff1a;在这里选择你要更改的具体代码步骤4&#xff1a;准备 Python…

「Mac畅玩鸿蒙与硬件27」UI互动应用篇4 - 猫与灯的互动应用

本篇将带领你实现一个趣味十足的互动应用&#xff0c;用户点击按钮时猫会在一排灯之间移动&#xff0c;猫所在的位置灯会亮起&#xff08;on&#xff09;&#xff0c;其余灯会熄灭&#xff08;off&#xff09;。应用会根据用户的操作动态更新灯光状态和文本提示当前亮灯的位置&…

ES海量数据插入如何优化性能?

2024年10月NJSD技术盛典暨第十届NJSD软件开发者大会、第八届IAS互联网架构大会在南京召开。百度文心快码总经理臧志分享了《AI原生研发新范式的实践与思考》&#xff0c;探讨了大模型赋能下的研发变革及如何在公司和行业中落地&#xff0c;AI原生研发新范式的内涵和推动经验。 …

OTFS基带通信系统(脉冲导频,信道估计,MP解调算法)

Embedded Pilot-Aided Channel Estimation for OTFS in Delay–Doppler Channels | IEEE Journals & Magazine | IEEE Xplore 一、OTFS通信系统 如下图简要概括了OTFS基带通信系统过程&#xff0c;废话不多说给出完整系统详细代码。 以下仿真结果基于四抽头信道 估计信道…

理解Web登录机制:会话管理与跟踪技术解析(二)-JWT令牌

JWT令牌是一种用于安全地在各方之间传递信息的开放标准&#xff0c;它不仅能够验证用户的身份&#xff0c;还可以安全地传递有用的信息。由于其结构简单且基于JSON&#xff0c;JWT可以在不同的系统、平台和语言间无缝传递&#xff0c;成为现代Web开发中不可或缺的一部分。 文章…

微积分复习笔记 Calculus Volume 1 - 4.8 L’Hôpital’s Rule

4.8 L’Hpital’s Rule - Calculus Volume 1 | OpenStax

用户流定义:绘制产品交互流程图

产品经理在进行产品设计时&#xff0c;经常利用交互流程图来提升团队的工作效率。这种流程图适用于传达方案、评审目标等需要团队协作的场景&#xff0c;使得视觉设计师、产品开发等团队成员能够迅速理解图示内容&#xff0c;节省了理解时间&#xff0c;有效提高了沟通效率。 …

Linux -- 冯诺依曼体系结构(硬件)

目录 概念 五大组成部分 为什么需要存储器&#xff1f; 计算机存储金字塔层状结构 为什么程序需要加载到内存中 概念 冯诺依曼体系结构是以数学家冯诺依曼的名字命名的一种计算机体系结构。这种体系结构是现代计算机的基础&#xff0c;它定义了计算机的基本组件及其相互…