【Linux】信号的产生,保存,捕捉机制
目录
一、产生信号的过程
二、操作系统得知硬件数据就绪手段
三、信号产生的方法
3.1键盘终端产生
3.2系统调用产生
3.3 硬件异常产生
3.4 软件异常产生信号
四、Core Dump核心转储
五、信号保存和信号集
5.1信号的相关术语
5.2 信号在内核中的保存方式
5.3 信号集操作函数
六、信号捕捉处理原理
6.1什么是内核态,什么是用户态?
6.2 进程如何表示内核态和用户态
6.3 操作系统如何被运行?
6.4 sigaction
6.5 信号的捕捉流程
七、可重入函数,不可重入函数
7.1 不可重入函数实例
7.2可重入函数,不可重入函数的区分
八、volatile关键字
九、SIGCHLD信号
十、完结

一、产生信号的过程
产生信号的过程如下:
第一步: 硬件中断处理
当外设有数据了,外设可以给 CPU 发送硬件中断。 中断都有自己的中断号,硬件中断就是这样,CPU 通过它的中断号来判断该中断是哪个外设发送的。
之后 CPU 会以这个中断号为下标,接着CPU去操作系统中的中断向量表(下面讲)中执行对应的方法(或者称为所对应的程序),即通过中断号找到并执行中断处理程序,该程序负责从产生中断的硬件中读取数据,并存入内存。”
第二步: 信号的“产生”与发送
操作系统的更高层会识别到刚刚进入内存的数据不是一个普通数据,而是一个特殊数据(如Ctrl + C),对于普通数据,操作系统会将这个数据拷贝给正在运行的前台数据,如输出"你好"。
但对于这种特殊数据,操作系统并不会这么做,而是将转换这个操作的语义,产生一个信号,把这个信号发送给前台进程
我们知道,在终端输入Ctrl+C这个指令可以让进程终止退出
那么还有几个指令也可以,分别是:Ctrl+\ , Ctrl+z ,这两个指令的功能不同,接下来详细讲解:
这几个硬件中断信号的执行原理就如开头所讲一样
Ctrl+\ :发送终止信号并生成core dump文件,用于事后调试(后面详谈)
Ctrl+z :可以发送停止信号,将当前前台进程挂起到后台等。
这里我们只演示Ctrl+z

可以发现Ctrl + z 确实让进程停止了
如果想要再次运行,只需要在终端输入fg即可
fg 是 “恢复暂停”,继续之前的执行状态;

