Linux | 可重入函数 | volatile | SIGCHLD信号

news2024/9/27 21:20:56

文章目录

    • 可重入函数
    • volatile
      • volatile和const同时修饰变量
    • SIGCHLD信号

可重入函数

当一个函数可以被两个流调用,我们称该函数具有重入特征

如果一个函数被重入后可能导致内存泄漏的问题,我们称该函数为不可重入函数,反之,一个函数被重入后不会导致内存泄漏,我们称该函数为可重入函数。

将一个节点new_node插入单链表分为两步,首先记录要插入位置的前后两个节点,prev和next,第一步是将new_node的next指针指向next节点,第二步时将prev的next指针指向new_node。
在这里插入图片描述
假设现在的main执行流正在执行insert函数,将new_node1插入单链表,在insert执行完第一步——将new_node1的next指针指向next节点后,由于进程收到一个信号,需要递达该信号,并且递达方式为自定义,包含insert函数,该insert插入一个节点new_node2,并且插入的位置的new_node1相同,信号的递达完成,new_node2被插入链表,进程将返回执行handler之前的代码,即执行insert的第二步,将prev节点的next指向new_node1,这样一来new_node1也被插入链表,由于两节点插入的位置一样,现在的实际情况是只有new_node1被插入链表,而new_node2在链表之外,这是一个明显的内存泄漏问题。
在这里插入图片描述
由于insert存在内存泄漏问题,所以insert是一个不可重入函数

但是可重入函数和不可重入函数函数没有优劣之分,是否能重入只是区别两个函数的方式,我们学习的大部分函数都是不可重入函数,STL库中的函数基本都是不可重入函数。由于函数重入导致的内存泄漏问题也只是在多个执行流执行进程时才会发生。

volatile

volatile属于C语言的易变关键字,其作用是告诉编译器该关键字修饰的变量可能发生变化,每次读取该变量的值时必须从内存中读取,以得到一个正确的值,防止编译器做自以为是的优化。

经过了信号的学习,可以通过一段有关信号的代码验证volatile的作用

#include <stdio.h>
#include <signal.h>

volatile int flag = 0;

// 自定义handler将flag的值设置为1
void handler(int signo)
{
    flag = 1;
    printf("flag->1\n");
}

int main()
{
    // 设置2号信号的handler方法
    signal(2, handler);
    // 如果flag的值为0,程序会卡在死循环中
    while (!flag);
    // flag的值不为0,程序会走到printf,打印语句
    printf("进程正常退出\n");    
    return 0;
}

这段demo根据flag的值决定程序是否会死循环,如果flag的值为0,程序死循环,flag的值不为0,程序会正常退出。flag作为一个全局变量,其初始值为0,如果不修改flag的值,程序会陷入死循环,这里可以通过捕捉2号信号,并自定义2号信号的handler方法,将flag的值设置为1,并打印"flag->1\n"这条语句。

所以一开始的程序会死循环,当按下Ctrl+C时,就意味着向前台进程发送2号信号,也就是将flag的值设置为1,程序退出死循环,打印"进程正常退出\n"这条语句。

