当前位置: 首页 > news >正文

[Linux——Lesson22.进程信号:信号保存 信号捕捉]

​​​​​​​

目录

前言

一、🤔核心转储

总结:

1️⃣核心转储的核心作用

2️⃣核心转储的启用与限制

二、🧐信号保存

2-1 🍕信号的三种状态

阻塞和忽略的区别:

三、 🤓信号集操作函数

3-1 🍟sigset_t类型接口

3-2 🌭sigprocmask接口

3-3 🍔sigpending接口

3-4 🥗使用场景及理解

四、🤨信号处理

4-1 🍞信号处理时间

4-2 🥪信号处理流程

五、😁捕捉信号的其他方式

5-1 🍿可重入函数

5-2 🥨volatile关键字

5-3 🍗SIGCHLD信号

总结与提炼

结束语


前言

上节内容我们初步了解了什么是信号,以及信号概念&信号产生

本章我们将继续学习Linux中信号的概念——阻塞信号、捕捉信号、可重入函数、volatile关键字、SIGCHLD信号等。🗝️


一、🤔核心转储

核心转储,又称核心 dump,是 Linux 系统在进程发生严重错误(如段错误、非法指令、浮点数异常等)时,将进程当前的内存镜像、寄存器状态、堆栈信息等关键数据保存到磁盘文件(默认文件名为core)的机制。它就像给故障进程 “拍了一张内存快照”,为后续的问题排查提供了关键依据。

在进程等待这一章节,有一张图我没有详细解释:

  当时在 进程等待 这一章节里我们并没有详细说明 Core dump标志,而我们通过man手册查看signal,会发现大部分的信号的作用都是 终止进程,而终止进程的动作却又分为 Core 和 Termtermination) 两个动作。

那么它们两个有什么区别呢?实际上,在云服务器上默认将进程core退出,进行了特殊的设定,默认core是关闭的

1️⃣查看core功能

 通过使用 ulimit -a 命令查看系统中的core 文件打开情况:

2️⃣打开core功能

  要打开core功能使用 ulimit -c core_size 命令打开core dump,其中 core_size 表示指定core文件大小

这个时候就开起了Linux的 Core dump 功能。当没有开起core dump功能时运行下面代码,会正常给出报错:

#include <iostream>
#include <unistd.h>
#include <signal.h>int main()
{int a = 10;a /= 0;// SIGFPE信号while(true) sleep(1);return 0;
}

未打开core dump功能时,正常报错,当打开core dump时:

 使用了core dump报错则会生成一个 core 文件,当然这个文件在不同的系统表现形式可能不同,在 ubuntu 下文件名为 core。而在 centos 下文件名为 core.pid 后面跟一串数字,这串数字是报错进程的进程pid。

 core文件的内容的实际上是 将进程在内存中保存的核心数据(与调试有关)转储到磁盘中形成的core文件【core dump:核心转储】。这样,当进程退出的时候我们就可以通过core定位到进程为什么退出,以及执行到哪步代码退出的。所以,core文件的作用就是帮助我们调试

 core文件可辅助调试,比如还拿上面那段除零错误代码,并且打开核心转储,生成core文件,进入gdb,使用core-file core 命令,即可查看进程出错原因:

 这种辅助调试被称为 事后调试方案,我们使用man 手册查看的signal手册中的所有信号只要执行动作为core都可以打开core dump进行事后调试。

而我们云服务器中核心转储功能是默认关闭的,是为了防止未知的core dump一直进行,不断生成core文件,从而使服务器资源被占满。把core的大小设置为0即可关闭core dump功能如果用户打开该功能忘记关闭了其实也不用太过担心,因为重启时core dump会默认关闭。

总结:

1️⃣核心转储的核心作用
  • 故障定位:通过gdb等调试工具加载核心转储文件,开发者可以回溯进程崩溃时的执行流程、变量值、函数调用栈,快速定位导致崩溃的代码行(例如访问了空指针、数组越界等)。
  • 状态保存:核心转储会完整保存进程崩溃时的内存数据,包括全局变量、堆内存、栈帧等,避免了 “故障现场” 随进程退出而消失的问题。
