专题网站策划书,做网站桂林,wordpress flash,如何将微信公众号文章转wordpress信号保存信号处理 1.信号保存1.1信号其他相关概念1.2信号在内核中的表示 2.信号处理2.1信号的捕捉流程2.2sigset_t2.3信号集操作函数2.4实操2.5捕捉信号的方法 3.可重入函数4.volatile5.SIGCHLD信号 自我名言#xff1a;只有努力#xff0c;才能追逐梦想#xff0c;只有努力… 信号保存信号处理 1.信号保存1.1信号其他相关概念1.2信号在内核中的表示 2.信号处理2.1信号的捕捉流程2.2sigset_t2.3信号集操作函数2.4实操2.5捕捉信号的方法 3.可重入函数4.volatile5.SIGCHLD信号 自我名言只有努力才能追逐梦想只有努力才不会欺骗自己。 喜欢的点赞收藏关注一下把 上一篇博客我们已经学过信号预备知识和信号的产生今天讲讲信号保存信号处理以及其他补充知识。
1.信号保存
1.1信号其他相关概念
补充一些概念。
实际执行信号的处理动作称为信号递达(Delivery)信号从产生到递达之间的状态,称为信号未决(Pending)。进程可以选择 阻塞 (Block) 某个信号。被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
阻塞和未决是两种状态。
1.2信号在内核中的表示
我们在OS内部在进程的内部我们要保存信号的周边信息我们其实是有三种数据结构与我们的信号是强相关的。 第一种数据结构我们称为pending表。其实这个pending表就是一张位图。
之前我们说过我们的进程可能在任意时刻收到OS给它发送的任意信号而该信号可能并不会被立即处理所有它要暂时被保存。之前我们又说过,进程为了保存信号采用的是位图结构来保存它收到的信号那么我们曾经所说的那张位图在今天它就叫做pending位图而凡是对应的信号被置为pending位图我们称该信号处于未决状态。 第二种数据结构我们称为block表。block表也是一张位图。 假设OS发送2号信号但2号信号被阻塞了。 也可以给某个信号预先设置阻塞状态
在说第三种数据结构handler表我写一个东西看看大家认不认识这个东西
typedef void(*handler_t)(int signo) 这其实这是一个函数指针。 所以可以在内核当中可以定义一个函数指针数组来保存信号抵达的所有方法。
handler_t handler[32]{0};这三种数据结构是内核中信号的基本数据结构构成。
我们之前写的signal(signo,handler)捕捉方法本质上就是拿着signo信号在对应的数组中查找对应的位置将用户层所设置的该信号handler处理方法的函数地址填入到对应信号所对应数组下标里未来当信号产生时修改pending表对应信号的比特位由0-1且该比特位没有被阻塞OS拿着这个信号根据信号的位置反向得到信号编号进而根据信号编号访问数组得到该信号的处理方法。
结论 1.如果一个信号没有产生并不妨碍它可以先被阻塞。 2.进程为何能够识别信号 在上一篇信号产生我们是这样说的进程本身就是程序员编写的属性和逻辑的集合。所以粗略的说是由程序员编码完成的。当然这是没错的。现在我们说的更仔细一点。 程序员在设计信号这一套体系或机制的时候在内核中给每一个进程设置了对应的三种数据结构分别是pending位图block位图handler表这三种结构组合起来就能完成识别信号的目的。
但是现在还有一个小小的问题。就是信号现在能产生了我觉得没问题可是它是用位图来产生的用位图来保存的可以用一个比特位来表示是否收到该信号这些都没有什么问题。但是呢我是一个用户我不断ctrlc假设我ctrlc100次。又或者这个进程因为某些原因而导致自己在某个时间段收到了大量的同一个信号假如收到的是2号信号把比特位由0-1可是你是位图啊只能记录下一个对应信号产生了那如果我现在同时来了大量的相同信号那此时是不是我们只能记录下一个呢只能记录下一个是不是意味着其他的信号丢失了呢?
答案是的我们在linux中学习的信号是普通信号普通信号当我收到多次并且没有来的及抵达处理的时候那么对应的信号它只会统计一次那么在处理一次之后后序剩下的相同信号就相当于被丢失了。
2.信号处理
2.1信号的捕捉流程
之前说过信号产生的时候不会被立即处理而是在合适的时候处理。 什么合适的时候 从内核态返回用户态的时候进行处理
先来了解什么是用户态和内核态。 以前我们在谈系统编程的时候我们一直在谈一个概念用户代码和内核代码这样的概念。其中我们要有两个重要概念叫做平时我们所写的代码比如数据结构的代码算法的代码还有你自己所写的所有代码在编译运行之后全部都是运行在用户态的。也就是说自己写的代码是属于用户态的代码。但是在自己写代码的时候难免会访问两种资源。 无论是操作系统自身资源还是硬件资源都是属于操作系统及其操作系统之下。 用户在自己写的代码访问这些资源必须直接或间接访问去访问操作系统提供的接口这批接口我们称为系统调用接口。 除了系统调用接口这件事情还有一个事情普通用户无法以自己用户态的身份来执行系统调用接口必须让自己的状态变成内核态。 就比如说在你还是学生时学校有些地方你是不能去的。然后当你毕业然后回当初学校当老师你会发行你以前不能去的地方现在都能去了。你依旧是你但是你的身份发送了变化那么你的权限级别也要发送变化。
换句话说系统调用除了我们在调用时调用这个函数还有一个就是我们还会发生身份的变化从用户态-内核态 其中对我们来讲因为我们从用户态到内核态身份的转变还要去调用OS内部的代码所以一般系统调用比我自己在应用层调自己写的函数方法成本要高(直接调用)所以往往系统系统调用比较费时间一些。
简单来说就是你现在要执行系统调用并不是你直接调用OS的代码首先你要将你的身份做变化(用户态-内核态)然后才能执行系统调用。 所以系统调用一定比你自己写个函数去调用成本要高因此尽量避免频繁调用系统调用。
那现在就有一个问题了。说起来用户态和内核态还是不太理解啊我只知道从用户态到内核态就是权限变大了。从内核态到用户态权限变小了。那我怎么知道我当前是在用户态还是在内核态呢或者说那怎么知道当前进程在运行什么时候是用户态什么时候是内核态
进程在实际执行时要把上下文数据投递到CPU中。 CPU中不仅有保存数据的寄存器还有非常多的寄存器在进程中有特定用途。
其实还有一个CR3寄存器表征当前进程的运行级别。 如何知道当前进程是用户态还是内核态很简单只要查看CPU对应的CR3寄存器是为0还是3就可以辨别出当前进程的运行级别。
虽然说了这么多但是还有一个问题 我一直不太理解我是一个进程怎么跑到OS中执行方法呢 当前进程正在占有CPU比如说当前进程有一个系统调用系统调用是我现在这个进程要跑过去调用对应这个函数这个方法说白了就是OS给我提供的方法。那么这个系统调用函数在哪里又是如何跑到OS中执行这个方法的呢
以前我们说过的进程加载到内存进程要通过页表的映射访问物理内存的代码和数据。这些都没有什么问题
可是当时有一个东西没有讲我们以前谈的进程地址空间都是用户空间0-3G图上还有一个内核空间3-4G没有说过这个空间是干什么的呢今天我们就来说说。
内核空间是用让当前进程来映射OS的。这里我们就不得不在引入一个概念。当时我们就说过进程地址空间和物理内存之间是通过页表来建立映射关系的。而进程的代码和数据是通过用户级页表与物理内存建立映射关系的。 除此之外OS内部它还维护了一张内核级页表。内核级页表是OS为了维护从虚拟地址到物理地址之间的OS级别的代码所构建的一张内核级映射表。 当你电脑开机说白了就是将操作系统的代码和数据加载到内存而操作系统的代码和数据在内存中只会存在一份不像进程的代码和数据可以有很多份。
操作系统在物理内存中的代码只有独一份并且将内核级代码和数据映射到当前进程内核空间(3-4G)只需要使用内核级页表就行了而每个进程都有(3-4G)内核空间要进行这种映射所以内核级页表只有一份就够了。 也可以理解成CPU内部也有一个寄存器指向内核级页表以后进程切换时这个寄存器不变就可以了。 所以每个进程都可以在自己的进程地址空间特定的区域内以内核级页表的方式经过内核级页表的映射去访问操作系统的代码和数据。
现在我们又知道了每个进程(3-4G)内核空间是属于操作系统的映射所以我们进程建立映射时可不仅仅是把用户的代码和数据和我们的进程产生关联每一个进程还要通过内核级页表和OS产生关联。 现在你的代码调用了系统调用其实就是在进程的上下文中跳转到内核空间找到对应方法通过内核页表映射找到OS代码执行完之后返回到你的代码处继续往下执行。
每个进程都有(3-4G)内核空间都会共享一个内核级页表无论进程如何切换都不会更改(3-4G)内核空间所以每个进程都能去访问OS。
虽然我知道每个进程也都能去访问OS但是还有问题。 1.进程凭什么能够执行内核的接口和数据呢?(或者跳转到(3-4G)内核空间呢?) 当进程想访问OS代码OS捕捉到这个行为先去读取CPU内部CPR寄存器如果发现当前进程的运行级别是0内核态才允许访问3就不允许。
2.用户什么时候从用户态就变成了内核态的 系统调用接口起始的位置会帮你做的。
3.怎么做的 linux中有一条中断汇编指令叫做int 中断编号 80int 80这个汇编指令----陷入内核它就帮我们把用户态改成内核态。
到目前为止我们把用户态和内核态及其相关概念说完了。但是我还是没有说信号怎么被处理的。接下来我们就开始。 从上面得知系统调用进程会进入内核态。 那假设我写的代码确实没有调用任何的系统调用接口就比如我就写了一个算法只是用CPU资源把代码放上去让CPU去跑。那这样的进程收到了信号就不会处理任何信号了吗 其实并不是的。你的代码再跑的时候有用户态的代码在跑也还有内核态代码也在跑。虽然你的进程没有用过这些资源但是你的进程要被OS调度。当时间片到达时即使你的进程没有任何调用接口或者其他原因导致你陷入内核但你一定要被调用只要被调度一定要把你这个进程从CPU上剥离下来在放上其他进程或线程谁拿的你呢—OS所以无论是主观还是客观上或多或少都会涉及内核态的切换过程。 信号捕捉的过程。
假设你的进程执行到系统调用经过一系列工作在采用内核级页表访问内核函数。执行完函数之后正常情况下是不是返回到系统调用处然后继续往下执行代码。但是一般不会这样干好不容易来一趟不干点其他事情怎么能好意思呢陷入内核本来就需要时间。所以OS在你返回的时候会做一件事情当前我们处于内核态返回之前也还是处于内核态是一个权限状态比较高的状态。所以OS在在对应的返回之前找到task_struct也就找到了三张表。 先查block表如果没有被阻塞然后再查pending表(循环往下查找block表和pending表如果没有信号。再往下面找如果对应信号位置block表被阻塞了直接往下走。对应信号位置block表没有被阻塞并且pending表有信号就读handler表三种处理动作默认忽略自定义。
默认大部分都是终止进程因为我现在是内核态啊所以直接终止进程。 忽略也处理这个信号但是什么都不做把比特位由1-0 自定义是最难的handler方法是我们自己写的这个方法是在用户态自己写的。只能跳转去执行。那没办法就跳转把。 那这里就有一个问题了我们能不能以内核态的身份执行用户态代码呢 答案是不能OS不相信任何人万一你在handler方法进行非法损坏OS呢那OS就没有办法做出处理。所以即便你现在身份是内核态你能去执行handler方法OS也不能让你这样干。
当执行自定义的handler方法时有特定的调用将自己的身份重新更改为用户态 并且调完handler方法(信号抵达)后也不能直接从用户态返回到曾经调用系统调用的位置往下继续执行代码。因为不能确定从哪里跳转的。 必须要在经过特定的系统调用返回内核态
再经过特定的系统调用从内核态返回调用的位置往下执行代码 到目前我们信号捕捉的所有流程才全部完成。关于几个特定的调用先不管。 现在这个流程有点复杂啊我们来根据刚才的思路画一下简化这个流程。
当我正在执行代码时经过一些特定的方式进入了内核。处理完内核工作后检测当前进程的三张表检测之后发现有信号要处理 然后我就去捕捉这个信号捕捉完成之后我在回到内核里回到内核里我在进行返回返回到当时的地方在往下执行代码。 这个像不像倒写的8
2.2sigset_t
内核中有两张位图分别是pending位图和block位图可这两张位图是内核的数据结构用户是没办法直接修改的而且如果你想改2个改10个呢OS没有办法设置十几个参数的函数调起来太麻烦OS为了能够让我们能够更好的使用信号所以给统一定义了一个 sigset_t类型 从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。 阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。
所以这个sigset_t类型它是OS为了用户更好设置当前进程的pending表和block表而设置的的用户级数据结构它是位图结构。我们一般称sigset_t为信号集而我们的信号集分为两种pending信号集block信号集block信号集我们一般称为信号屏蔽字。
sigset是什么东西我们已经知道了那怎么改呢先介绍下面的接口然后再演示。
2.3信号集操作函数 函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含 任何有效信号。
函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位为1,表示 该信号集的有效信号包括系统支持的所有信号。
注意,在使用sigset_ t类型的变量之前,一定要调 用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。
这四个函数都是成功返回0,出错返回-1。
sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1。 sigprocmask函数 那个进程调用这个函数就可以修改那个进程的block表。
how你想怎么修改屏蔽信号。 下表是how参数的可选值
SIG_BLOCKset包含了我们希望添加到当前信号屏蔽字的信号在原来的信号屏蔽字基础上做追加新增SIG_UNBLOCKset包含了我们希望从当前信号屏蔽子中解除阻塞的信号在当前信号屏蔽字中解除指定的信号SIG_SETMASK设置当前信号屏蔽字为set所指向的值重置当前信号屏蔽字
set做修改用的信号集
oset如果oset是非空指针则读取进程读取信号屏蔽字通过oset参数传出也就是说你想修改信号屏蔽字oset是将修改之前的信号屏蔽字返回 sigpending函数 获取当前进程的pending信号集
set通过用户定义的sigset_t类型对象获取该进程的pending位图。
2.4实操
函数介绍完了那现在来实操一下。 接下来我想实现这样的代码。 1.默认情况下我们的所有信号都不是被阻塞的 2.默认情况下如果一个信号屏蔽了该信号不会被抵达
现在以2号信号为例在刚开始的时候还没有收到2号信号pengding位图全是0然后我们阻塞了2号信号那2号信号永远不能被抵达那2号信号在那呢这个信号必须一直被保存在pending位图中我想一直打印出pending位图能看到这样这样的效果。没收到2号信号pending位图全是0收到但被屏蔽2号信号所在位置一直是1。
#define BLOCK_SIGNAL 2
int main()
{//1.先尝试屏蔽指定信号sigset_t block,oblock;//1.1初始化sigemptyset(block);sigemptyset(oblock);//1.2添加要屏蔽的信号sigaddset(block,BLOCK_SIGNAL);return 0;
}到1.2我给当前进程添加了2号屏蔽信号了吗 其实并没有我只是给当前block信号集添加了2号信号。
int main()
{//1.先尝试屏蔽指定信号sigset_t block,oblock;//1.1初始化sigemptyset(block);sigemptyset(oblock);//1.2添加要屏蔽的信号sigaddset(block,BLOCK_SIGNAL);//1.3开始屏蔽设置进内核进程sigprocmask(SIG_SETMASK,block,oblock);return 0;
}1.3才是把包含2号信号的信号集设置进当前进程。 第一个参数选SIG_BLOCK也可以。
#includeiostream
#includesignal.h
#includeunistd.husing namespace std;#define BLOCK_SIGNAL 2
#define MAX_SIGNUM 31void show_pending(const sigset_t pending)
{for(int signoMAX_SIGNUM;signo1;signo--){if(sigismember(pending,signo)){cout1;}else{cout0;}}coutendl;
}int main()
{//1.先尝试屏蔽指定信号sigset_t block,oblock;//1.1初始化sigemptyset(block);sigemptyset(oblock);//1.2添加要屏蔽的信号sigaddset(block,BLOCK_SIGNAL);//1.3开始屏蔽设置进内核进程sigprocmask(SIG_SETMASK,block,oblock);//2.遍历打印pending信号集sigset_t pending;while(true){//2.1初始化sigemptyset(pending);//2.2获取pending信号集sigpending(pending);//打印show_pending(pending);sleep(1);}return 0;
}运行结果是我想要的。
现在我还想看到当我解除对2号信号屏蔽信号被抵达之后pending位图就没有2号有效信号了。 //2.遍历打印pending信号集int cnt5;sigset_t pending;while(true){//2.1初始化sigemptyset(pending);//2.2获取pending信号集sigpending(pending);//打印show_pending(pending);sleep(1);if(cnt-- 0){ sigprocmask(SIG_SETMASK,oblock,block);cout解除对信号的屏蔽不屏蔽任何信号endl;}}不对啊运行结果与我预期不符并且进程怎么直接终止了连这句话也没给我打印。 修改一下代码看一下 //2.遍历打印pending信号集int cnt5;sigset_t pending;while(true){//2.1初始化sigemptyset(pending);//2.2获取pending信号集sigpending(pending);//打印show_pending(pending);sleep(1);if(cnt-- 0){ cout解除对信号的屏蔽不屏蔽任何信号endl;sigprocmask(SIG_SETMASK,oblock,block);}}这句话现在执行了那为什么后面进程直接终止了呢 原因很简单因为ctrlc的时候对2号信号的处理是默认的。默认动作是终止这个进程。所有当sigprocmask解除对2号信号的屏蔽那么此时这个进程就退出了。 那为什么这样写代码看不到刚刚的打印呢 因为一旦对特定的信号进行解除屏蔽一般OS要至少立马抵达一个信号 也就是说当你系统调用解除对2号信号的屏蔽解除屏蔽了然后从内核态返回用户态的时OS顺手就帮你把2号信号抵达了只不过抵达的时候默认处理动作是终止进程终止进程不需要返回到用户态了所有你看到这个进程就直接退出了不会再执行后面的语句。
但我就是想看见2号信号由0-1在由1-0怎么办很简单把2号信号进行自定义捕捉不让收到2号信号进程就退出就好了。
#includeiostream
#includesignal.h
#includeunistd.husing namespace std;#define BLOCK_SIGNAL 2
#define MAX_SIGNUM 31void show_pending(const sigset_t pending)
{for(int signoMAX_SIGNUM;signo1;signo--){if(sigismember(pending,signo)){cout1;}else{cout0;}}coutendl;
}void handler(int signo)
{coutsigno号信号已被抵达endl;
}int main()
{signal(2,handler);//1.先尝试屏蔽指定信号sigset_t block,oblock;//1.1初始化sigemptyset(block);sigemptyset(oblock);//1.2添加要屏蔽的信号sigaddset(block,BLOCK_SIGNAL);//1.3开始屏蔽设置进内核进程sigprocmask(SIG_SETMASK,block,oblock);//2.遍历打印pending信号集int cnt5;sigset_t pending;while(true){//2.1初始化sigemptyset(pending);//2.2获取pending信号集sigpending(pending);//打印show_pending(pending);sleep(1);if(cnt-- 0){ cout解除对信号的屏蔽不屏蔽任何信号endl; sigprocmask(SIG_SETMASK,oblock,block);}}return 0;
}如果想一次屏蔽多次信号可以使用vector
#includeiostream
#includesignal.h
#includeunistd.h
#includevectorusing namespace std;#define BLOCK_SIGNAL 2
#define MAX_SIGNUM 31vectorint sigarr{2,3};void show_pending(const sigset_t pending)
{for(int signoMAX_SIGNUM;signo1;signo--){if(sigismember(pending,signo)){cout1;}else{cout0;}}coutendl;
}void handler(int signo)
{coutsigno号信号已被抵达endl;
}int main()
{for(auto sig:sigarr) signal(sig,handler);//1.先尝试屏蔽指定信号sigset_t block,oblock;//1.1初始化sigemptyset(block);sigemptyset(oblock);//1.2添加要屏蔽的信号for(auto sig:sigarr) sigaddset(block,sig);//1.3开始屏蔽设置进内核进程sigprocmask(SIG_SETMASK,block,oblock);//2.遍历打印pending信号集int cnt5;sigset_t pending;while(true){//2.1初始化sigemptyset(pending);//2.2获取pending信号集sigpending(pending);//打印show_pending(pending);sleep(1);if(cnt-- 0){ cout解除对信号的屏蔽不屏蔽任何信号endl; sigprocmask(SIG_SETMASK,oblock,block);}}return 0;
}想一想能不能屏蔽9号信号呢 9号信号屏蔽不了也不能自定义。可以自己试试看。
2.5捕捉信号的方法
目前我们就学了一种捕捉信号的方法signal函数使用比较简单当然非常推荐。 接下来我们在学一个函数。 sigaction 和signal作用一模一样对特定信号设置特定的回调方法当触发信号时执行对应的捕捉动作。 不过它多了一个结构体sigaction你没有看错它结构体的名称和函数体的名称是一样的。以前再写C的时候不会出现类型名和函数名一样的情况但事实上它确实可以。 signum特定的信号对象 act你要设置特定的捕捉方法就要设置一个结构体对象进去(输入型参数) 这个结构体第一个参数 这就是我们曾经讲过signal函数要设置的函数指针。 实际要捕捉一个信号时你要先定义一个结构体对象然后把自定义捕捉方法设置进结构体对象中进行相关对应的操作。 暂时不用管设置为nullptr。 同样不用管设置为0。 sa_mask的类型是sigset_t它是一个信号集OS为了方便用户更好设置当前进程的pending表和block表而设置的用户级数据结构。它的具体作用是什么呢我们在写代码的时候再说。
这个sigaction结构体只需要关心sa_handlersa_mask就可以了。 oldact是一个输出型参数它是为了能够获取对于特定信号旧的处理方法。 接下来用用这个函数。
#includeiostream
#includesignal.h
#includeunistd.husing namespace std;void handler(int signo)
{coutget a signo: signoendl;
}int main()
{struct sigaction act,oact;act.sa_handlerhandler;act.sa_flags0;act.sa_mask;//是干什么的先不说但是它是信号集先初始化一下sigemptyset(act.sa_mask);sigaction(2,act,oact);while(true) sleep(1);return 0;
}也确实能够捕捉2号信号。和signal作用一样那这个函数到底和signal函数有什么差别呢
先来做一个小实验。
void handler(int signo)
{coutget a signosigno正在处理中...endl;sleep(20);
}当我们正在处理2号信号时如果再来一个2号信号呢如果系统允许我对信号再抵达那我们是不是系统正在处理2号信号内部它又反过头在递归似的在调handler。换句话说一个进程再未来时可能收到大量同类型的信号如果收到同类型的信号但我当前正在处理某一种信号时那么接下来会有什么问题OS允不允许我进行频繁的信号提交呢
修改一下代码看的更清楚一些
void count(int cnt)
{while(cnt){printf(cnt :%2d\r,cnt);fflush(stdout);cnt--;sleep(1);}
}void handler(int signo)
{coutget a signo: signo正在处理中...endl;count(20);
} 由上图可以看到当正在执行2号信号时后序同类型信号来了没有被递归似处理并且发了这么多2号信号只保留了前两个后面的都丢弃了。这是现象基于这个现象我们来阐述一下结论。
当我们进行正在抵达某一个信号时同类型信号无法抵达! 因为当前信号正在被捕捉系统会自动将当前信号加入到进程的信号屏蔽字(block表)中。 当信号完成捕捉动作系统又会自动解除对信号的屏蔽。
那为什么我们发送一堆2号信号只执行了两次捕捉呢不是系统又自动解除对信号的屏蔽了吗既然只执行2次那后序信号去哪里了
当信号要被抵达时OS会先将2号信号pengding位图位置由1-0然后再抵达当正在抵达的时候这时你在发送2号信号就可以再把pending位图由0-1你当然可以一直发送2号信号但只有一张pending位图你只可以改一次后序的这些2号信号根本没有意义所有当我们把对应信号处理完之后看到pengding表还有一个2号信号就又处理了一次。
一般一个信号被解除屏蔽的时候会自动抵达当前屏蔽的信号如果该信号已经被pending的话(也就是该信号在pending位图中的位置又由0-1)没有就不做任何动作。
我们进程处理信号的原则是串行的处理同类型的信号不允许递归。
接下来我们这个sa_mask的作用。 sa_mask 当我们正在处理某一种信号的时候我们也想顺便屏蔽其他信号就可以添加到这个sa_mask中。 就如上图正在处理2号信号但是3号信号照样可以干掉它因为你只会屏蔽2号信号处理那个屏蔽那个这是OS自动做的。 如果今天你也想把其他信号也屏蔽了那么你就可以通过设置sa_mask来完成。
void count(int cnt)
{while(cnt){printf(cnt :%2d\r,cnt);fflush(stdout);cnt--;sleep(1);}
}void handler(int signo)
{coutget a signo: signo正在处理中...endl;count(20);
}int main()
{ struct sigaction act,oact;act.sa_handlerhandler;act.sa_flags0;act.sa_mask;sigemptyset(act.sa_mask);sigaddset(act.sa_mask,3);//将3号信号也屏蔽掉sigaction(2,act,oact);while(true) sleep(1);return 0;
}刚才我发3号信号可以把进程终止现在发3号信号进程不会终止证明确实把3号信号屏蔽了。 但我2号信号发了这么多怎么进程最后退出了 当信号抵达完OS会自动解除被抵达的信号的屏蔽以及你刚才顺便屏蔽的其他信号。 所以第一次2号信号抵达的时候3号信号也被屏蔽了所以你发3号信号没有用当2号信号抵达完成之后解除屏蔽3号信号也被解除屏蔽可是你别忘了你还有一个2号信号这个2号信号被抵达时3号信号又被屏蔽了来不及处理最后2号信号再次抵达完成解除2号屏蔽和3号信号的屏蔽刚才说了发送一堆就执行两次2号信号的原因现在3号信号就可以抵达了。
至此我们的信号终于全部讲完。内容比较干值得好好学习。
接下来我们再说说与信号相关的一些知识。
3.可重入函数 上面是不带头单向链表的头插。我们看到的main函数中正在执行头插函数头插node1完成了第1步正在准备往下走到第4步的时候因为一些原因触发了信号捕捉的一些动作(比如进程的时间片到了进程被挂起零点几秒又被唤醒唤醒之后从内核态返回用户态做信号检测发现有信号然后就跑过去执行handler方法了)可是handler方法很不幸它的内部也做了insert方法把node2头插入链表。插入之后 返回handler方法执行完之后也返回了然后进行第4步。 head就不再指向node2而指向node1了最终导致内存泄漏。我们代码也没有写错就因为正在执行主执行流(main执行流)突然主执行流在某些特定时间段内而导致信号捕捉而又插入node2最终导致内存泄漏。
一般而言我们认为main执行流和信号捕捉执行流只两个信号流。而在这两套执行流中 main执行流进入insert函数正在访问这个函数期间另一个执行流也进入到这个函数并且因为这两个执行流都在重复进入insert函数导致代码出现出错情况我们将insert称为不可重入函数。
1.一般而言我们认为main执行流和信号捕捉执行流只两个信号流。
虽然在当前我们的代码中执行是串行的main执行流要停下来才能执行handlerhandler在执行时main就没执行了 当我正在执行main函数对应的方法时handler方法一定没有调用因为我们只有一个进程所以理论上其实我们只有一个执行流。 但是我们可以发现main执行流它和handler方法并不是正常调过去的而是信号到来它回调过去的直接执行自定义方法最典型的代表我设置了一个捕捉方法如果这个信号永远没有发生那么只有main执行流在跑但如果有这个信号产生了对应的信号捕捉方法也被执行了这意味着我们正在执行main方法可能跑过去执行其他方法了所以我们认为它们时两个信号流。
2.如果在main中和在handler中该函数被重复进入出问题-----称该函数(insert)为不可重入函数。 如果在main中和在handler中该函数被重复进入没有出问题-----称该函数(insert)为可重入函数。
那这个可重入函数和不可重入函数是问题吗 并不是我们目前大部分情况下使用的接口有个75%全部都是不可重入。 这个可重入函数和不可重入函数是特性是一个中性词。
如果一个函数符合以下条件之一则是不可重入的: 调用了malloc或free,因为malloc也是用全局链表来管理堆的。 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。
4.volatile
C语言有32个关键字其中大部分都见过使用过但是volatile很少使用今天我们站在信号的角度重新理解一下。
看下面一段代码捕捉到2号信号让quit变成1然后循环条件不符合就终止循环。往下执行代码。
#includestdio.hint quit0;void handler(int signo)
{printf(%d 号信号正在被捕捉!\n,signo);printf(quit: %d,quit);quit1;printf(- %d\n,quit);
}int main()
{signal(2,handler);while(!quit);printf(注意我是正常退出的\n);return 0;
}得到这个结果不是很正常嘛就和没讲一样。 下面我们做一个工作在gcc或g在进行编译的时候不知道有没有听说一个概念编译器优化。我们刚才的代码有没有编译器优化呢是有的不过优化的不是 那么过分。现在呢我想把我们的代码优化等级提高一下 像我们一般的优化级别有下面这么多。我们默认的优化是O1,O2这样的级别。 现在我们把优化级别改成O3。 我发了2号信号quit由0-1了为什么优化后进程不退出了 多发几次quit就是1while循环条件不满足进程应该退出的。但是进程还是没有退出。为什么
既然说了优化这肯定和优化有关循环没退出证明条件不满足但是我quit已经变成1了这优化把我的quit优化到哪里去了。 申请quit全局变量肯定是要在物理内存给我把对应的空间开辟出来。CPU在运算的时候永远要做3件事情。 前3条指令跟CPU本身有关而最后一个属于我们的固定流程数据处理完了如果用户需要就给用户写回去。
如何理解while呢 while循环对quit做检测逻辑上它应该重复着做取对应的quit内容然后在里面做判断。换句话说它就不断的从内存中取数据。 可是呢我们刚刚讲过main执行流和信号捕捉指令流是两个执行流当编译器在一般优化的时候它默认遵守取quit在CPU里判断判断为真然后执行后序代码只不过后序代码为空。然后不断从内存中取数据执行上述操作。所以当你不做优化时你进行信号捕捉执行信号捕捉方法把quit改1改1之后回到main执行流main执行流还依旧在做读取quit值读到CPU里做逻辑运算结果为false循环条件不满足就执行打印。没有优化时它是这样的。
可是我们后来带来一个优化选项所谓的优化就是编译器觉得在main执行流中发现while循环中quit只被做检测没被做修改它就觉得既然没修改就可以直接在编译器编译时建议它当然quit是全局变量空间还要开辟只不过在正常运行时默认优先把quit的0值直接放到了寄存器从此往后在检测再也不去做从内存中load数据的行为了while循环检测这条语句执行时只是再看寄存器的值因为这个值是0所以条件一直被满足后来做信号捕捉时把quit改1了修改的是内存中的quit的值由0-1和我当前在CPU内保存的预加载的优化到寄存器的quit没有任何关心所以呢你再怎么改内存中的值寄存器中的值不变while循环一直处于检测值为0逻辑变为真所以即使你确实是把quit值改了程序一直不退出。 所以就可能会存在代码没有问题因为编译器优化级别和策略(不同编译器优化级别不一样)而导致我们的程序没有按照我们的预期来进行工作。
所以为了解决这样的问题我们就需要使用volatile关键字。 在C/C的范畴里只谈一个常见作用叫做保持内存可见性 大白话就是while循环在这里做检测虽然quit不会再main执行流中做修改但以后在检测quit值时请不要给我给我优化到寄存器里我要时时刻刻每一次检测都要从内存里读我要保持内存的可见性而不是每次读都通过寄存器来覆盖我们物理内存当中的某个变量。这就叫做保持内存可见性。
同样的代码我们给quit带一个volatile再看一下效果。
#includestdio.hvolatile int quit0;void handler(int signo)
{printf(%d 号信号正在被捕捉!\n,signo);printf(quit: %d,quit);quit1;printf(- %d\n,quit);
}int main()
{signal(2,handler);while(!quit);printf(注意我是正常退出的\n);return 0;
}和最开始的一样可以正常退出。
所以以后写全局变量万一代码不通过或编译通过但结果不符合预期时代码怎么查问题都找不到要想到这个场景。
5.SIGCHLD信号
这个信号和进程等待有关。 在以前讲进程等待时说过一句话如果一个子进程退出了那么它就会变成僵尸状态然后呢让我们的父进程去读取它父进程在等待子进程时如果子进程并没有退出那么父进程就要阻塞或非阻塞去等待子进程直到子进程退出了父进程wait就会成功返回获取子进程的退出码退出信号。这些都是之前说过的。 可是呢有一个事实是以前刻意回避的这个事实就是当子进程在退出时它会僵尸但它不是默默无闻的进入僵尸状态而是它在死的时候告诉父进程我死掉了也就是父进程在进行某种工作的时候子进程在它死亡时会主动告诉父进程我死了。 它是通过向父进程发送SIGCHLD信号来告知父进程自身的死亡。
子进程在死亡的时候会向父进程直接发送SIGCHLD信号通过这个信号告知父进程我死了。只不过对于SIGCHLD(17号)这个信号父进程默认处理动作是Ign(忽略)这个忽略是内核级的忽略和我们等下讲的忽略有一点点差别。 怎么证明呢子进程在死亡的时候会向父进程直接发送SIGCHLD信号。
#includestdio.h
#includeunistd.h
#includestdlib.h
#includesignal.hvoid count(int cnt)
{while(cnt){printf(cnt :%2d\r,cnt);fflush(stdout);cnt--;sleep(1);}printf(\n);
}void handler(int signo)
{printf(pid :%d, %d号信号,正在被捕捉!\n,getpid(),signo);
}int main()
{signal(SIGCHLD,handler);printf(我是父进程,pid: %d, ppid :%d\n,getpid(),getppid());pid_t idfork();if(id 0){printf(我是子进程,pid: %d, ppid: %d,我要退出啦\n,getpid(),getppid());count(5);exit(1);}while(1) sleep(1);return 0;
}子进程退出确实向父进程发送SIGCHLD信号。
那有什么意义呢给我们带来什么好处呢 好处就是以前我们并不知道子进程什么时候退出所以只能主动去调用waitpid和wait这样的函数如果子进程还没有退父进程还得拉着老脸一直在等并且不管是阻塞等还是非阻塞等父进程得一直问要不然就不知道子进程退了。 但今天就不一样了我捕捉一下SIGCHLD信号就意味着我以后再也不关心子进程了你退的时候告诉我我来执行对应的回收方法就可以了。所以我们就可以在handler方法里来对子进程进行回收。
可是如果就按照刚才的思路写代码这个代码并不是一个非常健壮的代码。为什么呢
1.有没有一种可能父进程有非常多的子进程在同一个时刻退出了。那此时有什么后果
void handler(int signo)
{//伪代码//1.父进程有非常多的子进程在同一个时刻退出了waitpid()//printf(pid :%d, %d号信号,正在被捕捉!\n,getpid(),signo);
}是不是同时都在向你发送SIGCHLD前面刚讲当你正在抵达SIGCHLD的时候其他SIGCHLD都会被屏蔽更重要的是保存这个信号的位图只有一个如果同时 来了SIGCHLD信号要被设置那位图只能设置一个其他的SIGCHLD信号都被丢失。换句话说如果你的handler方法里只调用一次waitpid()是不对的因为你会遗漏掉那些已经退出的子进程发送的SIGCHLD信号。waitpid()只会调用一次是不合理的。那怎么办呢这就决定了我们在等的时候必须循环去等。
void handler(int signo)
{//伪代码//1.父进程有非常多的子进程在同一个时刻退出了waitpid()----while(1)//printf(pid :%d, %d号信号,正在被捕捉!\n,getpid(),signo);
}while循环去等的意思是虽然我收到SIGCHLD信号子进程退出了但是我不确定有几个子进程退出那我就while循环调用waitpid()waitpid()第一个参数是要等待进程的pid我们可以设置成-1。
void handler(int signo)
{//伪代码//1.父进程有非常多的子进程在同一个时刻退出了waitpid(-1)----while(1)//printf(pid :%d, %d号信号,正在被捕捉!\n,getpid(),signo);
}这个意思是我可以等待任意一个子进程退出。所以呢我只要把waitpid()第一个参数设置为-1然后while循环一直回收直到waitpid()函数出错的时候出错就对应我把退出的子进程全都回收了。
2.有没有一种可能父进程有非常多的子进程在同一个时刻只有一部分退出了。假设有10个子进程只有5个退出了即使只有1个退出也得循环。循环把5个退出的子进程回收了那第6次还调不调waitpid()
void handler(int signo)
{//伪代码//1.父进程有非常多的子进程在同一个时刻退出了//waitpid(-1)----while(1)//2.父进程有非常多的子进程在同一个时刻退出一部分while(1){pid_t idwaitpid(-1,NULL,0);//阻塞等待}//printf(pid :%d, %d号信号,正在被捕捉!\n,getpid(),signo);
}举个栗子你父母关系非常好你爸会把工资给你妈你妈每个月给你爸500零花钱。具体你妈给你爸多少钱你也不知道有一天你找你爸要100块钱你是知道你妈每个月都给你爸零花钱的。你爸二话没说就给你了第二天你找你爸再要100你爸又给你了第三天同样如此请问第四天你还会找你爸要钱吗肯定会的啊。因为你要他就给你要他就给证明他还有钱。因为你没站在上帝视角不知道你爸还有多少钱。
同样道理10个子进程5退出了也回收了问你还要不要第6次你怎么知道5个进程退出了答案是你根本不知道因为刚才那句话是站在上帝视角才知道有5个退了。 站在进程的视角它只知道自己创建了10个进程根本不知道有几个子进程退出了。就和你一样根本不知道你爸有多少零花钱一样。所以把5个子进程回收了还要再进行waitpid()。注意waitpid()刚才可是阻塞等待。如果第6个第7个子进程没退那不就尴尬了你还在while循环中出什么问题了
当前你的进程在handler方法里就出现了阻塞式调用。阻塞式调用的时候你就无法返回了。
所以正常写代码你必须非阻塞式等待所有子进程
void handler(int signo)
{//伪代码//1.父进程有非常多的子进程在同一个时刻退出了//waitpid(-1)----while(1)//2.父进程有非常多的子进程在同一个时刻退出一部分while(1){//pid_t idwaitpid(-1,NULL,0);//阻塞等待pid_t idwaitpid(-1,NULL,WNOHANG);//非阻塞等待if(id 0) break;}//printf(pid :%d, %d号信号,正在被捕捉!\n,getpid(),signo);
}当有子进程退出的就直接回收当没有子进程退出也不会阻塞并且我在返回证明这轮的子进程全部回收完了。
下面是回收子进程的代码
#include stdio.h
#include stdlib.h
#include signal.hvoid handler(int sig)
{pid_t id;//0就回收,0或0出错就不回收了while( (id waitpid(-1, NULL, WNOHANG)) 0){printf(wait child success: %d\n, id);}printf(child is quit! %d\n, getpid());
}int main()
{signal(SIGCHLD, handler);pid_t idfork();if(id 0){printf(child : %d\n, getpid());sleep(3);exit(1);}while(1){printf(father proc is doing some thing!\n);sleep(1);}return 0;
}虽然说了这么多但在这里想说的还不是这个。 最重要的知识是其实在处理子进程退出的时候我们也可以选择不waitpid()它。 事实上由于UNIX的历史原因Linux是脱胎于UNIX的要想不产生僵尸进程除了以前讲的阻塞和非阻塞式等待以及信号式的等待还有一种就是你可以调用sigaction将SIGCHLD的处理动作显示设置为SIG_IGN(忽略)那么从此往后父进程可以不用在等子进程了子进程退出它会自动的变成僵尸然后自动的被OS自动的回收。不会再通知父进程了。
int main()
{//signal(SIGCHLD,handler);//显示的设置对SIGCHLD进行忽略signal(SIGCHLD,SIG_IGN);printf(我是父进程,pid: %d, ppid :%d\n,getpid(),getppid());pid_t idfork();if(id 0){printf(我是子进程,pid: %d, ppid: %d,我要退出啦\n,getpid(),getppid());count(5);exit(1);}while(1) sleep(1);return 0;
} 刚开始父子进程都在最后只剩下父进程了子进程自动被回收了。
如果你创建出一个子进程你再也不想等待它每次等待太恶心了那你就可以直接对SIGCHLD进行手动忽略。这种方法只再linux下有效其他不做保证。
这里还有最后一个问题。查信号手册的时候好像看到SIGCHLD对17号信号默认处理动作就是Ign(忽略)啊。 这里还显示设置SIGCHLD为SIG_IGN(忽略)。 它本来就是还设置干什么
默认处理动作是Ign和我们手动设置SIG_IGN表示出来的特性是不一样的当你使用默认的Ign时它还是我们之前那个流程收到信号就处理你处理你我就捕捉这个信号然后该等还是要等子进程退出了它会僵尸。但是如果我们设置了SIG_IGNOS内部它就会修改你可以理解成它直接修改的是未来创建子进程的时候因为你肯定是先设置signal函数 后面才调用fork当你fork的时候OS它会直接去识别对于子进程的处理动作是什么样子如果手动的用户设置了SIG_IGN系统就设置让子进程退出自动回收OS回收的。这个SIG_IGN和系统的Ign在数值和区分度一定是不一样的。 关于信号的内容终于全部搞定内容较干值得好好品尝