二、操作系统得知硬件数据就绪手段
硬件和进程之间通常会伴随着数据拷贝。
比如我们在终端输入指令信息(如ls),因为ls所展示出来的效果是调用进程才给我们展示出来的,但是硬件的数据并不能直接被进程读取,因此必须让操作系统先把硬件的缓冲区拷贝到内存中的内核缓冲区中。
接着操作系统会根据场景来决定要不要把内存中的数据传给目标进程。
比如 ls指令 就需要传给目标进程。
首先,当你按下键盘上的一个键,键盘控制器会先将该按键对应的扫描码存入它自己的硬件缓冲区中
其次,因为Shell进程本质就是一直在等待着你的命令,又因为硬件的数据并不能直接被进程读取。而ls指令就是你的命令,所以你输入ls,数据就会被操作系统从硬件缓冲区中读取数据,并将这份数据拷贝到内核级缓冲区中。
最后,操作系统检测到内核级缓冲区存在数据了,就会唤醒Shell进程,并将内核级缓冲区的内容拷贝到Shell进程
如果是其他场景比如说输入的是用户密码,这时候操作系统就不会把数据传给进程了,这是因为我们刚才提到过操作系统会根据场景来决定要不要把内存中的数据传给目标进程。而这种涉及安全性的东西,操作系统不会把数据传给进程。这是为了保护你的密码安全,防止恶意窃取
上述过程中我们提到过操作系统要先把硬件的缓冲区拷贝到内存中的内核缓冲区中,才能实现软件读取硬件的数据,但是操作系统怎么知道硬件的资源就绪的?
操作系统监测硬件资源就绪,有两种方式:
- ①:操作系统定期轮询检测所有硬件,判断那些硬件数据就绪。但这非常不现实,如何评判设定轮询间隔时间?时间太长,系统整体效率降低;时间太短会导致CPU疯狂进程监测,减少进程调度次数,进而影响程序性能!
- ②:另一种方式就是硬件就绪后,让硬件主动通知操作系统!
在我们的CPU周围会存在很多针脚(如下图),这些针脚对应一个个的编号,每一个针脚对应一个唯一硬件。当硬件资源就绪之后,硬件会通过特定的针脚向CPU发送光电信号。此时CPU检测到光电信号后,会将收到的光电信号转化为对应针脚的编号,并且将该数字保存到寄存器中,我们也将这些编号称为中断号。
当OS识别到该寄存器中存在数据时,操作系统就知道存在硬件已经硬件资源就绪了,OS会立马停下手中的所有工作,将对应硬件中的数据加载到CPU中,完成数据的加载。
为了进一步提高读取硬件数据的效率,操作系统中存在一张函数指针数组,该数组中下标位置对应中断号种类,数组中保存了对应硬件的读取方法!我们将该函数指针数组称为中断向量表,该表会在操作系统初始化时生成!此时CPU通过在寄存器中保存的中断号即可索引到对应硬件的读取方法,快速从产生中断信号的硬件中读取数据并存入到内存!!
当然CPU的针脚是有限的,硬件也不是直接和CPU链接。在CPU和硬件之间通信需要经过主板,而主板存在一定的扩展能力。并且并不是所有的硬件都有发送中断能力!

三、信号产生的方法
信号产生的方法一共有四种
3.1键盘终端产生
第一种方法是:键盘终端产生,这种方法是最常见的,也是我们之前一直在讲的,也就是输入Ctrl+c,Ctrl+z 这样的方式来产生信号
3.2系统调用产生
第二种方法是: 系统调用产生
(1)实例
操作系统提供了系统调用接口kill,用来向指定进程发送信号!
#include <signal.h>int kill(pid_t pid, int sig); //发送成功,返回0;否则返回-1
//参数解释:
pid : 进程的pid,表示给哪个进程发送信号
sig : 发送的信号是什么(我们之前提到过信号都有其对应的编号和数字)
【代码实例】:进程打印3次消息后,通过系统调用接口发送2号信号
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>void handler(int signo)
{std::cout << "自定义捕捉信号: " << signo << std::endl;exit(0);
}int main()
{int count = 3, cnt = 0;signal(2, handler);//自定义捕捉2号信号while(true){std::cout << "running ..." << ++cnt << std::endl;if(--count == 0)kill(getpid(), 2);sleep(1);}return 0;
}
效果如图->:

(2)、产生信号的系统调用总结
①:kill函数可以给一个指定的进程发送指定的信号。(kill命令是调用kill函数实现的)int kill(pid_t pid, int sig)
②:raise函数可以给当前进程发送指定的信号(自己给自己发信号)。int raise(int signo); //成功返回0,错误返回-1。
③:abort函数异常终止当前进程,并且产生一个SIGABRT信号。这个函数通常用于程序遇到无法恢复的错误时,立即终止程序运行。void abort(void); //就像exit函数一样,abort函数总是会成功的,所以没有返回值
3.3 硬件异常产生
下面以浮点数溢出和空指针解非法解引用错误为例
(1)、浮点数溢出,CPU产生信号
我们知道除式中,除数为0是非法的。此时CUP硬件会发送8号信号,表示浮点数异常Floating point exception。
我们先来看看相关现象,代码如下:(我们特意让进程一直被运行,并且8号信号自定义捕捉。进程收到8号信号时不退出)
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>void handler(int signo)
{std::cout << "自定义捕捉信号: " << signo << std::endl;sleep(1);
}int main()
{std::cout << "pid: " << getpid() << std::endl;signal(8, handler);int x = 10;x /= 0;while(true){}return 0;
}

