【Linux】第三十九站:可重入函数、volatile、SIGCHLD信号

2024-01-29 16:52

本文主要是介绍【Linux】第三十九站:可重入函数、volatile、SIGCHLD信号,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

  • 一、可重入函数
  • 二、volatile
  • 三、SIGCHLD信号

一、可重入函数

如下图所示,当我们进行链表的头插的时候,我们刚刚执行完第一条语句的时候,突然收到一个信号,然后我们这个信号的自定义捕捉方法中,正好还有一个头插,于是这个执行流再次进入这个函数中。执行完毕以后,返回到原来的执行流中继续运行。

这种现象就是函数被重入

就会导致下面的现象。

image-20240127195244183

我们可以看到,这个node2结点丢失了,最终导致了内存泄漏了

insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为 不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。

上面的现象是这样的:

  1. insert函数被mainh和handler执行流重复进入
  2. 导致了结点丢失,内存泄漏

所以我们有了如下定义:

如果一个函数,被重复进入的情况下,出错了,或者可能出错。

我们就要把这个函数叫做不可重入函数

否则叫做可重入函数

目前我们用到的大部分函数都是不可重入的!

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

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

二、volatile

我们先看一下下面的代码

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstring>
using namespace std;int flag = 0;
void handler(int signo)
{cout << "catch a signal: " << signo <<endl;flag = 1;
}int main()
{signal(2, handler);while(!flag);cout << "process quit normal" << endl;return 0;
}

最终我们的结果如下

image-20240127202215532

一切都符合我们的预期

但是在极端情况下,由于main和handler属于两个执行流

编译器检测后发现这个flag没有发生过变化。检测的本质也是计算,逻辑运算,这里的逻辑反也是一种计算。

它会在优化条件下,flag变量可能被直接优化到CPU内的寄存器中。

如下所示,我们的g++可以通过带上O0~O3选项进行优化。后面的数字越大,优化级别越高

image-20240127202846501

如下所示,我们发现,如果是O0,就相当于没有优化,可以正常结束。如果是O1的话,那么此时就无法用二号信号退出了。

image-20240127203114223

如下所示,这是因为我们没有优化之前,CPU会不断的将内存中的数据放入到寄存器中。而我们使用2号信号修改了之后,也还是会不断的访存。所以这个flag会改变,所以就会跳出循环

image-20240127203617825

而现在,我们优化了之后,这个变量第一次拿到寄存器之后,就不再访存了,因为这样可以提高效率,就直接用寄存器当中的数据,而我们使用信号改掉的只是内存当中的数据。所以这里的运算就一直为真了。所以就不会退出了。

image-20240127204052935

这样因为优化,就如同形成了一个寄存器屏障。导致内存不可见了!

所以我们为了防止这样编译器的过度优化,我们可以给这个变量带上volatile关键字。

volatile int flag = 0; //防止编译器过度优化,保存内存的可见性

所以我们代码改为如下

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstring>
using namespace std;volatile int flag = 0;
void handler(int signo)
{cout << "catch a signal: " << signo <<endl;flag = 1;
}int main()
{signal(2, handler);while(!flag);cout << "process quit normal" << endl;return 0;
}

image-20240127204404438

三、SIGCHLD信号

我们之前用wait和waitpid函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻塞地查询是否有子进程结束等待清理(也就是轮询的方式)。采用第一种方式,父进程阻塞了就不 能处理自己的工作了;采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一 下,程序实现复杂。

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

我们可以先捕捉一下17号信号,验证一下是否真的有17号信号

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstring>
using namespace std;void handler(int signo)
{cout << "I am process: " << getpid() << ", catch a signo: " << signo << endl; 
}int main()
{signal(17, handler);pid_t id = fork();if(id == 0){while(true){cout << "I am child process: " << getpid() << ", ppid: " << getppid() << endl;sleep(1);break;}cout << "child quit...!!!" << endl;exit(0);}//fatherwhile(true){cout << "I am father process: " << getpid() << endl;sleep(1);}return 0;
}

运行结果为

image-20240127211307262

所以利用这个17号信号,我们可以采用基于信号的方式进行等待

等待的好处:

  1. 获取子进程的退出状态,释放子进程的僵尸
  2. 虽然不知道父子谁先运行,但是我们清楚,一定是father最后退出

所以我们还是要调用wait/waitpid这样的接口。而且father必须保证自己是一直在运行的。

所以我们可以试着把子进程等待写入到信号捕捉函数中!

如下代码所示:

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstring>
#include <sys/wait.h>
#include <sys/types.h>
using namespace std;void handler(int signo)
{sleep(3);pid_t rid = waitpid(-1, nullptr, 0);cout << "I am process: " << getpid() << ", catch a signo: " << signo << "child process quit: " << rid << endl; 
}int main()
{signal(17, handler);pid_t id = fork();if(id == 0){while(true){cout << "I am child process: " << getpid() << ", ppid: " << getppid() << endl;sleep(3);break;}cout << "child quit...!!!" << endl;exit(0);}//fatherwhile(true){cout << "I am father process: " << getpid() << endl;sleep(1);}return 0;
}

运行结果如下所示

image-20240127212515932

如果有十个进程呢??如果同时退出呢??如果退出一半呢??

如果是个进程同时退出,那么上面代码就有问题了,因为可能一个进程进程正在退出的时候,已经将这个信号屏蔽了,导致有很多进程无法被回收,全部都是僵尸进程了。