2️⃣核心转储的启用与限制
  • 启用条件:默认情况下,部分 Linux 系统可能关闭了核心转储功能(通过ulimit -c查看,输出为0表示禁用)。若需启用,可执行命令ulimit -c unlimited(临时生效,仅当前终端有效),或在/etc/security/limits.conf中配置永久生效规则。
  • 限制场景:即使启用核心转储,以下情况也可能导致无法生成core文件:
    • 进程所在目录无写权限(如/tmp外的只读目录);
    • 进程设置了SUID/SGID权限,且系统配置(/proc/sys/fs/suid_dumpable)限制了此类进程的核心转储;
    • 磁盘空间不足或core文件大小超过ulimit -c设置的上限。

二、🧐信号保存

当信号从 “发送方”(如内核、其他进程)传递到 “接收方”(目标进程)时,并非所有信号都会被立即处理 —— 进程可能正处于不可中断的关键操作中(如内核态执行系统调用),此时信号会被暂时 “保存”,等待进程进入合适的时机再处理。信号的保存本质是内核在进程控制块(PCB)中记录信号的状态,确保信号不会丢失。

2-1 🍕信号的三种状态

信号的生命周期可分为产生(Generated)、未决(Pending)、递达(Delivered) 三种状态,三者的流转关系构成了信号保存的核心逻辑:

  • 实际执行信号的处理动作称为 信号递达(Delivery)
  • 信号从产生到递达之间的状态,称为 信号未决(Pending)
  • 进程可以选择 阻塞 (Block ) 某个信号。
  • 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。
  • 注意阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。

 其中信号递达有三个处理动作(默认执行、忽略、自定义),这个在[Linux——Lesson21.进程信号:信号概念 & 信号的产生]中提到过。信号未决表示信号已经写入到进程当中,但是并未处理。信号阻塞 也叫做 信号屏蔽,跟pending位图一样,会提供一个带有屏蔽数的位图,当屏蔽比特位为1则表示信号屏蔽。

 在上一篇说过,信号写入的位置在进程中,所以pending位图和block位图也都在task_struct中:

task_struct 
{unsigned int pending;//未决位图unsigned int block;//阻塞位图//...
}

 那么,如果一个信号被阻塞(屏蔽),那么这个信号将永远不会被递达,除非解除阻塞

阻塞和忽略的区别

忽略是信号递达的一种执行动作,阻塞仅仅是不让对应的信号进行递达。形象一点理解:忽略是boss上已读不回,阻塞是根本就没看你的简历。

经过上面的学习,我们知道task_struct 中有两张位图表,实际上在task_struct 中还有一张表:

  前31个信号则有31种默认处理方法,而这些默认处理方法则是调用对应的函数接口,图中SIG_DFL以及SIG_IGN等宏,都为函数指针数组的数组下标。

  • 每个信号都有两个标志位分别表示 阻塞(block) 和 未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例子中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作即SIG_DFL。
  • SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
  • SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函sighandler。 如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。本章不讨论实时信号。

从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此, 未决和阻塞标志可以用相同的 数据类型 sigset_t 来存储sigset_t称为信号集,这个类型可以 表示每个信号的 “有效” 或 “无效” 状态,在 阻塞信号集 中 “有效” 和 “无效” 的含义是 该信号是否被阻塞,而在 未决信号集 中 “有效” 和 “无效” 的含义是 该信号是否处于未决状态。阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask)这里的“屏蔽”应该理解为阻塞而不是忽略。

阻塞(Block)忽略(Ignore)
作用阶段信号未决阶段(信号已产生,未递达)信号递达阶段(信号已通过未决状态,准备执行处理动作)
核心本质暂时 “屏蔽” 信号,阻止其递达,信号仍处于未决状态信号正常递达,但进程选择 “不响应”,信号直接被丢弃
信号生命周期阻塞解除后,未决信号会继续递达忽略后,信号直接消失,不会再次处理
底层实现内核通过 “阻塞信号集”(mask)标记需阻塞的信号内核将信号的处理动作设置为SIG_IGN(忽略)
示例场景进程执行文件写入时,阻塞SIGINT避免写入中断进程不需要处理SIGPIPE(管道破裂)信号,直接忽略

