本内容主要做回顾,以及对一些信号内核运行逻辑的补充,补充了很多通俗易懂的案例和讲解,说能听懂的话,讲能听懂的知识。
信号的基本模型
信号怎么产生?
计算机在运行的时候,会出现各种各样的异常,举一个很常见的例子,如果你是一个程序员,那么你肯定遇到过访问野指针的情况,再者在进行复杂计算的时候,一不小心将0作为了分母,这两种情况都会导致程序直接停止,再或者是一些段错误,总线错误等。还有很多,不一一列举,总之会产生非常多的异常。
这些异常也可以称之为事件,这都是可以的,下面我们所说的异常就代指事件,反之亦可。
虽然异常很多,但是总的可以分为这三类:
- 硬件异常(事件):CPU运行的时候检测到错误,例如在运算器中发现了除数为0的情况,还有段错误,总线错误等。
- 软件异常(事件):内核内部触发的事件,例如进程写管道的时候无读端,定时器到期等。
- 用户事件:用户主动通过系统调用发送信号给进程。
这其实是从人机交互的过程抽象出来的三个分类,硬件是最底层,软件是硬件与用户的接口。
这些异常触发之后,都会统一通知内核,然后内核产生对应的信号(信号一定是由内核发送给进程的),然后发送给正在运行的进程。
提示:所谓内核,就是操作系统的代码,这部分代码直接跟硬件挂钩,权限高,对用户的代码有一定的限制,尤其是高风险的代码(例如修改不属于他的内存)。下面提到的"内核会发送信号给进程", "内核鉴权和处理"等一系列需要内核做的事,本质上就是在执行内核的代码,但凡涉及到内核代码的执行,就离不开用户态向内核态的转变。内核执行完毕之后,再由内核态转回用户态。核心态的转变会带来额外的性能开销,比一般的调度消耗更大。
信号送给谁?
那肯定有人要问了,内核产生了信号,内核程序怎么知道要把信号发送给谁啊?
举个例子,如果公司的某个员工闯祸了,那么责任肯定是和这个员工高度绑定的,信号的机制也是如此,信号触发源本身会与特定进程或者进程组产生强上下文关联,内核从触发事件的上下文或用户态的显式指定中提取目标进程标识(PID / 进程组 ID 等)从而找到该进程,最终完成信号投递。
简单来说++就是一般都是谁引发的事件、谁关联的事件(这里的关联是指进程在闯祸之间就跟特定的事件绑定了),这个信号就发给谁,当然用户也可以制定信号的接受者。++
列举一些例子:
- 除数为0,这是最经典算术异常,其异常事件的绑定逻辑是,进程a执行了代码int var = 1 / 0, 那么就会被cpu检测到,于是立即出发硬件异常,这个异常直接由进程a导致,那么信号也将发送给进程a,a收到进程之后,执行默认的处置(暂且不管处置是什么意思,简单理解就是进程收到信号之后,默认执行的行为,就好像你妈叫你吃饭,那你就得去吃饭一样)。
上面的例子是直接由触发异常的进程来接受内核传递来的信号(除数为0异常)。
下面的一个例子是由内核关联的进程接受信号的例子:
- 内核在fork创建子进程的时候(创建进程的代码只有在内核态才能执行),会在子进程中记录父进程的PID,子进程在退出的时候,内核就会把子进程退出的信号发送给子进程中绑定的父进程PID,这就是所谓的关联。
但是用户可以摆脱这种硬件强制关联和软件内核绑定,直接让内核发送指定的信号给进程,内核只负责按用户的指令办事(鉴权+投递,所谓鉴权就是看你有没有权限执行)。下面是一个典型的场景:
- 用户发送kill -9 pid来强制结束指定的进程,本质就是让内核发送了一个sigkill信号给该进程
用户事件的核心特征就是事件和进程解绑,由用户来指定信号的接收者而非绑定和关联的进程。但是信号的发送还是得交给内核,不然用户就可以越权发送信号,会导致一些安全问题。
进程接受的信号存哪了?
这里提前告诉大家,内核发送给进程的信号,都被存储在了进程自己的数据结构中了,也就是PCB中,PCB中有一个未决信号仓库(未决可以理解为还未解决,方便理解),字段名为pending(adj.待处理的,待定的)。所有的信号都会存储在这个未决仓库。
但是在进入仓库之前还需要进行一次筛查,也就是看这个信号是否被忽略,简单来说就是看这个进程是否把这个信号拒之门外,就好像你拒收快递员给你的快递。通过筛查的,就会存放到未决仓库,等待进程的处理。
首先,信号产生自我们上面所说的三类异常,这每一类异常不管哪一类,都可以在异常或者事件发生后,让内核得知需要发送的信号类型和接收的进程的PID,进程的PCB统一由内核管理,内核可以通过pid,来找到对应进程的PCB,内核程序通过PCB来查看进程是否忽略了当前的信号,并将其放入其未决仓库,等待进程处理。
但是也不是什么信号都能被"拒之门外",有些信号是无法被忽略的,例如kill -9
所有的信号都能拒之门外,那么这个进程将上天。
进程如何处理信号?
前面说信号都存储在了未决仓库,那么进程只需要读取这个仓库就行,然后根据不同的信号做出具体的行动即可。
例如进程收到SIGINT信号之后,其默认行为就是终止自己。但是这个进程也可以忽视这个信号,也就是信号来了就给他删了,不让他进入pending仓库(未决仓库),但是,依旧要提一下的是,不是所有的信号都能被忽视。
进程也可以选择阻塞某个信号,某个信号被阻塞,不代表这个信号被忽略,更不代表这个信号无法进入未决仓库,只代表在这个信号被阻塞的时候,即使未决仓库有,也不会被处理,相当于是暂停了对这个信号的处理,但并不阻止他进入未决仓库。
每个信号都有一个默认处置,这里的处置翻译过来就是action,也就是行为的意思,简单来说就是内核发送某个信号个进程,只要信号没被忽视,进程正常读取之后,就会执行当前信号的默认指定行为。
但是进程可以自己指定遇到某个信号该做什么,你可以理解为PCB中某个特定的信号的处置函数的指针本来是指向默认内核函数,但是这个时候指向的另外一个函数。修改的鉴权和过程由内核完成,进程依旧不能脱离内核程序来进行修改信号的默认处置,不然依旧上房揭瓦。
比如kill -9对应的信号是无法修改默认处置的,不然这个强制kill有什么用?
进程也可以忽视信号,被忽视的信号会直接被抛弃,不会进程未决仓库。
信号处理的优先级(了解)
信号处理有严格的固定优先级,进程每次仅从 PCB 的pending(前面提到的未决仓库)中选当前最高优先级的未阻塞信号处理,串行执行(处理完一个再选下一个),优先级从高到低分三层:
- 最高级:SIGKILL(9),SIGSTOP(19),这类信号无视 PCB 的blocked阻塞标记和action自定义规则,只要出现在未决仓库中,进程必优先处理(直接终止 / 暂停),无法被覆盖。
- 次高级:普通信号(1-31),信号编号升序定优先级,编号越小优先级越高(如 2 > 10 > 13)。
- 最低级:实时信号,同样按信号编号升序,同编号的实时信号,按内核投递顺序依次处理(不会丢失,先投先处理)
此外被blocked阻塞的信号,暂不参与优先级排序,解除阻塞后,才按上述规则参与选路;
信号和默认处置(了解)
信号很多,默认处置也很难记,忘了的时候再查:

