Linux 中的信号(signal) 是进程间一种轻量级的异步通知机制,常被比喻为“软件中断”。下面按产生 → 保存 → 处理三个阶段给你系统梳理清楚整个流程(基于主流 Linux 内核实现,x86_64 架构为主)。
1. 信号的产生(谁/什么能产生信号)
| 产生方式 | 典型例子 | 内核函数(大致) | 是否可靠(可指定目标进程) |
|---|---|---|---|
| 硬件异常/陷阱 | 除0、段错误、非法指令、浮点异常 | do_trap() / do_page_fault() | 是(当前进程) |
| 终端特殊按键 | Ctrl+C → SIGINT Ctrl+\ → SIGQUIT Ctrl+Z → SIGTSTP | tty 驱动 → send_sig_info() | 是(前台进程组) |
| 软件主动发送 | kill(pid, sig) raise(sig) pthread_kill() | sys_kill / do_send_sig_info | 是 |
| 定时/闹钟 | alarm()、setitimer()、timer_create() | it_real_fn / posix_timer | 是(当前进程) |
| 内核主动产生 | 子进程退出 → SIGCHLD OOM killer | do_notify_parent() 等 | 是 |
| 其他系统事件 | 窗口大小改变 → SIGWINCH 电源事件 | 相应子系统 | 通常是当前进程或进程组 |
最常见的产生路径总结为三类:
- 当前进程自己作死(硬件异常、alarm)
- 别人用 kill() 系列打你
- 终端、前台进程组相关的控制信号
2. 信号的保存(内核怎么记住“有信号来了”)
信号到达内核后,并不立即执行处理函数,而是先记录下来。记录的位置在进程的 task_struct(PCB)里,主要有三个关键位图/集合:
| 字段(内核名字) | 含义 | 数据结构(现代内核) | 大小(常见64位系统) | 备注 |
|---|---|---|---|---|
| pending.signal | 未决信号集(pending) | struct sigpending → sigset_t | 通常2个 long(128位) | 还没处理的信号在这里“排队” |
| blocked | 被阻塞的信号集 | sigset_t | 同上 | sigprocmask / pthread_sigmask 设置 |
| real_blocked | 临时阻塞(rt_sigaction用) | sigset_t | 同上 | 较少见 |
核心逻辑:
- 信号来了 → 把对应 bit 置1 放入 pending.signal
- 如果该信号没被阻塞(即 !sigismember(&blocked, sig)),则标记为可递送
- 如果被阻塞,就只挂在 pending 里等待
- 同一个普通信号(非实时)多次产生只记录一次(pending bit 只有0/1)
- 实时信号(SIGRTMIN ~ SIGRTMAX)可以排队,带数据(siginfo),有计数
现代内核(5.x+)常用 sigpending 结构,包含:
struct sigpending {
struct list_head list; // 实时信号的 sigqueue 链表
sigset_t signal; // 位图(普通+实时信号的第一层)
};
普通实时信号会同时占用位图 + 挂到 list 上排队。
3. 信号的处理(什么时候、在哪里被处理)
信号真正被执行的时机只有一种:
进程即将从内核态返回用户态时(最典型的就是系统调用返回、时钟中断返回、中断返回等)
检查流程(do_signal / exit_to_user_mode_loop 大致逻辑):
- 当前是否有未决且未阻塞的信号?(pending & ~blocked)
- 有 → 选一个优先级最高的信号(普通信号按编号小→大,实时信号按产生顺序)
- 根据进程对该信号的处理方式(sighandler_t sa_handler / sa_sigaction)分三种情况:
| 处理方式 | sa_handler 值 | 行为 | 是否清 pending bit |
|---|---|---|---|
| 默认处理 | SIG_DFL | 大部分信号:终止进程 SIGCHLD:忽略 SIGURG等:忽略 | 是 |
| 忽略 | SIG_IGN | 直接丢弃该信号(部分信号不能被忽略,如 SIGKILL/SIGSTOP) | 是 |
| 自定义捕捉 | 函数指针 | 保存现场 → 构造 ucontext → 修改栈 → 返回用户态时先执行信号处理函数 | 是(执行完才清) |
捕捉函数执行完后,内核会通过特殊的返回路径(通常是 rt_sigreturn 系统调用)恢复上下文,继续执行被打断的位置。
快速记忆口诀版
产生:硬件异常、终端按键、kill家族、定时器、内核事件
保存:task_struct → pending.signal(位图) + sigqueue(实时排队)
被阻塞 → 只保存不处理
未阻塞 → 标记待递送
处理时机:从内核返回用户态前一刻
处理方式:默认 / 忽略 / 捕捉 → 三选一
普通信号:只记一次(后来的覆盖)
实时信号:可排队、可带数据
常见面试/笔试高频考察点总结
- 信号在内核用什么数据结构保存?(sigset_t + sigpending)
- 为什么普通信号不能排队?(节省空间 + 历史原因)
- 信号处理函数在什么时候执行?(从系统调用/中断返回用户态时)
- SIGKILL 和 SIGSTOP 能被捕捉/忽略/阻塞吗?(不能)
- 子进程退出一定会给父进程发 SIGCHLD 吗?(是,但父进程可忽略或阻塞)
- 信号处理函数里能调用哪些函数?(async-signal-safe 函数)
- sigsuspend、sigwait、sigsuspend 的区别?
需要更深入的某一部分(比如实时信号排队、siginfo、内核函数调用路径、信号栈、SA_SIGINFO、SA_RESTART 等)可以直接告诉我,我继续展开。