简单来说:阻塞是 “暂时不让信号来”,忽略是 “信号来了但我不处理” —— 阻塞的信号仍有 “后续处理的机会”,而忽略的信号则彻底 “石沉大海”。

三、 🤓信号集操作函数

信号集(Signal Set)是 Linux 中用于管理一组信号的数据结构,内核通过 “阻塞信号集”(Block Set,又称信号掩码)控制哪些信号需要阻塞,通过 “未决信号集”(Pending Set)记录已产生但未递达的信号。用户层无法直接操作这两个内核态的信号集,需通过系统提供的信号集操作函数间接管理,核心函数围绕sigset_t类型展开。

3-1 🍟sigset_t类型接口

有了sigset_t类型我是是否可以直接操作进程中的信号位图呢?

答案是否定的,其属于内核数据结构,并不会让用户直接访问,为了支持用户访问一些位图结构,操作系统给我们提供了系统调用接口。

sigset_t类型对于每种信号用一个bit表示 “有效” 或 “无效” 状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t类型的变量,而不应该对它的内部数据做任何解释,比如用printf直接打印sigset_t变量是没有意义的(因为不同平台的类型可能会有差异)。
 

sigset_t是 Linux 定义的信号集类型(本质是一个位图,每一位对应一个信号,位为 1 表示信号在集合中),其操作函数主要用于初始化、添加、删除、判断信号集中的信号,函数原型及功能如下:

#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
  • 函数 sigemptyset 初始化set所指向的信号集,使其中所有信号的对应bit清零,表示 该信号集不包含 任何有效信号
  • 函数 sigfillset 初始化set所指向的信号集,使其中所有信号的对应bit置位,表示 该信号集的有效信号包括系统支持的所有信号
  • 注意:在使用sigset_ t类型的变量之前,一定要调用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。

这四个函数都是 成功返回0,出错返回-1sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种 信号,若包含则返回1,不包含则返回0,出错返回-1。

3-2 🌭sigprocmask接口

sigprocmask函数原型:

int sigpricmask(int how, const sigset_t *set, sigset_t *oset);
  • 作用:可读取或更改进程的信号屏蔽字(阻塞信号集)。
  • 返回值:若成功则为0,若出错则为-1。
  • set与oset指针如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号 屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。
  • how参数:用来指示更改或读取进程信号屏蔽字的动作,通常使用以下几种选项:

如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。

3-3 🍔sigpending接口

函数原型:

int sigpending(sigset_t *set);//set为输出型参数
  • 作用:获取当前进程的pending位图。
  • 返回值:成功返回0,否则返回-1。
  • set参数:读取当前进程的未决信号,通过set传出给用户。

3-4 🥗使用场景及理解

通过前面的学习,我们知道了信号的写入是在进程当中的task_struct 中有三张表,分别是 未决位图,阻塞位图对应信号的默认处理动作。我们之前也学习了 signal 接口可以自定义信号捕捉动作,对应第三张表。而sigprocmask和sigpending接口分别对应第二和第一张表:
 

 三个接口分别控制三张表,为了更好理解这些接口,准备应用以下场景:

①屏蔽2号信号。

②给目标进程发送2号信号。

③获取pending位图。

④打印所有pending位图中的信号。

