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,插入完后,信号处理完毕 返回回我们之前被中断的位置 继续运行 最后head由node2又指向了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或者signal将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的⼦进程在终⽌时会⾃动清理掉,不会产⽣僵⼫进程,也不会通知⽗进程。

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

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

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

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

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

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

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

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

相关推荐
雯宝6 小时前
mnt_init 函数
linux
律宏阔12 小时前
WSL Docker 端口明明空闲,但就是绑不上端口
linux·windows
律宏阔12 小时前
WSL 突然断网,无法 ping 内网或外网
linux·windows
穷人小水滴12 小时前
用容器编译 VirtualBox 虚拟机软件 (ArchLinux, podman)
linux·容器·virtualbox
乱码三千12 小时前
如何优雅地直连无公网 IP 的远程 GPU 服务器
linux·人工智能·程序员
yunwei3712 小时前
使用 eBPF 跟踪 Nginx 请求
linux·后端·性能优化
yunwei3712 小时前
使用 eBPF 跟踪 MySQL 查询
linux·后端·性能优化
yunwei3712 小时前
eBPF 示例教程:使用 XDP 捕获 TCP 信息
linux·后端·性能优化
GeW12 小时前
制造业高端化,为什么必须重估Linux和数据库?
linux
GeW12 小时前
工业场景中的Linux与数据库选型:高端化转型的技术底座
linux