王道操作系统,视频链接:2.1.5.2 信号

知识总览

信号知识总览

  1. 信号需要学习的内容:
  • 信号的作用、Linux信号示例
  • 信号的发送与保存
  • 信号的处理
  • 信号与异常的关系
  1. 信号与信号量区分
  • 信号量(Semaphore):实现进程间的同步、互斥
  • 信号(Signal):实现进程间通信(IPC,Inter Process Communication)

信号的作用

  1. 信号:用于通知进程某个特定的事件已经发生。进程收到一个信号后,对该信号进行处理。
    Linux操作系统定义的符合POSIX标准的30种信号类型(部分示例)如图所示:
    Linux信号示例
    PS:除了这30种之外,Linux操作系统还定义了更多的信号,这张表格简单了解一下即可。
  2. 关于信号:
  • 每个信号拥有一个序号,并且拥有一个信号名
  • 不同的操作系统对信号类型的定义是不一样的,比如早期Unix操作系统定义了19种信号,但是原理与Linux类似,每个信号对应一个序号、一个信号名
  • 通常用宏定义常量表示信号名,如:#define SIGINT 2,本质上一个信号名的背后对应的就是这个信号的序号
  • 当某些事件发生时,就会触发一个信号,比如序号2,在Linux系统中,终端(小黑框)正在运行一个程序,此时按下ctrl+c就会触发一个信号,使得操作系统的内核向正在终端中运行的进程发送SIGINT信号,正在运行的进程收到信号后,它会对这个信号进行相应的处理,对于SIGINT这个信号来说,默认处理就是终止进程。
  • 类似的,如果触发序号8的SIGFPE(FPE是Floating Point Exception的缩写)的信号,比如程序中存在bug除以0的形况,默认的处理就是终止并转储内存(转储内存就是把当前正在运行的出了浮点异常的进程的内存数据存储到硬盘里面)
    关于转储内存,对于某些软件,在使用过程中发生崩溃时,其崩溃的信息会被存储在硬盘中,用于该软件后续查找原因修复bug。
  • 总之,当某一些特定事件发生后,可以向对应进程发送一个相应的信号,通知它这个事情发生了。
  1. 完整的30条Linux信号:Linux30条信号表