运行这段demo,运行结果和预期相同
在这里插入图片描述
gcc编译器有一些高优化级别的选项,-O2表示以较高的优化级别编译这段demo,带上这个选项后,再运行程序,通过截图可以看出不论发送多少次2号信号,程序都不能退出死循环,也就是说程序认为flag的值依然为0。
在这里插入图片描述
导致这样结果的原因是:while信号的判断条件是!flag,也就是说判断条件只需要进行单纯的值判断,不需要进行计算,而while循环之前也没有对flag的值进行修改(signal只是设置了信号的自定义handler方法,虽然方法修改了flag的值,但编译器只能做语法检测,不能做逻辑判断,因此编译器认为在while之前,flag的值没有被修改。而循环需要不断的读取flag的值,每次从内存中读取flag的速度比每次从寄存器中读取的速度慢很多,而flag的值又没有修改,因此编译器将flag的值优化到寄存器中,这样每次的读取就能从寄存器中读取,有效的提高了程序运行的速度。

所以在寄存器中,flag的值始终为0,即使2号信号的自定义handler方法将内存中的flag值修改为1,由于cpu没有重新向内存中读取数据,所以寄存器的flag值不会因为内存的flag值改变。因此不论进程收到多少次2号信号,flag的值都为0,程序一直卡在死循环中。

所以编译的优化使得进程屏蔽了内存上的flag值,进程只可见寄存器上的flag值。而volatile的作用就是使得内存可见,强制程序每次读取flag的值都要从内存中读取。

在对flag添加了volatile修饰后,重新编译程序并带上-O2选项,可以看到,由于volatile对flag的修饰,在编译器的高优化下,2号信号的handler方法依然可以修改fla的值使程序退出死循环。
在这里插入图片描述

volatile和const同时修饰变量

volatile的作用是告诉编译器其修饰的变量很可能发生变化,需要编译器从内存中读取变量的值,而const的作用是告知编译器其修饰的变量不能被修改,如果有代码对const修饰的变量进行修改,编译将报错。

因此,const对于编译器来说,只需要在编译器期间检查const修改的变量是否被修改即可,const属于一种编译时就能完成语法检查的关键字。而volatile则是在程序运行起来后,才能实现其作用的关键字,编译器在编译期间无法对volatile修饰的变量做什么语法检查。

而volatile和const一起修饰一个变量,保证了该变量在后续代码中不会被修改,而在运行期间,其可能被修改,而编译器总是需要从内存中读取它的值,保证了程序不会过度优化出错。

SIGCHLD信号

当父进程fork创建子进程后,子进程退出时,会向父进程发送SIGCHLD信号,以表示子进程的退出。对于SIGCHLD信号,进程的递达方式为忽略,所以子进程的退出总是默默的,但我们可以捕捉父进程的SIGCHLD信号,并设置其自定义handler方法,以观察子进程的退出
在这里插入图片描述

void handler(int signo)
{
    cout << "父进程接收到子进程退出的信号:" << signo << "父进程pid:" << getpid() << endl;
}

int main()
{
    signal(SIGCHLD, handler);
    pid_t id = fork();
    if (id == 0)
    {
        // child
        while (1)
        {
            cout << "我是子进程,pid:" << getpid() << endl;
            sleep(1);
        }
        exit(1);
    }
    // parent
    while (1)
    {
        cout << "我是父进程,pid:" << getpid() << endl;
        sleep(1);
    }
    return 0;
}

在这里插入图片描述
19号信号:暂停一个进程,18号信号:唤醒一个进程
在这里插入图片描述
可以看到,除了子进程的退出,子进程的暂停与继续都会向父进程发送SIGCHLD信号

之前回收子进程资源时,总是父进程调用waitpid以阻塞或者非阻塞的方式,提前进行子进程的等待,经过了信号的学习,我们就可以通过捕捉子进程退出时向父进程发送的SIGCHLD信号,并自定义handler方法使父进程调用waitpid回收子进程资源。

// mypro.cc
void handler(int signo)
{
	// 第一个参数为-1表示等待任意的子进程
    pid_t id = waitpid(-1, nullptr, 0);
    if (id > 0) // 等待成功
    {
        cout << "父进程成功地回收了子进程资源," << "父进程pid:" << getpid() << endl;
    }
}

int main()
{
    signal(SIGCHLD, handler);
    pid_t id = fork();
    if (id == 0)
    {
        // child
        int cnt = 5;
        while (cnt)
        {
            cout << "我是子进程,pid:" << getpid() << "cnt:" << cnt << endl;
            cnt--;
            sleep(1);
        }
        exit(1);
    }
    // parent
    while (1)
    {
        cout << "我是父进程,pid:" << getpid() << endl;
        sleep(1);
    }
    return 0;
}

修改代码,在父进程对SIGCHLD的handler方法中添加对子进程的回收。运行以上demo
在这里插入图片描述
但是如果父进程有很多的子进程,这些子进程同时退出,也就是说很多子进程同时向父进程发送SIGCHLD信号,当进程在递达一个信号时,会将该信号加入信号屏蔽字,也就是阻塞该信号,所以进程在递达信号期间最多只会收到一次信号。这就造成了有些子进程虽然退出了,但是SIGCHLD信号没有被父进程接收,导致其资源无人回收,因此子进程陷入僵尸状态,无法释放资源,造成了内存泄漏。


using namespace std;

void handler(int signo)
{
    pid_t id = waitpid(-1, nullptr, 0);
    if (id > 0) // 等待成功
    {
        cout << "父进程成功地回收了子进程资源," << "父进程pid:" << getpid() << endl;
    }
}

int main()
{
    signal(SIGCHLD, handler);
    // 创建10个子进程,它们的退出时间可能会重叠
    for (int i = 0; i < 10; i++)
    {
        pid_t id = fork();
        if (id == 0)
        {
            // child
            int cnt = 5;
            while (cnt)
            {
                cout << "我是子进程,pid:" << getpid() << " cnt:" << cnt << endl;
                cnt--;
                sleep(1);
            }
            exit(1);
        }
    }

    // parent
    while (1)
    {
        cout << "我是父进程,pid:" << getpid() << endl;
        sleep(1);
    }
    return 0;
}

在这里插入图片描述
由于子进程在同一时间退出,可能造成有的子进程SIGCHLD信号没有被父进程接收到的情况,所以SIGCHLD的handler方法不能只回收一次子进程资源,应该多次调用waitpid回收子进程,并清理处于僵尸状态的子进程,当还有子进程在运行却没有退出时,父进程不再调用waitpid,至此完成了一次handler方法,信号递达完成,父进程可以去执行自己的代码了。

在这里插入图片描述
在这里插入图片描述

如果程序没有对SIGCHLD进行自定义handler,系统对SIGCHLD的默认处理方式是忽略,当子进程退出时,会陷入僵尸状态等待资源被释放,以上demo运行结果,以及运行期间有关的进程情况
在这里插入图片描述
在这里插入图片描述
父进程调用signal设置SIGCHLD信号的递达方法为系统提供的SIG_IGN后,子进程退出时将不再向父进程发送SIGCHLD信号,而是直接退出并释放资源,不会陷入僵尸状态。

系统对SIGCHLD信号的默认递达方法SIG_DFL,在该递达方法下,进程依然接收SIGCHLD信号,只是不对其做处理,当SIGCHLD信号的递达方法被设置为SIG_IGN时,系统会对SIGCHLD进行特殊处理,使该进程的子进程退出时不再发送SIGCHLD信号,并且退出时会自动释放资源,不会陷入僵尸态。
在这里插入图片描述

在这里插入图片描述
运行结果,子进程没有陷入僵尸态

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

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

相关文章

BER转Q

BER转Q Q(2^0.5)*erfcinv(2*BER) Q_dB20*log10(Q) 1、为什么要这样转&#xff1a; 暂时我也不知道&#xff0c;知道了再来补 2、关于erfcinv&#xff1a; yerf(x) 误差函数 yerfc(x) 互补误差函数 yerfinv(x) 逆误差函数(误差函数的反函数) yerfcinv(x) 逆互补误差函数(互补误差…

测试网络、磁盘使用情况和最大性能

1、测最大网络带宽&#xff0c;当前流量 查看网卡信息&#xff1a;ethtool p2p1 最简单的方法是用scp复制一个大文件&#xff0c;例如50G&#xff0c;复制时间要长&#xff0c;至少30分钟。之前在数据库迁移时&#xff0c;发现网速对迁移速度导致了重大影响&#xff0c;我们的…

基于go-micro微服务的实战-Gateway网关层的限流降级(八)

基于go-micro微服务的实战-Gateway网关层的限流降级(八) 文章最后附带完整代码 这一节主要是在Gateway网关层&#xff0c;基于go-micro的装饰器引入限流和降级。限流降级用的是开源库hystrix,类似java的hystrix&#xff0c;这里不做具体介绍和使用&#xff0c;可自行查看文档。…

车载ECU嵌入式设备的诊断测试 – DTC

作者 | 李伟 上海控安安全测评中心安全测评部总监 来源 | 鉴源实验室 01 DTC-Diagnostic Trouble Code&#xff08;诊断故障代码&#xff09; 车辆在运行的过程当中&#xff0c;控制器会监控状态&#xff0c;特定故障发生时控制器会记录这些故障。车辆送4S店进行维修保养时&…

Numpy入门[4]——数组类型

Numpy入门[4]——数组类型 参考&#xff1a; https://ailearning.apachecn.org/ 使用Jupyter进行练习 import numpy as np之前已经看过整数数组和布尔数组&#xff0c;除此之外还有浮点数数组和复数数组。 复数数组 a np.array([1 1j , 2 , 3 , 4]) aarray([1.1.j, 2.0.j, …

Java基于PHP+MySQL干洗店管理系统的设计与实现

干洗店管理系统是信息时代的产物,它是干洗店管理的一个好帮手。有了它不再需要繁重的纸质登记,有了它干洗店管理员不在需要繁重的工作,一些收费标准和干洗业务等基本信息可以由管理人员及时的对信息进行查询、更新、修改和删除,方便简易,且时效性高。 干洗店管理系统是一个典型…

java 中使用BigDecimal 解决科学计数法问题

一 BigDecimal的Api 1.1 常用方法介绍 ROUND_CEILING 向正无穷方向舍入 ROUND_DOWN 向零方向舍入 ROUND_FLOOR 向负无穷方向舍入 ROUND_HALF_DOWN 向&#xff08;距离&#xff09;最近的一边舍入&#xff0c;除非两边&#xff08;的距离&#xff09;是相等,如果是…

在python 深度学习Keras中计算神经网络集成模型

神经网络的训练过程是一个挑战性的优化过程&#xff0c;通常无法收敛。最近我们被客户要求撰写关于深度学习的研究报告&#xff0c;包括一些图形和统计输出。 这可能意味着训练结束时的模型可能不是稳定的或表现最佳的权重集&#xff0c;无法用作最终模型。 解决此问题的一种…

MyBatis-Plus

MyBatis-Plus 1、简介 MyBatis-Plus&#xff08;简称 MP&#xff09;是一个 MyBatis 的增强工具&#xff0c;在 MyBatis 的基础上只做增强不做改变&#xff0c;为简化开发、提高效率而生。 润物无声 只做增强不做改变&#xff0c;引入它不会对现有工程产生影响&#xff0c;…

数据结构学习笔记(Ⅷ):排序

目录 1 排序基础 1.1 排序的基本概念 2 排序算法 2.1 插入排序 1.思想 2.实现 3.效率分析 4.优化 2.2 希尔排序 1.定义 2.实现 3.效率分析 3 交换排序 3.1 冒泡排序 1.定义 2.实现 3.效率分析 3.2 快速排序 1.算法思想 2.实现 3.效率分析 4 选择排序 4.…

第4章 SpringBoot与Web应用

文章目录第4章 SpringBoot与Web应用4.1 配置Tomcat运行4.2 https安全访问4.3 数据验证4.4 配置错误页4.5 全局异常处理4.6 文件上传4.6.1 基础上传4.6.2 上传文件限制4.6.3 上传多个文件4.7 拦截器4.8 AOP拦截器4.9 本章小结4.9 本章小结第4章 SpringBoot与Web应…

[附源码]计算机毕业设计病人跟踪治疗信息管理系统Springboot程序

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

Linux系统移植二:生成fsbl引导文件并制作BOOT.bin

前情提要 对于ZYNQ而言&#xff0c;在引导过程中&#xff0c;先运行FSBL来设置PS&#xff0c;然后运行U-Boot用于加载Linux内核映像并引导Linux Linux系统移植一&#xff1a;移植U-BOOT 添加自己的板子并编译&#xff08;非petalinux版&#xff09; 一文中已成功生成了u-boot…

基于MPPT的PV光伏发电simulink建模和仿真

目录 1.算法描述 2.matlab算法仿真效果 3.MATLAB核心程序 4.完整MATLAB 1.算法描述 MPPT控制器的全称是“最大功率点跟踪”&#xff08;Maximum Power Point Tracking&#xff09;太阳能控制器&#xff0c;是传统太阳能充放电控制器的升级换代产品。MPPT控制器能够实时侦测…

ManiSkill 2022机器学习顶会ICLR上的世界顶尖机械臂大赛赛题解读,演示轨迹转换,点云查看

1.赛事相关信息 点击查看 2.赛题分析 软体对GPU要求较高&#xff0c;环境配置复杂&#xff0c;选择刚体环境先以模仿学习/强化学习的刚体环境为基础&#xff0c;后期再考虑无限制刚体环境部分任务&#xff08;如将物块移动到指定位置&#xff09;&#xff0c;存在相机之外的…

Day818.电商系统的分布式事务调优 -Java 性能调优实战

电商系统的分布式事务调优 Hi&#xff0c;我是阿昌&#xff0c;今天学习记录的是关于电商系统的分布式事务调优。 一个线上事故&#xff0c;在一次 DBA 完成单台数据库线上补丁后&#xff0c;系统偶尔会出现异常报警&#xff0c;开发工程师很快就定位到了数据库异常问题。 具…

SQL通用语法与DDL操作

学习笔记 sql通用语法 1 sql语句可以单行或多行书写&#xff0c;以分号结尾&#xff1b; 2 sql语句可以使用空格/缩进来增强语句的可读性&#xff1b; 3 mysql数据库的sql语句不区分大小写 4 单行注释&#xff1a;-- 内容 或 # 内容 多行注释&#xff1a; /* 内容 */ sql语句…

【地图之vue-baidu-map】点击获取坐标(点Marker)、坐标集(多边形polygon)

点击获取坐标&#xff08;点Marker&#xff09; 官网链接&#xff1a;Vue Baidu Map 需求 1.点击某点设置该点为中心点 2.获取点的经纬度 3.确定选取成功&#xff0c;取消就不赋值。 实现步骤 第一步&#xff1a;设置打开弹窗的地方 <el-button click"clickAdd…

c# .net 树莓派/香橙派用到物联网包Iot.Device.bindings 支持设备说明文档

c# .net 树莓派&#xff08;进口&#xff0c;贵&#xff09;/香橙派&#xff08;国产&#xff0c;功能相同&#xff0c;性价比高&#xff09;用到物联网包Iot.Device.bindings 支持设备说明文档 我们c# .net 开发树莓派/香橙派都需要用到Iot.Device.bindings和System.Device.G…

阿里巴巴 Github 星标 57.9KJava 面试突击汇总(全彩版)首次公开

现在互联网大环境不好&#xff0c;互联网公司纷纷裁员并缩减 HC&#xff0c;更多程序员去竞争更少的就业岗位&#xff0c;整的 IT 行业越来越卷。身为 Java 程序员的我们就更不用说了&#xff0c;上班 8 小时需要做好本职工作&#xff0c;下班后还要不断提升技能、技术栈&#…