信号的基本共识
首先需要明确一个基本共识:信号是发送给进程的事件通知机制 。
在 Linux/Unix 系统中,可以通过命令:
kill -9 PIDkill -SIGINT PID
向指定 PID(Process ID) 的进程发送信号。
因此可以确认:
信号的接收对象是进程。
进程如何识别信号
类比人识别红绿灯需要:
- 能够识别信号
- 知道对应行为
进程识别信号同样需要两个要素:
识别信号
进程必须能够识别某种信号类型,例如:
- SIGINT
- SIGTERM
- SIGKILL
由于进程本质是程序代码的执行实例,因此:
进程对信号的识别能力是由程序员通过程序代码实现的。
换句话说:
信号识别逻辑由程序设计实现。
对信号产生行为
当进程识别到信号后,需要执行相应行为,例如:
- 终止进程
- 忽略信号
- 执行信号处理函数
这意味着:
每种信号必须定义对应的处理策略。
信号不一定被立即处理
当进程收到信号时,进程可能正在执行其他代码,因此:
信号不一定会被立即处理。
原因:
- 进程可能正在执行关键代码
- 当前代码优先级更高
- 系统调度策略影响
因此:
信号处理具有延迟性。
信号必须被暂存
由于信号可能无法立即处理,因此系统必须解决一个问题:
信号到达后,在被处理之前如何保存?
因此进程必须具备:
信号暂存能力
也就是说:
信号需要被记录下来,等待后续处理。
信号在进程中的存储位置
进程在操作系统中由 PCB(Process Control Block) 描述。
在 Linux 中,PCB 对应的数据结构是:
task_struct
因此:
信号状态会被保存在进程的
task_struct中。
信号的存储方式(位图)
Linux 使用 位图(bitmap) 记录信号状态。
普通信号编号:
1 ~ 31
因此可以使用一个 32位整数 来表示:
c
struct task_struct {
...
unsigned int signal;
...
};
位图规则:
| 位位置 | 表示信号 |
|---|---|
| bit1 | signal1 |
| bit2 | signal2 |
| bit3 | signal3 |
| ... | ... |
| bit31 | signal31 |
| 位值含义: |
| 位值 | 含义 |
|---|---|
| 0 | 未收到信号 |
| 1 | 已收到信号 |
| 例如: |
00000000000000000000000000001000
表示:
收到了 signal4
信号发送的本质
教材中通常称为:
发送信号 (send signal)
但从系统实现角度来看:
信号发送的本质是修改进程PCB中的信号位图。
例如:
发送 signal9:
bit9 = 1
即:
signal_bitmap |= (1 << 9)
因此:
信号发送 ≈ 修改 PCB 中的信号标志位。
并不是真正意义上的"传输数据"。
谁有权限修改信号位图
需要注意:
- PCB 是 内核数据结构
task_struct由 操作系统维护
因此:
用户程序没有权限直接修改 PCB。
只有:
操作系统内核
才能修改进程的信号位图。
发送信号的最终结论
因此可以得到一个重要结论:
所有信号发送行为,本质上都是操作系统内核修改目标进程PCB中的信号状态。
即:
用户程序
↓
系统调用
↓
操作系统内核
↓
修改目标进程 PCB 信号位图
进程处理信号的三种方式
当进程处理信号时,可以采取三种策略:
默认处理(Default Action)
操作系统定义默认行为,例如:
- 终止进程
- 产生 core dump
- 忽略
捕捉信号(Signal Catch)
进程可以注册:
signal handler
即:
自定义信号处理函数
忽略信号(Ignore)
进程可以选择:
忽略该信号
例如:
SIG_IGN
信号发送
在 Linux / Unix 系统中,无论信号通过何种方式产生,其本质都是:
由操作系统内核向目标进程设置相应的信号状态。
原因如下:
- 进程的控制信息存储在 PCB(Process Control Block) 中
- Linux 中 PCB 对应的数据结构是:
c
task_struct
- PCB 属于 内核数据结构
因此:
用户程序没有权限直接修改 PCB。
只有:
操作系统内核(Kernel)
才能修改进程的信号状态。
用户发送信号必须依赖系统调用
由于用户程序不能直接访问内核数据结构,因此如果希望用户程序能够发送信号,就必须通过:
系统调用(System Call)
来完成。
因此操作系统必须提供一组 信号相关系统调用接口,用于:
- 发送信号
- 设置信号处理方式
- 处理信号
例如常见接口:
| 系统调用 | 作用 |
|---|---|
kill() |
向进程发送信号 |
signal() |
设置信号处理方式 |
sigaction() |
更规范的信号处理接口 |
raise() |
向当前进程发送信号 |
kill 命令的底层实现
在 Linux 中常用命令:
bash
kill -9 PID
虽然这是一个 shell 命令,但其本质是:
调用系统调用
kill()
执行流程如下:
用户命令 kill
↓
shell 调用 kill() 系统调用
↓
内核修改目标进程 PCB 信号位图
↓
目标进程收到信号
因此可以得出结论:
所有信号发送最终都由操作系统内核完成。