实现原理——信号的发送与保存

  1. 待处理与被阻塞信号
  • 一个进程的PCB,也就是进程控制块,其中存储有两个位相量:待处理信号 pending 与被阻塞信号 blocked,其中向量的N个bit位对应N种信号。
  • 其中 blocked 位相量也称作信号掩码(signal mask),当对应信号的比特位为1时,进程将阻塞(屏蔽)对应比特位的信号,可以通过系统调用自行设置阻塞哪些信号。
  • 图示(图中假设有8种信号):
    待处理与被阻塞信号
  1. kill函数
  • int kill(pid_t pid, int sig)是最常用的用于发送信号的函数,其参数分别对应接收进程的pid,以及信号的序号。(也有其它用于发送信号的函数,只是这里以kill为例)
  • 假设进程P2、P3、OS内核进程都需要向P1发送信号,如图所示:
    kill函数使用示例
    图中,kill(2666,1)表示的是发送信号序号为1的信号给pid=2666的进程,如图所示,因为P1收到了序号1和8的信号,所以对应位置的 pending 置为1。
    注意,P1进程是无法分辨信号是谁发送的,并且假设P2、P3、OS依次发送信号,那么由于P1在处理了P2发送的1信号后,对应比特位已经置为1,所以后续OS的序号1的信号就会被简单丢弃。
  1. 从上面的例子可以看出:
  • 用户进程之间可以发送信号(其实也可以进程给自己发送信号
  • 内核进程也可以给用户进程发送信号
  • 注意:进程之间允许发送的信号是有限制的,比如前文提到的序号9的SIGKILL信号,只能由父进程发送给子进程,其它用户进程间不能随意发送。但是内核进程就随便发了,人家权限大,比较豪横。

实现原理——信号的处理(When)

  1. 问:进程什么时候处理信号?
    答:当进程从内核态转为用户态时(如:系统调用返回、或中断处理返回时),例行检查是否有待处理信号,如果有,就处理信号
    PS:还记得之前的中断信号吗,那个是硬件中断,这里的信号是软件信号,前者是在执行完每条指令后,由CPU进行检查,后者是在进程由内核态转为用户态时,由内核进行检查。
  2. 问:如何判断要处理哪些信号?
    答:如图所示:
    如何判断要处理的信号
    先将 blocked 按位取反,再与pending进行按位与,这样就能计算出待处理且未被阻塞的信号有哪些了,如图所示,计算出来的结果只有信号1为1,所以只处理信号1,信号8因为被阻塞,所以不做处理,符合预期。
  3. 问:如何处理?
    答:操作系统会给每个信号配备一个默认的处理程序,由于之前的例子需要处理的信号是信号1,所以会运行信号1的处理程序,具体规则如下:
  • 执行操作系统为此类信号设置的缺省(默认)信号处理程序(某些信号默认忽略,不作处理)
  • 执行进程为此类信号设置用户自定义信号处理程序(自定义信号处理程序将覆盖前者)
    比如:收到序号为28的SIGWINCH信号时,默认为忽略,但是UI可能需要根据窗口大小变化动态调整,所以可以将处理程序自定义为根据窗口大小进行UI调整。
  • 需要注意的点:
    • 信号处理程序运行结束后,通常会返回进程的下一条指令继续执行(除非信号处理程序将进程阻塞或终止)
    • 一旦处理了某个信号,就将pending位重置为0
    • 重复收到同类的信号,将被简单地丢弃(因为仅有1bit记录一类待处理信号)
    • 当同时收到多个不同类信号时,通常先处理序号更小的信号。
    • **有些信号既不能被用户自定义处理函数,也不能被阻塞,甚至不能被捕获。**如:Linux的SIGKILL、SIGSTOP信号
  1. 整体流程图:
    信号处理的流程
  2. PS:(来自deepseek)

1. 触发与总控:内核在从内核态返回用户态前,通过外层循环反复检查TIF_SIGPENDING标志。只要存在未屏蔽的未决信号,就进入处理流程(直到所有信号处理完毕,才最终恢复原程序上下文)。

2. 单次查找规则:每次查找时,内核按信号编号从小到大扫描pending位图,找到第一个未被阻塞的信号。找到后先将该位清0

3. 分类处理

  • SIG_IGN(忽略):直接丢弃,继续本轮查找。
  • SIG_DFL(默认动作):执行默认行为(可能终止、忽略、停止等),若为终止则进程结束。
  • 自定义处理保存上下文至用户栈,切至用户态执行处理函数;函数返回后,通过信号跳板调用**sigreturn系统调用切回内核**,恢复上下文。随后外层循环重新发起下一轮查找
  • PS1:sa_handler 就是一个变量(槽位),你只能往里面填三类东西:
    • 填 SIG_DFL(宏,数值0)
    • 填 SIG_IGN(宏,数值1)
    • 填 &my_handler(自定义函数的地址,比如 0x400520)
    • 结论:在这个层面,它们都是“值”,完全平级,你可以随意替换赋值。
  • PS2:内核先看 sa_handler 的值:
    • 是0 → 走默认分支,调用内核内部的默认处理函数;
    • 是1 → 走忽略分支,直接丢弃;
    • 是其他(用户地址) → 走自定义分支,切到用户态执行。
    • 0和1从来不被当作地址使用,它们只是触发不同分支的“条件码”。

4. 本质总结:处理过程不是递归,而是由外层while循环驱动的多次“内核-用户”态切换,每次切换至多处理一个自定义信号。

个人理解就是:如果是默认行为,那处理程序就是内核函数,在内核态执行,如果是自定义行为的函数,那就在用户态执行。

信号和异常的关系

  1. 关系:“信号”可以作为“异常”的配套机制,让进程对操作系统的异常处理进行补充。
  2. 在进程运行过程中,某些特殊事件可能引发“异常”,操作系统内核负责捕获并处理异常
  • 有些异常可由内核完成全部处理(如:缺页异常),此时就不必再使用信号机制。
  • 有些异常无法由内核完成全部处理,可能还需要用户进程配合,此时就可以用“信号机制”与“异常机制”相互配合(如:在Linux中,发生除以0异常时,内核的异常处理程序会向用户进程发送SIGFPE信号。SIGFPE信号的默认处理程序会将进程终止并转储内存;当然进程可以自定义SIGFPE信号处理程序,比如,开发一个计算器应用,每当用户输入除以0,如果默认处理,应用会直接闪退崩溃,用户体验就很差,所以可以自定义SIGFPE信号处理程序,按照实际需求去决定如何处理

知识回顾与重要考点

知识回顾与重要考点

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