Linux下 可重入函数、volatile关键字和SIGCHLD信号

欢迎来到我的频道 【点击跳转专栏】

码云链接 【点此转跳】


信号相关内容(可点击转跳):

  1. Linux下 进程信号初识和信号的产生
  2. Linux下 信号的保存与捕捉【含中断、内核态用户态底层解析】
  3. Linux下 可重入函数、volatile关键字和SIGCHLD信号

文章目录

  • [1. 可重入函数](#1. 可重入函数)
  • [2. volatile](#2. volatile)
  • [3. SIGCHLD信号](#3. SIGCHLD信号)

1. 可重入函数

先看一段伪代码:

此时在main函数中 我们调用了我们的头插方法,将node1插入头节点 可是我们运气不好 刚执行完 insert函数里的 p->next=head;的时候 收到了信号 并触发了时钟中断 进入了信号捕捉流程 不过很不信的是 此时handler方法也要进行头插 插入node2,插入完后,信号处理完毕 返回回我们之前被中断的位置 继续运行 最后headnode2又指向了node1

此时我们发现 node2出现了丢失问题 即内存泄漏问题!

insert方法在被main函数调用时,再还没调用完时,进程捕捉到了信号,我们的信号捕捉流程也进入到了insert了。这种情况 我们可以叫作 insert函数被重入了!!

很明显 insert函数被重入 造成很严重的问题 所以该函数应该是 不可重入函数

如果函数重入不会出现问题 那么我们称为 函数是可重入函数!

⚠️: 关于函数重入或者不可重入,我么应该将其理解成函数的特点,而不是函数的优缺点!

2. volatile

cpp 复制代码
#include <iostream>
#include <signal.h>
#include <unistd.h>

int flag= 0;
void change(int signo)
{
    flag = 1;
    printf("change flag:0 ->1\n");
}

int main()
{
   signal(2,change);
    while (!flag);//flag只会被检测,不会被修改!
    printf("进程正常退出");

    return 0;
}

标准情况下,输⼊ CTRL-C ,2号信号被捕捉,执⾏⾃定义动作,修改 flag=1 , while 条件不满⾜, 退出循环,进程退出!

我们发现: 在main函数内部 如果没有2号信号 flag只会被检测,不会被修改!

于是就可能会产生这样的现象: falg是内存中的变量 flag=0 while内部需要检查flag是否为0 检查的本质也是运算 所以每次检测都需要把flag的值会读取到寄存器内部! 然后在cpu内部执行该检查!但是在部分编译器优化的情况 发现main执行流里面 flag没有被修改 于是编译器就会把while循环检测这段代码 转化成把flag变量转化成寄存器变量 这样每次判断就再也不需要访问内存了! 每次都是检测eax内的值!!

于是就有了个以下关键词 register int x = 1;建议把该变量x 优化到寄存器内 内存中会开辟该变量 但是内容还会放到寄存器内!于是用到该变量的时候就再也不需要访问内存了 只需要查看寄存器即可!!

但是如果上面的代码也这么优化的话 就发现哪怕 发出信号 flag被修改为1 但是程序依然没退出!!

gcc\g++给我们提供了默认的优化选项(了解即可):

  • -O0:无优化(默认)。生成与源码高度对应的代码,最适合开发阶段的断点调试。
  • -O1:基础优化。进行常量折叠、死代码消除等廉价且安全的优化,在不大幅增加编译时间的前提下提升运行速度。
  • -O2 :中级/标准优化(生产环境首选)。开启了几乎所有不涉及空间和时间折衷的安全优化,包括函数内联、循环展开、指令重排等。它在性能和稳定性之间取得了最佳平衡。
  • -O3 :激进优化。在 -O2 的基础上进一步启用自动向量化(SIMD)、大规模循环展开等极端提速手段。适合对性能要求极高的计算密集型任务,但可能导致生成的二进制文件体积变大,甚至引发某些边界条件下的
    bug。
  • -Os:体积优化。侧重于最小化可执行文件的体积,关闭了会导致代码膨胀的优化,非常适合存储空间受限的嵌入式系统或移动端应用。

于是我们用

cpp 复制代码
g++ -o testsig testsig.cc -O1

编译代码,执行后:

此时 编译器把flag优化成寄存器变量 此时检测falg是否为0 将不会再读取 内存中的flag 而是只检测 寄存器中的值!这种情况 我们称为:寄存器覆盖了内存,让内存不可见了!!

所以如果我们执意 相对代码进行优化 但是不想让flag被优化 于是 我们就可以用volatile(易变的;不稳定的)关键词进行修饰!!

相当于明确告诉编译器:"这个变量的值随时可能被改变,而且这种改变不是由当前程序代码引起的。"

因此,编译器每次使用该变量时,都必须老老实实地去内存中重新读取它的值,而不能为了提升性能把它缓存在 CPU 寄存器中,也不能对其进行各种激进的指令重排优化。

cpp 复制代码
volatile int flag = 0;

3. SIGCHLD信号

问题: 父进程创建子进程,如果子进程退出了,子进程是安静退出吗?

其实,⼦进程在终⽌时会给⽗进程发SIGCHLD信号!!!该信号的默认处理动作是忽略!!

问题: 怎么证明?

用下面代码,太简单了!就不解释了!!

cpp 复制代码
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>

void handler(int signo)
{
    printf("父进程获取信号:%d,pid:%d\n",signo,getpid());
}

int main()
{

    signal(SIGCHLD,handler);
    pid_t id = fork();
    if(id == 0)
    {
        printf("子进程退出,pid:%d\n",getpid());
        sleep(4);
        exit(10);
    }

    while(1)
    {
        printf("父进程在运行:Pid:%d\n",getpid());
        sleep(1);
    }
}

以后我们回收子进程,如果父进程也想同时做其他事情,并不想父进程因为 wait、waitpid 阻塞等待让父进程卡住,我们可以在父进程收到SIGCHLD信号后,用自定义的handler来回收子进程!

cpp 复制代码
void handler(int signo)
{
    printf("父进程获取信号:%d,pid:%d\n",signo,getpid());
    int status =0;
    waitpid(-1,&status,0);
    printf("status code:%d\n",WEXITSTATUS(status));
}

int main()
{
    signal(SIGCHLD,handler);
    pid_t id = fork();
    ....
}   

问题1: 如果是10个子进程,而且几乎同时退出呢??

此时只会有一个SIGCHLD信号被受理,其他进程的信号会被丢弃 那么就会造成有进程没能成功被回收,变成僵尸进程,所以可以用一个while循环不停的回收子进程 ,当无子进程时,则会报错传-1,不过缺点是一旦有一个子进程不退出 依然会一直阻塞住。

cpp 复制代码
void handler(int signo)
{
    printf("父进程获取信号:%d,pid:%d\n",signo,getpid());
    int status =0;
    while(1)
    {
    pid_t ret = waitpid(-1,&status,0);
    if(ret <= 0)
      break;
    }
    printf("status code:%d\n",WEXITSTATUS(status));
}

问题2: 如果是10个子进程,但是9个退出,1个没退出呢?

那么上面的代码 因为waitpid最后一个参数是0 表示是阻塞等待,所以如果 无子进程 会返回-1直接退!!但是一旦有那么一个不退,就会一直阻塞在waitpid这里!让进程被阻塞住!所以我们可以通过修改waitpid 最后一个参数来改善这种情况!

cpp 复制代码
void handler(int signo)
{
    printf("父进程获取信号: %d, pid: %d\n", signo, getpid());
    int status = 0;
    while (1)
    {
        pid_t ret = waitpid(-1, &status, WNOHANG);
        // ret > 0:成功
        // ret < 0:失败
        // ret == 0:waitpid函数调用是成功的,但是没有子进程退出!
        if(ret <= 0)
            break;
    }
    printf("status code: %d\n", WEXITSTATUS(status));
}

但是这么做 好麻烦啊 ! ! !我回收子进程大部分是为了查看其退出状态 如果我压根不关心呢?? 因为历史的一些原因,在类UNIX系统中有一种特性 就是让子进程退出的时候不产生僵尸进程!

⽗进程调 ⽤sigaction或者signalSIGCHLD的处理动作置为SIG_IGN,这样fork出来的⼦进程在终⽌时会⾃动清理掉,不会产⽣僵⼫进程,也不会通知⽗进程。

在父进程内 加上这段代码:

cpp 复制代码
signal(SIGCHLD,SIG_IGN);

问题: 但是SIGCHLD默认就是 忽略 为什么要手动设置?

当进程通过 signal()sigaction() 等系统调用,将某个信号的处理动作设置为 SIG_IGN 时,意味着告诉操作系统: 当该信号到达时,不要采取任何行动,直接将其丢弃。该信号的交付对进程或线程的执行流程没有任何影响,就好像这个信号从未发生过一样。

该处理行为会自动回收僵尸进程 :将子进程退出信号设置为忽略,即 signal(SIGCHLD, SIG_IGN);,内核会自动回收结束的子进程,从而防止产生僵尸进程。

虽然 SIGCHLD默认动作也是 忽略 但是其数字在内核是不同的 ! SIG_IGN在内核表示的是1,而信号默认的忽略虽然我们是不知道具体是几 但是可以肯定绝对不是1

虽然都叫 忽略 但是这两个忽略 含义不同 这么理解就行了!!

⚠️: 这种方法只能在类UNIX系统如:LinuxMacOS下有效!

相关推荐
qeen872 小时前
【Linux】Linux简单介绍与基本指令(上)
linux·运维·服务器·学习
tianyuanwo2 小时前
深入解析 RISC-V 虚拟化中的 UEFI 固件配置:从 XML 到 NVRAM 的生命周期管理
xml·linux·risc-v
道川贤林3 小时前
OrangePi 系统启动优先级修改
linux·linux驱动·orangepi·u-boot
xsc-xyc3 小时前
用 Tailscale + Syncthing 实现手机、电脑与 NAS 的跨网络文件同步
linux·网络·网络安全·智能手机·电脑
IsJunJianXin4 小时前
pdd小程序 cdp 保存响应体
linux·服务器·小程序·pdd小程序·拼多多响应体解密·小程序cdp·拼多多rpc取响应体
爱就是恒久忍耐4 小时前
现代CMake的build方式
linux·运维·服务器
古城小栈5 小时前
Python 的主流Ai框架为什么优先适配 Linux 系统?
linux·人工智能·python
盼小辉丶6 小时前
Ubuntu极速部署OpenClaw完全指南(本地模型+DeepSeek)
linux·ubuntu·openclaw
字节高级特工6 小时前
【Linux】C语言进程地址空间分布
linux·c++·后端·算法