我们观察到进程确实收到了8号信号。
(2)、浮点数溢出,CPU产生信号的原理
在CPU中存在着许多寄存器,其中存在一个名为status的状态寄存器,在这个状态寄存器中存在着一个标志位,这个标志位是用来判断最近一次的运算是否存在结果溢出的
一旦发生结果溢出,这个标志位就会标记为1,否则为0
异常处理阶段:
对于刚才的代码,我们执行的是x/0, 假设x是10,此时CPU中的寄存器eax保存的就是10,寄存器ebx保存的就是0,10/0本质上是除以一个无限小的数,那么结果就是无限的大,导致发生结果溢出,那么结果溢出了,status寄存器中的标志位就会由0变为1,同时停止当前进程,并通知操作系统。
接下来操作系统知道status寄存器的标志位变为1了,OS识别到这是一个错误,于是把这个异常解释为kill(targetprocess, signo),然后将kill生成的信号保存到PCB中。
异常处理完后:
异常处理完后,因为并没有exit()退出进程,所以操作系统需要恢复进程继续执行,但是在恢复之前,操作系统必须恢复进程的上下文,这是为了还原异常发生前的所有状态。
因此,操作系统需要根据PCB来恢复寄存器eax中的10,和ebx中的0,以及下文(依旧是10/0这一串代码)。(PCB中存储着下一条要执行的指令地址和寄存器上下文)
恢复好了之后,进程重新开始由CPU调度,CPU一调度发现,status寄存器标志位还是1,那就会继续停止进程并通知操作系统,操作系统又会解释为 kill 命令,解释后的信号又会被存放到PCB中。存放完了后操作系统又会通过PCB恢复上下文好让CPU继续进程调度。这样就陷入了一个死循环

(3)、空指针解引用错误,MMU产生信号原理
MMU也叫做内存管理单元,它是负责处理CPU内存访问请求的计算机硬件。现如今一般集成到CPU上。它的功能包括虚拟地址到物理地址的转换(即虚拟内存管理),内存保护等
在我们对空指针进行非法解引用时,即试图对0号地址写入。我们知道,空指针通常都有虚拟地址,但是没有真实的物理地址,那么这就意味着在页表中,虚拟地址和物理地址没有办法映射。那么MMU在进行虚拟地址向物理地址转换时就会发生失败,MMU就会报错,并改变相关标志位。之后OS会识别到这个错误,并向目标进程发送信号。
3.4 软件异常产生信号
软件异常名词解释:
当程序在运行过程中出现不符合预期的 “软件层面异常”(如逻辑错误、非法操作等)时,操作系统会检测到这些异常,并通过 “信号(Signal)” 机制向发生异常的进程发送特定通知,强制进程处理该异常
对于管道,比如匿名管道等存在同步机制的管道。当读端关闭,此时管道写端也会关闭退出。这就是一种典型的软件异常。当管道读端关闭时,写端进行写入时会触发 SIGPIPE14信号。进而关闭写端退出!
接下来为了更直观的感受软件异常产生信号,我们要新学习一个函数,alarm函数
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
//返回值是0或者是以前设定的闹钟时间还余下的秒数
// alarm(0):表示取消原有闹钟,并返回上一个闹钟的剩余时间
/参数介绍:
/int seconds : seconds秒后给当前进程发送SIGALRM信号
/ 该信号的默认处理动作时终止当前进程

可以看到,当alarm开始执行后,就开始了计时,当3s后就停止了输出
需要注意的是,alarm(0)的作用是取消当前闹钟并返回上一个闹钟所剩余的时间值,但是并不会给当前进程发送任何信号