如下代码所示,我们在捕捉函数中循环等待,但是要主要加上非阻塞式。否则会一直卡在那里了。

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstring>
#include <time.h>
#include <sys/wait.h>
#include <sys/types.h>
using namespace std;void handler(int signo)
{sleep(3);pid_t rid;while((rid = waitpid(-1, nullptr, WNOHANG)) > 0){cout << "I am process: " << getpid() << ", catch a signo: " << signo << "child process quit: " << rid << endl; }
}int main()
{srand(time(nullptr));signal(17, handler);for(int i = 0; i < 10; i++){pid_t id = fork();if(id == 0){while(true){cout << "I am child process: " << getpid() << ", ppid: " << getppid() << endl;sleep(10);break;}cout << "child quit...!!!" << endl;exit(0);}sleep(rand() % 5 + 3);}//fatherwhile(true){cout << "I am father process: " << getpid() << endl;sleep(1);}return 0;
}

image-20240127214154334

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

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstring>
#include <time.h>
#include <sys/wait.h>
#include <sys/types.h>
using namespace std;
int main()
{signal(17, SIG_IGN);srand(time(nullptr));for(int i = 0; i < 10; i++){pid_t id = fork();if(id == 0){while(true){cout << "I am child process: " << getpid() << ", ppid: " << getppid() << endl;sleep(10);break;}cout << "child quit...!!!" << endl;exit(0);}sleep(rand() % 5 + 3);}//fatherwhile(true){cout << "I am father process: " << getpid() << endl;sleep(1);}return 0;
}

运行结果如下,可以看到是没有僵尸进程的

image-20240127214707488

这里需要注意的是,默认是SIG_DFL,它的动作是忽略。和SIG_IGN是不一样的!!!

这篇关于【Linux】第三十九站:可重入函数、volatile、SIGCHLD信号的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/657561

相关文章

Linux内核定时器使用及说明

《Linux内核定时器使用及说明》文章详细介绍了Linux内核定时器的特性、核心数据结构、时间相关转换函数以及操作API,通过示例展示了如何编写和使用定时器,包括按键消抖的应用... 目录1.linux内核定时器特征2.Linux内核定时器核心数据结构3.Linux内核时间相关转换函数4.Linux内核定时

Linux镜像文件制作方式

《Linux镜像文件制作方式》本文介绍了Linux镜像文件制作的过程,包括确定磁盘空间布局、制作空白镜像文件、分区与格式化、复制引导分区和其他分区... 目录1.确定磁盘空间布局2.制作空白镜像文件3.分区与格式化1) 分区2) 格式化4.复制引导分区5.复制其它分区1) 挂载2) 复制bootfs分区3)

pandas使用apply函数给表格同时添加多列

《pandas使用apply函数给表格同时添加多列》本文介绍了利用Pandas的apply函数在DataFrame中同时添加多列,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习... 目录一、Pandas使用apply函数给表格同时添加多列二、应用示例一、Pandas使用apply函

Linux服务器数据盘移除并重新挂载的全过程

《Linux服务器数据盘移除并重新挂载的全过程》:本文主要介绍在Linux服务器上移除并重新挂载数据盘的整个过程,分为三大步:卸载文件系统、分离磁盘和重新挂载,每一步都有详细的步骤和注意事项,确保... 目录引言第一步:卸载文件系统第二步:分离磁盘第三步:重新挂载引言在 linux 服务器上移除并重新挂p

Python中Namespace()函数详解

《Python中Namespace()函数详解》Namespace是argparse模块提供的一个类,用于创建命名空间对象,它允许通过点操作符访问数据,比字典更易读,在深度学习项目中常用于加载配置、命... 目录1. 为什么使用 Namespace?2. Namespace 的本质是什么?3. Namesp

Linux下屏幕亮度的调节方式

《Linux下屏幕亮度的调节方式》文章介绍了Linux下屏幕亮度调节的几种方法,包括图形界面、手动调节(使用ACPI内核模块)和外接显示屏调节,以及自动调节软件(CaliseRedshift和Reds... 目录1 概述2 手动调节http://www.chinasem.cn2.1 手动屏幕调节2.2 外接显

Linux(centos7)虚拟机没有IP问题及解决方案

《Linux(centos7)虚拟机没有IP问题及解决方案》文章介绍了在CentOS7中配置虚拟机网络并使用Xshell连接虚拟机的步骤,首先,检查并配置网卡ens33的ONBOOT属性为yes,然后... 目录输入查看ZFhrxIP命令:ip addr查看,没有虚拟机IP修改ens33配置文件重启网络Xh

linux实现对.jar文件的配置文件进行修改

《linux实现对.jar文件的配置文件进行修改》文章讲述了如何使用Linux系统修改.jar文件的配置文件,包括进入文件夹、编辑文件、保存并退出编辑器,以及重新启动项目... 目录linux对.jar文件的配置文件进行修改第一步第二步 第三步第四步总结linux对.jar文件的配置文件进行修改第一步进

MySQL中如何求平均值常见实例(AVG函数详解)

《MySQL中如何求平均值常见实例(AVG函数详解)》MySQLavg()是一个聚合函数,用于返回各种记录中表达式的平均值,:本文主要介绍MySQL中用AVG函数如何求平均值的相关资料,文中通过代... 目录前言一、基本语法二、示例讲解1. 计算全表平均分2. 计算某门课程的平均分(例如:Math)三、结合

linux ssh如何实现增加访问端口

《linuxssh如何实现增加访问端口》Linux中SSH默认使用22端口,为了增强安全性或满足特定需求,可以通过修改SSH配置来增加或更改SSH访问端口,具体步骤包括修改SSH配置文件、增加或修改... 目录1. 修改 SSH 配置文件2. 增加或修改端口3. 保存并退出编辑器4. 更新防火墙规则使用uf