【Linux】信号处理 --- 可重入函数、volatile、SIGCHLD信号(补充篇)

👦个人主页:Weraphael

✍🏻作者简介:目前正在学习c++和算法

✈️专栏:Linux

🐋 希望大家多多支持,咱一起进步!😁

如果文章有啥瑕疵,希望大佬指点一二

如果文章对你有帮助的话

欢迎 评论💬 点赞👍🏻 收藏 📂 加关注😍


目录

  • 一、可重入函数
  • 二、volatile关键字
  • [三、SIGCHLD 信号](#三、SIGCHLD 信号)
      • [3.1 概念](#3.1 概念)
      • [3.2 基于信号的方式回收子进程](#3.2 基于信号的方式回收子进程)
      • [3.3 多个子进程回收](#3.3 多个子进程回收)
      • [3.4 基于3.3更加优雅的方案](#3.4 基于3.3更加优雅的方案)
  • 四、相关代码

一、可重入函数

  • main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的 时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换sighandler函数。

  • sighandler也调用insert函数向同一个链表head中插入节点node2

  • 插入操作的两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续往下执行,也就是开始执行head=p。当做完这步时,我们发现:只有一个节点node1真正插入链表中了,而另一个节点丢失了,最终导致了内存泄漏了。

因此,我们可以得出以下概念

  • 可以被重复进入的函数称为可重入函数

  • 如果一个函数被重复进入的情况下,出现出错了或者可能出错,那么我们称这个函数叫做不可重入函数

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

  • 调用了内存管理相关函数,诸如mallocnew等。
  • 调用了标准I/O库函数#include <iostream>,因为其中很多实现都以不可重入的方式使用数据结构。

了解概念即可,在线程部分我们会经常提到 ~

二、volatile关键字

借助全局变量flag设计一个死循环的场景,在此之前将2号信号进行自定义动作捕捉,具体动作为:将flag改为 1,可以终止main函数中的循环体

cpp 复制代码
#include <signal.h>
#include <iostream>
using namespace std;

int flag = 0;

void handler(int signum)
{
    cout << "捕捉" << signum << "号信号" << endl;
    flag = 1;
}

int main()
{
    signal(2, handler);
    while (!flag)
    {
        ;
    }

    cout << "进程正常退出" << endl;
    return 0;
}

【程序结果】

一切都符合我们的预期。2号信号发出后,循环结束,程序正常退出。

这段代码能符合我们预期般的正确运行是因为当前编译器默认的优化级别很低,没有出现意外情况。

那什么是意外情况呢,往下看这四点:

  • handlermain属于两个不同的执行流,即它们之间并没有调用和被调用关系。

  • 因此,编译器在编译main函数时,发现没有对flag变量进行修改,而while循环仅仅是对flag做检测,并且这个检测是在做逻辑运算,那么就要将flag值加载到CPU寄存器上,然后再CPU进行计算。

  • 通过以上两个条件:① flag值被加载到CPU寄存器上。② flag值没有被修改。那么有可能编译器在编译的时候会对flag进行优化。

  • 在优化条件下,flag变量可能被直接优化到CPU内的寄存器中,这样循环中对flag的检查将只会读取寄存器中的值,而不是每次都从内存中读取。这种优化是为了提高程序的性能。注意:内存中也会为flag变量开辟空间

那么在Linux中如何进行优化呢?

通过指令查询gcc优化级别的相关信息

bash 复制代码
# g++和gcc都可以
man g++
/-O1

数字越大,优化级别越高。注:默认优化级别是-O0

让我们重新编译上面的程序,并指定优化级别为O1

编译成功后,再次运行程序

此时得到了不一样的结果:这是因为编译器做了优化。2号信号捕捉后,将flag变量修改为1,这个修改只是在内存中修改,并没有在CPU的寄存器修改。而每次循环中对flag的检查只会读取寄存器中的值,那么while循环判断始终为真,导致死循环。因此完全符合我们以上的理论。

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

volatile是一个关键字。主要有两个方面的作用:

  1. 通常用于告诉编译器,某个变量可能会在程序的其他地方被意外修改,因此编译器不应该对这个变量进行优化。
  2. 保证内存的可见性。
  3. 总结一句话:防止编译器过度优化,保证内存的可见性。

所以我们代码改为如下:

cpp 复制代码
#include <signal.h>
#include <iostream>
using namespace std;

volatile int flag = 0;

void handler(int signum)
{
    cout << "捕捉" << signum << "号信号" << endl;
    flag = 1;
}

int main()
{
    signal(2, handler);
    while (!flag)
    {
        ;
    }

    cout << "进程正常退出" << endl;
    return 0;
}

【程序结果】

三、SIGCHLD 信号

3.1 概念

在进程控制时,不可否认的是:父进程必须等待子进程退出后回收子进程,主要是避免子进程变成僵尸进程占用系统资源、造成内存泄漏

那么父进程是如何知道子进程退出了呢?

在之前的讲解中,父进程waitpid()要么就是设置为阻塞式专心等待,要么就是设置为WNOHANG非阻塞式等待,这两种方法都需要父进程主动去检测子进程的状态

如今学习了进程信号相关知识后,可以思考一下:难道子进程真的是静悄悄的退出的吗?还是真的依靠父进程的每次询问?

答案当然不是,子进程在退出后,会向父进程发送17号信号SIGCHLD。子进程而是通过发送17号信号通知父进程,我要退出了。这样可以解放父进程,不必再去主动检测。

我们可以验证一下,当子进程退出是否真的有17号信号?

cpp 复制代码
#include <signal.h>
#include <unistd.h>
#include <iostream>
using namespace std;

void handler(int signum)
{
    cout << "who am I: " << getpid()
         << ", catch a signum: " << signum << endl;
}

int main()
{
    signal(17, handler);

    pid_t id = fork();
    if (id == 0) // 子进程
    {
        while (true)
        {
            cout << "I am child: " << getpid() << ", ppid: " << getppid() << endl;
            sleep(1);
            break;
        }
        exit(17); // 打印一句后,子进程直接退出
    }
    // 父进程
    while (true)
    {
        cout << "I am father: " << getpid() << endl;
        sleep(1);
    }
    return 0;
}

【程序结果】

如上足可以证明SIGCHLD是被子进程发出的!

3.2 基于信号的方式回收子进程

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

回顾等待的好处:

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

所以还是要调用wait/waitpid这样的接口。并且基于信号的方式进行等待还要保证父进程不能退出。

接下来,我们可以将子进程等待方法写入到信号捕捉函数中!

cpp 复制代码
#include <signal.h>
#include <unistd.h>
#include <iostream>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;

void handler(int signum)
{
    pid_t rid = waitpid(-1, nullptr, 0);

    cout << "who am I: " << getpid()
         << ", catch a signum: " << signum
         << ", 子进程" << rid << "回收成功!" << endl;
}

int main()
{
    signal(17, handler);

    pid_t id = fork();
    if (id == 0) // 子进程
    {
        while (true)
        {
            cout << "I am child: " << getpid() 
            	<< ", ppid: " << getppid() << endl;
            sleep(5); 
            break;
        }
        cout << "子进程退出..." << endl;
        exit(17); // 打印一句后的5秒,子进程直接退出
    }
    // 父进程
    while (true) // 父进程不能退出
    {
        cout << "I am father: " << getpid() << endl;
        sleep(1);
    }
    return 0;
}

【程序结果】

3.3 多个子进程回收

虽然成功了,但还是有点缺陷。以上代码是在只有一个子进程的场景,代码没问题,但如果是涉及多个子进程回收时,这个代码就有问题了

原因是:SIGCHLD 是一个信号,在信号处理中我们验证过(如下)

如果多个子进程同时退出,可能只有多个进程中的某一个进程在执行handler方法,那么执行的过程中就已经将这个信号屏蔽了,导致(n - 1)个进程无法被回收,都是僵尸进程,造成内存泄漏。

  • 解决方案:自定义捕捉函数中,采取 while 循环式回收,有很多进程都需要回收没问题,排好队一个个来就好了,这样就可以确保多个子进程同时发出 SIGCHLD 信号时,可以做到一一回收

  • 细节:多个子进程运行时,可能有的退了,有的没退(不是同时退),这会导致退了的子进程发出信号后,触发自定义捕捉函数中的循环等待机制,回收完已经退出了的子进程后,会阻塞式的等待还没有退出的子进程,如果子进程一直不退,就会一直被阻塞,所以我们需要把进程回收设为 WNOHANG 非阻塞式等待

cpp 复制代码
void handler(int signum)
{
    pid_t rid;
    while ((waitpid(-1, nullptr, WNOHANG)) > 0)
    {
        cout << "who am I: " << getpid()
             << ", catch a signum: " << signum
             << ", 子进程" << rid << "回收成功!" << endl;
    }
}

3.4 基于3.3更加优雅的方案

其实还有一种更加优雅的子进程回收方案

由于 UNIX 历史原因,要想子进程不变成僵尸进程,可以把 SIGCHLD 的处理动作设为 SIG_IGN 忽略,这里的忽略是个特例,只是父进程不对其进行处理,转变成由操作系统对其负责,自动清理资源并进行回收。

也就是说,直接在父进程中使用 signal(SIGCHLD, SIG_IGN) 就可以优雅的解决子进程回收问题,父进程相当于啥也不用干(前提是你不想获取子进程的退出信息)。

注意:

  • SIGCHLD的默认处理动作SIG_DFL是忽略(什么都不做),而忽略动作SIG_IGN是让操作系统帮忙回收,父进程不必关心。
  • 此方法对于Linux可用,但不保证在其它UNIX系统上都可用

四、相关代码

本篇博客的代码:点击跳转

相关推荐
Mike!2 分钟前
C++进阶 set和map讲解
java·开发语言·数据结构·c++·set·map·cpp
翔云1234563 分钟前
Go语言的垃圾回收(GC)机制的迭代和优化历史
java·jvm·golang·gc
小O_好好学26 分钟前
Linux帮助命令
linux·运维·服务器
MXsoft61827 分钟前
监控易监测对象及指标之:Kubernetes(K8s)集群的全方位监控策略
运维
怒放的生命.33 分钟前
电气自动化入门05:三相异步电动机的正反转点动控制电路
运维·自动化·电气自动化·电工基础
莫泽Morze37 分钟前
VMware安装rustdesk服务器
运维·服务器
不见长安见晨雾39 分钟前
将Java程序打包成EXE程序
java·开发语言
六点半8881 小时前
【C/C++】速通涉及string类的经典编程题
c语言·开发语言·c++·算法
汤姆和杰瑞在瑞士吃糯米粑粑1 小时前
string类(C++)
开发语言·c++
卡戎-caryon1 小时前
【操作系统】01.冯·诺伊曼体系结构
服务器·笔记·操作系统·冯·诺伊曼体系结构