四、Core Dump核心转储
Core Dump核心转储是指在进程异常终止时发送信号,如果信号的默认处理方法为core,此时操作系统会将该进程退出的核心上下文转储到磁盘上,形成core文件。该文件中包含调试信息,文件较大,并且可能包含用户密码等敏感信息,一般默认禁止生成。
在LInux中绝大部分信号的默认行为都是终止进程,并且分为Term、Core两大类。其中Term报错进程终止原因非常明确,比如管道破损读端关闭导致进程终止。而Core类报错更像一种真正的异常,虽然问题原因瞄准准确,但问题一般比较严重,通常是程序代码本身存在问题,并且需要用户对代码进一步排查。

所以在发送Core时,除了终止程序,还会生成一个Core文件。Core文件会将当前进程退出的核心上下文进行保存,转储到磁盘上。可以通过ulimit -a查看系统配置信息,其中就包含core文件的默认大小.(默认为0).但我们可以用ulimit -c命令改变Shell进程的Resource Limit,允许core文件最大为1024K: ulimit -c1024
五、信号保存和信号集
5.1信号的相关术语
1. ①:执行信号的处理动作称为信号递达
2. ②:信号从产生(发送到进程)到递达(进程实际处理)的过程中,若信号被阻塞(通过 sigprocmask 设置),则信号会处于“未决状态”。即信号产生后未被立即处理,因为某种原因卡住了,保存在信号位图中。
3. ③:进程可以阻塞信号。被阻塞的信号会始终保持未决状态,只有解除阻塞后才能被递达!
4. ④:阻塞和忽略不同。"忽略"本身是处理信号的一种方式;而阻塞的信号没有被处理,处于未决状态。
5.2 信号在内核中的保存方式
在进程PCB中,存在3张表:block(阻塞)表、pending(未决)表和handler表(1~31号信号保存方式)。具体如下:

三个表的介绍:
1. 每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针用来表示处理动作。这三种相关信号信息都是由PCB管理的,而PCB通过位图的方式来管理。
2. 在信号产生后,操作系统会将pending表中的特定比特位内容置为1。如果需要将信号设置为阻塞状态,此时仅需将block表位图种的特定比特位由0置1即可。
没阻塞的信号,其未决状态是瞬时的。
所以即使阻塞位表是0,pending表也可以为1
3. handler表中保存的是对应信号的信号处理函数指针
操作系统处理信号工作原理:
4. 操作系统在处理信号时,首先会检查 Pending 是否存在信号,即位图中是否存在比特位为1。如果存在,此时 OS 会查看 block 表中查看信号是否设置位阻塞。如果信号没有阻塞,此时 OS 会根据信号编号索引到handler表中,执行对应的信号处理方法!
5. 在Linux中,常规信号在递达之前产生多次只计一次。而实时信号在递达之前产生多次可以依次放在一个队列里。
6. 上述xxx位图只是站在内核的角度表述。在用户层,我们一般将block位图表称为阻塞信号集或信号屏蔽字;而将Pending位图称为未决信号集!
5.3 信号集操作函数
对于信号的处理方法,们直接通过系统调用signal函数即可将自定义方法设进至内核。(即:将handler表种对应的函数指针更换)。但信号屏蔽字和未决信号集是位图结构,如果也让用户自定义设置不仅繁琐,而且非常容易出错。
那么为了让我们想要更换信号屏蔽字和未决信号集,操作系统给我们提供了新的数据结构sigset_t(称为信号集),这个数据结构是一种拟信号集的数据结构,它可以添加信号。并不是直接将内核中的信号集拿出来用
这个数据结构还可以使用一些函数表示每个信号的“有效”或“无效”状态,但是"有效"状态具体指的是阻塞还是未决是不清楚的。它的作用是帮你批量管理信号。
信号集合分为阻塞信号集和未决信号集。在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。
也就是说我们对sigset_t这个信号集进行操作,我们就可以间接的更改信号的block表和pending表,那么我们需要新学以下几个函数的使用->:
1)、信号集操作函数
系统中提高了5个信号集操作函数:
#include <signal.h>
int sigemptyset(sigset_t *set); // 将信号集清空,比特位全0
int sigfillset(sigset_t *set); // 将信号集比特位全部置1
int sigaddset (sigset_t *set, int signo); // 将指定信号添加到信号集中
int sigdelset(sigset_t *set, int signo); // 从信号集中删除指定信号
int sigismember(const sigset_t *set, int signo); // 判断信号集中是否存在指定信号需要注意的是sigismember是一个布尔函数,若存在则返回1,不包含返回0,出错返回-1
使用实例->:

