要处理信号, 我们进程就得知道自己是否收到了信号, 收到了哪些信号, 所以进程需要再合适的时候去查一查自己的pending位图 block 位图 和 hander表
, 什么时候进行检测呢?
当我们的进程从内核态
返回到用户态
的时候, 进行信号的检测和处理。
我们就先简单的解释一下嘛, 内核态最常见的时候就是我们在使用系统调用
的时候, 此时不仅仅我们要去执行系统调用里的代码, 我们还得有资格去访问操作系统内的资源, 所以此时操作系统会自动将我们的身份变化为内核身份, 这就是我们的内核态, 而用户态就是我们在执行我们自己编写的代码的时候的状态。
为什么要选择内核态返回到用户态的时候进行信号的检测呢?
因为可以保证此时进程一定没有在做重要的事情, 所以顺路就检测一下信号。
内核是如何实现信号捕捉的?
sigaction
接下来我们看看struct sigaction的结构
由于我们现在只关心普通信号, 所以我们只需要了解其中的第一个字段和第三个字段这两个字段就可以了。
其中 第一个其实就是一个函数指针, 他指向的就是处理这个信号的hander方法。
如果想快速的使用这个接口, 我们只需要使用第一个参数即可
问题1 pending位图是什么时候被处理的?
我们知道在处理完信号后pending位图会将信号对应的位置从1 -> 0, 但是, 具体是在什么时候变更的呢?
我们在handler方法的开头打印一下pending位图, 看看对于信号的位置是否为0, 如果为0那么就代表调用handler前pending就已经被处理了, 否则就是在调用玩handler后pending才被处理的。
可以发现2号位为0
所以就可以得出结论, 在调用handler前, 操作系统就已经将pending位图处理过了。
问题2 为什么在调用handler的时候要将对应的信号屏蔽呢?
因为如果我们hanlder里有系统调用, 那么就会存在内核态向用户态的变化, 就又会去检测信号, 可能会导致信号被嵌套调用
。
可重入函数
假如我们在执行链表的头插, 刚执行完p->next = head
进程就切走了, 切回来后我们会先进行信号的检测, 如果此时检测到一个信号然后去执行他的处理方法, 恰好他也要往链表头插, 那么这个函数有在handler中被进入了, 我们把这种现象称为函数的重复进入 简称 函数被重入了
, 也就是main执行流还没执行完呢, handler执行流又去执行这个函数了。
最后就会变成这种情况
最后node2节点就丢失了, 这样就出了问题。
如果一个函数在被重复进入的情况下, 可能会出错, 我们称这种函数为不可重入函数
否则称为可重入函数
。
显然, 我们刚刚的那个insert就是不可重入函数。
注意: 可重入 或 不可重入 都不是褒义 或者 贬义, 他们只是描述现象。
目前我们所学到的大部分函数都是不可重入函数。
volatile关键字
我们不加优化编译运行, 发现按下Ctrl+ c
后代码正常退出
而当我们加上优化后再运行
此时就发现退不了了。
由于不同的编译器处理策略不一样, 所以并不是所有的编译器都会发这个flag优化进CPU内寄存器的。
为什么这次没有退出呢?
我们可以看到, 当我们在按下ctrl + c 的时候, 打印出了catch 2 的消息, 说明此时flag应该是被置为1了的, 可是奇怪的是, 为什么进程没有退出呢?
正常情况下在执行!flag的时候, 是CPU先将flag从内存中读取到寄存器里, 然后再对flag执行逻辑运算, 而在优化后, 他发现了flag在内存中不会做修改, 那么他就不再去内存里拿flag了, 而是放在寄存器里直接用。
为了防止编译器的这种过分优化 我们可以使用volatile
关键字来修饰这个flag
此时我们就发现, 按下ctrl + c后我们进程还是可以正常退出的。
volatile关键字的作用就是 防止编译器过度优化, 保持内存的可见性。
SIGCHLD信号(17号信号) - 了解即可
所以我们父进程在等待子进程的时候, 可以根据信号进行异步等待
但是我们还是必须得保证父进程不能再子进程之前退出!
上诉的代码还是有问题的, 因为父进程的子进程可能有很多个, 如果他们同时退出的话就只能成功的回收其中的一个了。
事实上, 由于Unix 的历史原因(Linux是仿Unix的), 想要不产生僵尸进程还有一种办法就是在父进程中将SIGCHLD的处理动作设为SIG_IGN, 这样的话fork出来的子进程将会在终止后自动被清除, 不会产生僵尸进程, 也不会通知父进程。
(系统默认的忽略动作通常与用户设置的SIG_IGN是一样的, 但是在这里是一个特例
)
在官方手册里, 17号新号的默认动作其实不是SIG_IGN 而是 SIG_DFL, 只是SIG_DFL的动作是IGN也就是什么都不做。