#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
#include <assert.h>
#include <signal.h>void PrintSig(sigset_t &pending)
{std::cout << "Pending bitmap:";for(int signo = 31; signo > 0; signo--){if(sigismember(&pending, signo))//检测pending位图中的信号是否存在{std::cout << "1";}else{std::cout << "0";}}std::cout << std::endl;
}int main()
{//1.屏蔽2号信号sigset_t block, oblock;//栈上定义的变量可能为随机值sigemptyset(&block);// 清空信号集sigemptyset(&oblock);sigaddset(&block, 2);// 此时并未将2号信号写入到pcb block位图中,而是在栈中// 开始屏蔽2号信号,即设置进入内核中int x = sigprocmask(SIG_SETMASK, &block, &oblock);(void)x;// 取消无返回值接收报警std::cout << "block 2 signal sucess" << std::endl;while(true){  //2.获取进程的pending位图sigset_t pending;sigemptyset(&pending);x = sigpending(&pending);assert(x == 0);//3.打印pending位图中收到信号PrintSig(pending);sleep(1);}return 0;
}

注意有一些信号是不能被用户屏蔽的,9号信号 和 19号信号 时无法被屏蔽的,而18号信号会做出特殊处理,如果手动屏蔽 18号信号 可能会释放出其他被屏蔽信号

在前面代码的基础上,我们想要将2号信号最后递达处理:

#include <iostream>
#include <unistd.h>
#include <sys/type.h>
#include <sys/wait.h>
#include <assert.h>
#include <signal.h>void PrintSig(sigset_t &pending)
{std::cout << "Pending bitmap:";for(int signo = 31; signo > 0; signo--){if(sigismember(&pending, signo))//检测pending位图中的信号是否存在{std::cout << "1";}else{std::cout << "0";}}std::cout << std::endl;
}int main()
{//1.屏蔽2号信号sigset_t block, oblock;//栈上定义的变量可能为随机值sigemptyset(&block);// 清空信号集sigemptyset(&oblock);sigaddset(&block, 2);// 此时并未将2号信号写入到pcb block位图中,而是在栈中std::cout << "proc pid: " << getpid() << std::endl;// 开始屏蔽2号信号,即设置进入内核中int x = sigprocmask(SIG_SETMASK, &block, &oblock);(void)x;// 取消无返回值接收报警std::cout << "block 2 signal sucess..." << std::endl;int cnt = 0;while(true){  //2.获取进程的pending位图sigset_t pending;sigemptyset(&pending);x = sigpending(&pending);assert(x == 0);//3.打印pending位图中收到信号PrintSig(pending);cnt++;//4.解除2号信号的屏蔽if(cnt == 10){std::cout << "unblock 2 signal sucess..." << std::endl;x = sigprocmask(SIG_UNBLOCK, &block, &oblock);//解除屏蔽,2号信号则会立刻递达(执行)assert(n == 0);}sleep(1);}return 0;
}

10次以后,解除block位图,pending位图中2号信号立即开始递达,进程立马终止。为了不让进程立马终止,我们对2号信号进行自定义捕捉:

  这个时候的动作就变为了自定义捕捉,并且在信号解除阻塞时,pending位图会立马清零,然后再进行递达处理

四、🤨信号处理

4-1 🍞信号处理时间

 我们知道,用户设置的信号需要再内核中进行处理,而信号的处理时间是在 进程从内核态切换回用户态 时被处理。

  • 用户态:进程执行用户编写的代码(如main函数、自定义函数);
  • 内核态:进程执行内核代码(如调用readwritesleep等系统调用,或处理硬件中断)。

  我们信号处理一般遵循下面这张表:

 单看这张图可能你一时半会不能很好理解,我以一个系统调用为例解释说明:

  • 一个程序在正常的执行自己的代码,但是突然收到一个系统调用,这时就会陷入内核执行系统调用,而执行完系统调用时并不会立即返回用户态,而是对block bitmap和pending bitmap进行遍历检测:
  •   如果没有信号则返回用户态。如果信号为忽略或者默认执行,那么无外乎终止或者暂停信号,则把进程杀死或者将进程的状态设置为暂停状态,并且放入等待队列中。
  •   如果信号为自定义捕捉,那么在内核中检测到信号需要自定义捕捉,则会切换回用户态执行捕捉函数,但是这时并不会在用户态就结束了。而是返回内核态从上次被中断的地方继续向下执行,最后再返回用户态。

我们都知道,内核中拥有进程的代码和数据,那么这时你可能就会有疑问了:既然进程拥有我们的代码,为何还要从内核态转换为用户态再执行自定义捕捉函数呢???

  实际上,因为自定义捕捉是由用户来写的,而内核并不知道你这个用户究竟是不是病毒,会不会危害OS的安全,所以对用户默认是有害的,这样,不在内核中执行自定义捕捉,到用户态执行,就算崩溃了也会减少对操作系统的影响。
那么信号的捕捉,可以简化为下面这张图:

4-2 🥪信号处理流程

  为了更好的理解信号在操作系统中从产生到执行的过程,我们有必要深入理解 用户态  内核态 这两个概念。

 要理解信号的处理流程,还得从进程地址空间说起,在操作系统中,进程地址空间分为用户空间([0-3GB])和 内核空间([3-4GB])。我们知道,电脑开机时最先加载到物理内存的软件是操作系统,而进程要与操作系统产生联系,这就需要用到进程的内核空间,内核空间和操作系统由 内核级页表 进行映射

操作系统中页表可分为 用户级页表 和 内核级页表,在此之前我们所提到的页表皆是用户级页表,内核级页表用来映射OS和进程的,这样进程就可以调用操作系统的系统调用。注意,这两个页表其实是一个页表,只不过是根据其特性进行的划分

 而 系统调用的本质是 函数指针数组。而我们把这个 数组的下标 称为 系统调用号,我们使用系统调用或者访问系统数据,其实还是在进程地址空间内跳转的。

而操作系统中存在许多进程,而每个进程都有自己的代码和数据,所以每个进程都拥有自己的用户级页表。而操作系统对进程来说只有一份,所以 操作系统中内核级页表也只有一个。也就是说,每个进程的地址空间0-3GB(用户级)都不一样,3-4GB(内核级)都一样,所以每个进程都可以调用系统调用

 以上的过程意味着,在操作系统当中,无论进程如何切换,总是能找到操作系统。所以我们所访问操作系统,其实是通过进程地址空间的3-4GB来访问OS的。

那么操作系统又是如何运行的呢?我们前面说过,硬件中断的问题,键盘通过硬件中断被CPU的针脚识别从而调用中断向量表对应的中断方法,不过这是硬件层面。而Linux信号技术,本身就是 通过软件的方式来模拟硬件中断

而在OS中,每隔非常短的时间,就给CPU发送中断,CPU就需要通过中断向量表不断的处理中断,这种高频间隔中断被称为 操作系统的周期时钟中断。而操作系统就是一个死循环,在不断接收外部的其他硬件中断。

而我们所用的系统调用实际上也被封装过,比如我们调用read接口,则会把 系统调用号 保存到寄存器里,然后陷入内核,根据read中保存的中断方法地址,从而去执行对应的方法。说白了就是通过数组下标 调用数组元素。

有一个至关重要的问题,既然进程地址空间中3-4GB的空间可以直接访问OS,那么为什么我们还需要陷入内核调用呢?前面我们说过,操作系统对用户默认是不信任的,如果用户写了一段病毒,来访问3-4GB的地址空间,那不就危险了?所以,OS就必须要区分当前用户的运行模式,也就是 用户态 和 内核态。
  在CPU中有一个寄存器叫做 CS寄存器,CS寄存器最后两位比特位表示 进程的权限标志位为0表示内核态,为3表示用户态。当然具体的情况要很复杂,如果感兴趣还请自行查阅资料。

五、😁捕捉信号的其他方式

  除了signal 自定义捕捉以外,Linux还提供了一种其他自定义捕捉方法:sigaction函数

  • 功能sigaction函数可以读取和修改与指定信号相关联的处理动作
  • 返回值调用成功则返回0,出错则返回- 1
  • signo参数signo是指定信号的编号
  • act 和 oact参数若act指针非空,则根据act修改该信号的处理动作。若oact指针非 空,则通过oact传出该信号原来的处理动作。act和oact指向sigaction结构体

 其中结构体中第二个成员处理的是实时信号,不需要管,第四个成员设置为0即可,第五个成员也不需要管。所以我们使用这个结构体只需要把第一个参数与第三个参数设置好即可。

为什么会有mask参数?实际上,当某个信号的处理函数被调用时,内核自动将当前信号加入到进程的信号屏蔽字。而当我们处理完信号之后,该信号也会从阻塞状态解除OS这么做的目的是禁止一个信号被嵌套捕捉,只允许一个信号进行串行处理。我们做个实验验证一下:

#include <iostream>
#include <signal.h>
#include <unistd.h>void Print(sigset_t &pending)
{std::cout << "curr process pending: ";for(int sig = 31; sig >= 1; sig--){if(sigismember(&pending, sig)) std::cout << "1";else std::cout << "0";}std::cout << std::endl;
}void handler(int signo)
{std::cout << "signo: " << signo << std::endl; //不断获取当前进程pending信号集合sigset_t pending;sigemptyset(&pending);while(1){sigpending(&pending);Print(pending);}
}int main()
{struct sigaction act, oact;act.sa_handler = handler;act.sa_flags = 0;sigemptyset(&act.sa_mask);// 有什么用?sigaction(2, &act, &oact);while(1) sleep(1);return 0;
}

我们不断对2号信号进行自定义捕捉并且无间断的执行,这时我们无论怎么按Ctrl-C 都毫无相应,因为此时当前进程正在处理2号信号,2号信号被屏蔽,故别的进程无法使用2号信号。

 上面代码中还有一个疑问的点,sigaction函数照这样看来不是和signal函数没两样吗?为什会更复杂?实际上,sa_mask参数可以额外屏蔽其他信号使用时可将需要额外屏蔽的信号设置到函数当中。

5-1 🍿可重入函数

 可重入函数与链表相关,如果数据结构还没学过的建议看一看链表。这里只是简单认识一下,具体过程将会在线程篇详细解读。

main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换 到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的 两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续 往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后 向链表中插入两个节点,而最后只有一个节点真正插入链表中了。
简单来说,就是在head节点后插入一个新节点,但是在插入过程中需要从用户态转内核态,而前面说了,进程在内核态的时候会顺便检查信号,这时刚好收到信号,执行自定义捕捉,而自定义捕捉也是在head后插入一个节点。handler完成后,main函数依旧在刚才插入那步,最后head = p,使得头结点指向第一个被插入的节点,而自定义捕捉方法内插入的节点就会丢失。
像上例这样,insert函数被不同的控制流程调用, 有可能在 第一次调用还没返回时就再次进入该函数这称为 重入insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为 不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为 可重入(Reentrant) 函数

5-2 🥨volatile关键字

 如果你学过C语言或者C++,那么你一定听说过volatile关键字,但是你可能并不能记得它的具体作用。

#include <stdio.h>
#include <signal.h>int g_flag = 0;void changeflag()
{g_flag = 1;printf("将g_flag, 从%d->%d\n", g_flag, 1);
}int main()
{signal(2, changeflag);while(!g_flag);// 有时候编译器会对其进行自动优化,全局变量原因printf("process quit normal!\n");return 0;
}

编译时带上 -O 选项,表示优化程度,其中gcc编译器分为4个优化级别,分别是 O0, O1, O2, O3 其中 00 表示编译时不带任何优化。

 现在的编译器可能会对一些地方进行优化,但是有时候我们并不想让其被优化,比如全局变量g_flag,现代编译器,为了优化代码,因为编译器认为全局变量访问概率大,大概率会把g_flag放置到寄存器当中,每次需要访问g_flag时,只需要从寄存器内取即可,但是今天我们需要修改g_flag的值,修改的却是内存中的g_flag的值,而保存在寄存器中的g_flag却不曾改变。
我们对全局变量使用了volatile关键字,这样,无论编译器怎么优化,都不会影响g_flag的值了:

这样,在怎么优化,都不会把我们预期动作改变了。

5-3 🍗SIGCHLD信号

我们在学习进程的时候曾经说过,僵尸进程出现的原因是父进程没有回收子进程,实际上 子进程在终止时会发送 SIGCHLD 信号给父进程,而该 信号的默认处理动作是忽略,父进程可以对该信号进行自定义捕捉,这样就没必要浪费资源对子进程进行等待了。

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>void handler(int signo)
{// v1if(signo == SIGCHLD){pid_t rid = waitpid(-1, nullptr, 0);if(rid > 0){std::cout << "wait child sucess: " << rid << std::endl; }}std::cout << "wait child sucess done" << std::endl;
}int main()
{signal(SIGCHLD, handler);pid_t id = fork();if(id == 0){// childint cnt = 5;while(cnt --){std::cout << "I am child process: " << getpid() << std::endl;sleep(1);}std::cout << "child process died" << std::endl;exit(0);}// father while(1) sleep(1);return 0;
}

我们对SIGCHLD信号做了捕捉回调,一旦子进程退出就回收子进程。

  这样,通过信号处理就不需要父进程在将资源用在监视子进程是否退出这件事上。但是这种代码却是一种错误的代码。

  我们说过,pending位图如果收到同一个信号多次,只会记录一次,那么如果有个场景是多个子进程在同时运行,最后子进程都结束了,发送了多次的SIGCHLD信号,但是pending位图只记录一次,所以这个时候我们只能处理一个子进程,剩下的子进程会变为僵尸。

把子进程回收改为如上图所示,就解决了所有问题,无论是100个子进程退出,还是100个只有50个子进程退出,这样就都可正常将子进程回收了。

总结与提炼

信号是操作系统中进程间异步通信的核心机制,其核心知识点可归纳为以下几个方面:

1️⃣信号的生命周期:从产生(硬件/软件事件触发)保存未决状态/递达状态,受阻塞集控制)处理(默认/忽略/自定义),关键在于理解阻塞集和未决集的交互关系,以及信号在从内核态返回用户态时处理的时机。

2️⃣信号集操作:sigset_t类型是信号集的基础,sigemptysetsigaddset等函数用于操作信号集,sigprocmask用于修改进程阻塞集,sigpending用于获取未决信号集,这些函数是实现信号阻塞和监控的核心工具。

3️⃣信号处理的注意事项:自定义信号处理函数时,需使用可重入函数避免数据冲突,使用volatile关键字防止变量被编译器优化,这些细节直接影响程序的正确性。

4️⃣特殊信号的应用SIGCHLD信号是监控子进程状态、回收僵尸进程的关键,通过捕捉该信号可实现父进程对子进程的高效管理;SIGSEGV等信号的核心转储功能是程序调试的重要手段。

掌握信号的工作原理,不仅能理解程序中的异常处理、进程控制等场景,更能为后续学习多进程、多线程通信等高级主题奠定坚实的基础。在实际开发中,需结合信号的异步特性,合理设计信号处理逻辑,避免出现资源泄漏、数据不一致等问题。


结束语

以上是我对于【Linux文件系统】进程信号:信号保存 & 信号捕捉的理解

感谢您的三连支持!!!

http://www.dtcms.com/a/582673.html

相关文章:

  • 10.【NXP 号令者RT1052】开发——实战-RT 看门狗(RTWDOG)
  • 维护一个网站需要多少钱黄山网站建设哪家强
  • 深夜思(原创诗)
  • 阿里云做的网站怎么样做网站都需要具备什么
  • openEuler 集群部署Nova计算服务:控制节点与计算节点实战操作
  • 怎么建设网站商城衡阳企业网站建设
  • 广渠门做网站的公司潍坊网站建设公司慕枫
  • 网店装修网站wordpress界面变宽
  • 强化学习的原理
  • Python 装饰器原理与实战技巧(深度解析生成机制)
  • 全国各地网站开发外包餐饮网站建设教程
  • python购物网站开发流程专业制作网站有哪些
  • 中小学校园网站建设wordpress使用邮箱验证
  • 深入剖析C++临时对象:从创建到优化
  • OLED代码演示-使用缓存区
  • 怎么查看网站disallow找做网站
  • C语言结构体入门:定义、访问与传参全解析
  • 住房城乡建设部门户网站苏州建设公司有哪些
  • 软件工程综合实践3实验报告——校园二手交易平台系统(黑龙江大学)
  • 设计制作网站板面网站建设优化开发公司哪家好
  • “职场心态与心穷
  • 网站怎么做微信支付宝wordpress占用cpu过高
  • 郑州网站推广营销百度搜索引擎竞价排名
  • 班级网站建设思路手机模板网站模板下载
  • Rust 练习册 :Nucleotide Codons与生物信息学
  • 东坑网站仿做麻涌镇做网站
  • stm32的gpio模式到底该怎么选择?(及iic,spi,定时器原理介绍)
  • 【MySQL】触发器、日志、锁机制 深度解析
  • 电商网站后台艺术设计
  • 【湖北政务服务网-注册_登录安全分析报告】