特别注意事项:
我们定义出来的信号集并不是真正意义上的搞出了信号的集合,也就是说即使我们把所有比特位设定为1,也只是虚拟上的存在,并不是真正意义上的存在,我们使用sigaddset也是如此。我们定义出来的信号集并不是真实的,而是逻辑上的。
但是后续可以通过这个逻辑的信号集去更改真正的block信号集和pending信号集
2)、sigprocmask
调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功则为0,若出错则为-1
- ①:
sigprocmask函数可以将信号机通过参数set传入设置进内核,并将老的信号屏蔽字通过oset带出。 - ②:
how参数选项有如下3种:

第三个参数平常设置为NULL即可。
为什么通常设置为NULL?
因为大多数场景下,我们只需要 “修改当前的阻塞信号集”,并不需要 “记住之前的状态”。例如:你的代码中,只是想临时阻塞所有信号,执行完后直接退出,不需要恢复之前的阻塞状态,所以设为
NULL即可。什么时候需要不设为NULL?
如果需要 “先临时修改阻塞集,之后再恢复原样”,就需要用
oldset保存旧状态。
使用实例如下->:

3)、sigpending
#include <signal.h>
int sigpending(sigset_t *set);
//读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。
sigpending的使用需要使用一个新的信号集,这个新的信号集用来当作未决信号集供给sigpending函数使用
我们上面说过,未决状态就是信号发出但是没有立即处理,那么把信号阻塞了其实就相当于信号处于未决状态,接下来我们来验证一下->:
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;int main()
{sigset_t set; // 定义信号集sigset_t pending_set; // 定义未决信号集sigemptyset(&set); // 初始化信号集sigaddset(&set, 1); // 将1号信号添加进信号集中sigprocmask(SIG_SETMASK,&set,NULL); //使进程阻塞1号信号kill(getpid(),1);//发送1号信号sigemptyset(&pending_set); // 初始化未决信号集sigpending(&pending_set);//读取当前未决信号并放入自己定义的未决信号集中if(sigismember(&pending_set,1)){cout << "1号信号未决状态ing..." << endl;}
}

