linux 信号内核模型

本内容主要做回顾,以及对一些信号内核运行逻辑的补充,补充了很多通俗易懂的案例和讲解,说能听懂的话,讲能听懂的知识。


信号的基本模型

信号怎么产生?

计算机在运行的时候,会出现各种各样的异常,举一个很常见的例子,如果你是一个程序员,那么你肯定遇到过访问野指针的情况,再者在进行复杂计算的时候,一不小心将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阻塞的信号,暂不参与优先级排序,解除阻塞后,才按上述规则参与选路;

信号和默认处置(了解)

信号很多,默认处置也很难记,忘了的时候再查:

相关推荐
i建模2 小时前
在 Rocky Linux 上安装轻量级的 XFCE 桌面
linux·运维·服务器
Data_Journal3 小时前
Scrapy vs. Crawlee —— 哪个更好?!
运维·人工智能·爬虫·媒体·社媒营销
若风的雨3 小时前
WC (Write-Combining) 内存类型优化原理
linux
YMWM_3 小时前
不同局域网下登录ubuntu主机
linux·运维·ubuntu
zmjjdank1ng3 小时前
restart与reload的区别
linux·运维
哼?~3 小时前
进程替换与自主Shell
linux
Suchadar3 小时前
Docker常用命令
运维·docker·容器
FIT2CLOUD飞致云3 小时前
赛道第一!1Panel成功入选Gitee 2025年度开源项目
服务器·ai·开源·1panel
你才是臭弟弟3 小时前
MinIo开发环境配置方案(Docker版本)
运维·docker·容器
Bruk.Liu3 小时前
Gitea Actions 的概念及基础使用
运维·ci/cd·持续集成