一、信号是什么
信号是操作系统向进程发送的异步通知 ,告诉进程"某个事件发生了"。
我们可以把信号理解为软件层面的中断------它不是帮进程做事,只是告诉进程"该做事了"。
信号从产生到处理,经历三个阶段:
信号产生 → 信号保存 → 信号处理
二、信号的处理方式
大部分信号的默认操作都是终止进程 。
就像红灯的默认操作是让车停下来一样,信号的"默认"就是系统预设的动作。
但进程也可以自己处理信号:
-
调用
signal函数注册一个处理函数(handler) -
当信号来临时,进程自己调用这个 handler 来处理
signal函数
NAME
signal - ANSI C signal handling
SYNOPSIS
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
参数说明
signum:信号编号,是数字,其实就是信号的编号,通过kill -l可以看到所有信号
handler:函数指针,表⽰更改信号的处理动作,当收到对应的信号,就回调执⾏handler⽅法,这个函数指针可以换成SIG_DFL (通常是 0)和**SIG_IGN**(通常是 1)
1. SIG_DFL(通常是 0)
含义:Default,恢复信号的默认处理
数值:通常定义为 (void (*)(int))0或 0
效果:按照系统默认方式处理信号
2. SIG_IGN(通常是 1)
含义:Ignore,忽略这个信号
数值:通常定义为 (void (*)(int))1或 1
效果:收到信号时什么都不做,直接丢弃
三、信号的产生方式
1. 键盘产生
| 按键 | 信号编号 | 说明 |
|---|---|---|
| Ctrl + C | 2 (SIGINT) | 中断进程 |
| Ctrl + \ | 3 (SIGQUIT) | 退出进程(带 core dump) |
2. kill 命令
kill -9 1234 # 发送 9 号信号
底层调用的是 kill 系统调用。
3. 系统调用函数
① kill
int kill(pid_t pid, int sig);
给任意进程发送任意信号,成功返回 0,失败返回 -1。
② raise
int raise(int sig);
给自己发送任意信号,等价于:
kill(getpid(), sig);
③ abort
void abort(void);
给自己发送 6 号信号 SIGABRT,默认动作是终止进程。
即使 SIGABRT 被捕捉,handler 执行完后进程依然会终止**------这是它和 9、19 号不同的地方。相同的地方是和9和19一样是特殊信号,**前两个不会被捕捉
4. 软件条件产生信号
① SIGPIPE(13 号)
当管道读端关闭,写端还在写时产生。
② alarm(14 号 SIGALRM)闹钟
alarm(3); // 3 秒后给当前进程发 SIGALRM
-
每个进程同时只能有一个闹钟
-
每次调用都会刷新闹钟时间,所以只有第一个alarm生成了闹钟,剩下的都是重新上旋转发条
-
alarm(0)取消闹钟 -
返回值:上一次闹钟的剩余时间
alarm(3); // 设置 3 秒闹钟 sleep(1); int n = alarm(10); // 重置为 10 秒,n = 2(上次还剩 2 秒)闹钟的管理:
内核为每个闹钟维护一个结构体:
struct alarm { uint64_t timeout; // 闹钟时间 uint32_t who; // 进程标识 task_struct *pcb; // 进程控制块指针 };所有进程的闹钟用最小堆 管理,堆顶是剩余时间最短的闹钟。
判断超时:用过期时间戳(当前时间 + timeout)比较,OS 时间戳不断增长,当 OS 时间 ≥ 堆顶的过期时间戳时,闹钟超时。
5. 异常产生信号
像除零、野指针这些操作,为什么会直接导致进程崩溃?
本质是:硬件报错 → OS 识别 → 发信号 → 进程终止
除零(8 号 SIGFPE)
int a = 10;
a / 0;
CPU 执行除法时:
-
将
a的值加载到寄存器 eax -
将
0加载到另一个寄存器 -
执行除法运算,结果无穷大,CPU 寄存器存不下这个结果
此时 CPU 硬件会将状态寄存器(EFLAGS)的溢出标志位置为 1,表示计算结果不可信、硬件报错。然后怎么办呢,下一段会讲
状态寄存器(EFLAGS)里有很多的比特位,每个比特位代表一种标志,
其中一种标志叫做溢出标志位,为0,代表计算结果可信,但如果是/0这种计算,
那么会将溢出标志位置为1,表示本次计算在硬件计算上报错了,不可信
野指针(11 号 SIGSEGV)
访问非法内存地址,同样触发硬件异常。
四、异常信号的处理流程
-
CPU 执行指令 → 发生异常(如除零)
-
CPU 硬件置溢出标志位为 1,触发异常
-
CPU 保存当前进程的上下文(寄存器、指令位置等)
-
跳转到内核 ,OS 通过全局指针
current找到当前正在被调度的进程(current会永远指向当 前被调度的进程) -
OS 向该进程发送对应信号(除零发 8 号,野指针发 11 号)
-
查看进程的信号处理表:
-
如果进程没有注册 handler → 执行默认动作(终止进程)
-
如果进程注册了 handler → 恢复上下文,让进程执行自己的 handler
-
五、一个关键问题:为什么保存上下文?OS 不是要杀掉进程吗?
OS 发信号不是为了立即杀掉进程,而是给进程一个"自行了断"的机会。
如果进程注册了 handler,说明它想自己处理这个信号。
OS 必须配合:
-
恢复之前保存的上下文
-
让 CPU 重新回到用户态,执行 handler 的代码
这就引出了一个经典场景:
进程除零 → CPU 置溢出标志位 → OS 发 SIGFPE
进程捕捉了信号,handler 里没做特殊处理(没 exit、没跳转)
handler 返回后,CPU 恢复上下文,重新执行同一条除法指令
再次除零 → 再次置溢出标志 → 再次触发异常 → 再次发信号
→ 死循环
这不是 OS 反复发信号,而是 CPU 反复执行同一条指令导致硬件反复报错。
六、信号保存的本质
信号在内核里用位图管理:
-
每个信号对应一个比特位
-
信号产生时,OS 将对应比特位设为 1
-
信号处理完,对应比特位清 0
这就是"信号被读取"的本质。
八、pause 函数
int pause(void);
暂停当前进程,等待信号 。
当有信号递达到当前进程时,pause 结束暂停。
九、总结一波
信号产生(键盘/kill/系统调用/软件条件/异常)
↓
信号保存(内核位图置 1)
↓
信号处理(默认/忽略/自定义 handler)
├─ 默认 → 终止/停止/忽略
├─ 忽略 → 啥也不做
└─ 自定义 → 执行 handler,返回后可能恢复上下文重新执行
核心理解 :
信号不是 OS 强行介入的工具,而是一个协作机制 ------OS 告诉进程"你出事了",进程可以选择自己处理,也可以选择让 OS 按默认处理。
但如果进程自己处理了却没解决问题,就会陷入死循环。