六、信号捕捉处理原理
6.1什么是内核态,什么是用户态?
内核态:
内核态是操作系统内核运行的状态(即操作系统的一种工作状态),此时处理器可以访问系统的所有资源,包括对硬件设备的直接控制、内存的任意读写、执行特权指令等 。
用户态:
用户态是普通用户程序运行的状态(如你写的main函数),此时处理器的权限受到严格限制,程序只能访问属于自己的内存空间,不能直接访问硬件设备,也不能执行特权指令,以保证系统的稳定性和安全性。
6.2 进程如何表示内核态和用户态
进程是被加载到CPU上运行的,而CPU本身就存在工作级别。在CPU中存在一个名为CS的寄存器。该寄存器中的后两个比特位的内容表示CPU的工作级别。(1表示内核态,3表示用户态)
所以进程运行时,只需要检测CS寄存器的后两个比特位的内容即可判断进程当前所处工作状态!
除此之外,CPU中还存在两个寄存器CR3和CR1。CR3寄存器中保存的是页目录表的物理地址,帮助快速找到页表地址!而CR1寄存器保存了曾经引发缺页中断的虚拟地址。
名词解释:
缺页中断:当你试图访问一个在虚拟地址空间中存在但是在物理地址不存在时(也就是没有建立有效的虚拟地址到物理地址映射)的页面时,就会触发缺页中断。
6.3 操作系统如何被运行?
首先进程间是相互独立的,因此每个进程都存在一张用户级页表。此处在外,还存在一张内核级页表。所有进程共用同一张内核级页表,该页表指向的是操作系统的代码和数据!!
除此在外,还存在一个名为CMOS的硬件单元,该硬件会周期性的,高频率的向CPU发生时钟中断。当CPU收到对于的中断信号时,CPU会通过当前进程的PCB找到内核级页表,进而找到操作系统的代码和数据进行执行!
名词解释->:操作系统的代码和数据:
操作系统内核运行所需的指令、数据结构、常量等
6.4 sigaction
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact)struct sigaction {void (*sa_handler)(int);void (*sa_sigaction)(int, siginfo_t *, void *);sigset_t sa_mask;int sa_flags;void (*sa_restorer)(void);
};
1. sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回- 1。
signo 是指定信号的编号。
若 act 指针非空,则根据act修改该信号的处理动作。
若 oact 指针非 空,则通过 oact 传出该信号原来的处理动作。
act 和 oact 指向 sigaction 结构体。
2. 将sa_handler赋值为常数 SIG_IGN 传给sigaction表示忽略信号,赋值为常数 SIG_DFL 表示执行系统默认动作,赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函 数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用
6.5 信号的捕捉流程
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。
举例如下: 用户程序注册了SIGQUIT信号的处理函数sighandler。 当前正在执行main函数,这时发生中断或异常,当前运行的进程就需要切换到内核态好让操作系统进行处理异常或中断。 中断/异常处理完毕后进程本应回到main函数,却突然发现有信号需要处理
因此在内核返回用户态恢复main函数的上下文继续执行之前,要先进行信号处理,因此先去执行sighandler函数而不是main函数。sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。 sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。
流程图如下->:

一次完整的信号捕捉,一共涉及4次状态切换

1. 在函数栈帧开辟时,就以及内置有关函数结束后返回值放回位置信息。此时OS只需在创建用户自定义方法时,串改该函数的返回值,改为sigreturn的地址即可!
2. 信号捕捉处理是在所有信号都已经处理完成之后,才会返回用户态!
3. 我们不一定通过系统调用陷入内核,执行完,返回用户态才进行信号检查。
一个进程是有时间片的,那么有时间片就会涉及到进程切换。当进程重新被加载运行时,OS需要先进行内核态将当前进程的数据进行恢复。恢复完成之后,OS需要将进程的工作模式从内核态切换回用户态。在该过程中,OS会对进程信号进行自定义捕捉,对信号进行处理!(因为我们上面提到过,最后一步从内核态回到用户态是要检测是否还有信号未处理的,但是对进程信号自定义捕捉并不意味着一定会发生信号捕捉。如果没有信号要处理那就不会发生信号捕捉,而是直接返回用户态)
七、可重入函数,不可重入函数
(在阅读下文请先熟知 6.5信号的捕捉流程)
7.1 不可重入函数实例
先来解释下面这段流程图函数。首先main函数正在利用 insert函数 执行链表List的头插。但不巧的时,新节点在插入时(即只执行性node1->next=head->next, 但还未执行head->next=node1),由于进程时间片已经到了,需要却换到下一个进程。

这时,别的进程利用 insert函数 突然发来了一个信号,这个信号是 insert 信号,它的动作是头插node2节点
当最开始的进程再次被调度时,是在内核态完成的。
正常情况下,当下一次该进程被重新调度时,OS在内核态工作模式将当前进程的数据和硬件上下文恢复之后,需要返回用户态执行剩下代码。但是此时我们存在1个未决信号!
因为这个未决信号,在返回用户态之前,要先执行 insert 信号
但是 insert 信号执行的是头插 node2 节点,那么就会先去执行头插 node2 节点。
再经过更改头结点后,头结点指向的就是node2节点,但是 insert 信号处理完了,接着处理的就是因为时间片到了而结束的剩下的代码,那么头节点就会去指向 node1 节点
此时我们发现头节点head指向node1,而node2失效导致问题产生!

7.2可重入函数,不可重入函数的区分
可重入函数是指一个函数被多个执行流进入,但程序本身或程序逻辑不会出现任何异常时,我们称该函数为可重入函数!
但像上例这样,insert函数 被不同的控制流程调用,有可能在第一次调用还没返回时就被其他流程再次进入该函数,这称为重入。
insert函数 访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为 不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数!
如果一个函数符合以下条件之一则是不可重入的:
1. 调用了malloc或free,因为malloc也是用全局链表来管理堆的.
2. 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构.
3. 使用全局或静态变量,或未加保护的共享资源!
八、volatile关键字
保持内存的可见性。告诉编译器,被volatile修饰的变量,不允许被优化。对该变量的任何处理操作,都必须在真实的内存中进行操作!
#include <stdio.h>
#include <signal.h>
int flag = 0;
void handler(int sig)
{printf("chage flag 0 to 1\n");flag = 1;
}
int main()
{signal(2, handler);while(!flag);printf("process quit normal\n");return 0;
}
[hb@localhost code_test]$ cat Makefile
sig:sig.cgcc -o sig sig.c -O2 // -O2表示编译器优化等级,让它极可能的去优化
.PHONY:clean
clean:rm -f sig
[hb@localhost code_test]$ ./sig
^Cchage flag 0 to 1
^Cchage flag 0 to 1
^Cchage flag 0 to 1
我们 Ctrl + C 结束进程,是为了把 flag 设置为 1,这样就可以结束循环,我们可以看到确实输出了"chage flag 0 to 1",让我们认为确实把 flag 改为 1 了。
但是循环并没有结束,这是因为编译器尽可能的优化了,使用 volatile 定义 flag 就可以解决
九、SIGCHLD信号
子进程在退出的时候,会主动的向父进程发送 SIGCHLD 信号(17号)。进程收到该信号的默认处理动作是忽略。所以,父进程在进行等待的时候,可以采用基于信号的方式进行等待。
若采用这种方式,父进程的主逻辑中可以不用调用 waitpid 函数,但是需要在自定义捕捉函数里面调用 waitpid 函数,并且父进程必须保证自己是一直在运行的,因为它不知道子进程什么时候会退出。
如果有 10 个子进程同时退出,这样做可以嘛?只有一半退出又该怎么办呢 ?可以采用 while 循环等待的方式,去应对多个子进程同时退出的场景。可以通过非阻塞去解决只有一半子进程退出的场景,因为已经设置成循环等待了。
此时如果是阻塞等待,那么在一个进程退出后,进到 handler 方法里面,handler进入 while循环,开始调用 waitpid(-1, NULL, NULL)。每一个子进程都要等待其他新的子进程退出才能退出,这样handler方法就会很长时间甚至子进程非常多的时候一直无法退出,整个父进程就卡住了
但是下面的代码是非阻塞等待,当一个进程退出后,进入到 handler 方法里面,
#include <iostream>
#include <signal.h>
#include <cstring>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>using namespace std;void handler(int signum)
{pid_t ret;cout << "cat a signal, signum is: " << signum << endl;while ((ret = waitpid(-1, nullptr, WNOHANG)) > 0) // 非阻塞式等待,防止只有一半子进程退出,卡在这里{cout << "wait " << ret << " success" << endl;}
}int main()
{signal(17, handler);for (int i = 0; i < 10; i++){pid_t id = fork();if (id == 0){// childint cnt = 5;while (cnt--){cout << "I am child, pid: " << getpid() << endl;sleep(1);}exit(0);}sleep(2);}// fatherwhile (true){cout << "I am father, pid: " << getpid() << endl;sleep(1);}
}
父进程调用 sigaction 函数,将 SIGCHLD 的处理动作设置为 SIG_IGN,这样 fork 出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。
系统默认的忽略动作和用户 sigaction 函数自定义的忽略,通常是没有区别的,但这是一个特例。此方法只对 Linux 可用,不保证在其它 UNIX 系统上都可用。
十、完结

