一、什么是信号?
信号是操作系统提供的一种异步事件通知机制 。 当进程收到信号,相当于收到一条 "短消息",通知进程发生了某个事件:除零错误、段错误、用户按下Ctrl+C、定时器超时、子进程退出等。
核心特点:异步。信号随时可能抵达,进程代码执行到任意位置都可能被打断,不是函数调用那种同步逻辑。
举几个常见信号:
|-----------|----|---------------|--------------|
| 信号 | 编号 | 含义 | 默认动作 |
| SIGINT | 2 | Ctrl+C 终端中断 | 终止进程 |
| SIGSEGV | 11 | 段错误,访问非法内存 | 终止 + core 转储 |
| SIGFPE | 8 | 浮点异常,除零错误 | 终止 + core 转储 |
| SIGALRM | 14 | alarm 定时器到期 | 终止进程 |
| SIGCHLD | 17 | 子进程退出 | 忽略 |
注意:
SIGKILL(9)、SIGSTOP(19)不能被捕获、不能屏蔽、不能忽略,是操作系统强制杀死 / 暂停进程的最后手段。
二、信号完整生命周期:产生 → 保存 (pending) → 处理
-
信号产生:内核给目标进程发送信号
-
信号保存(未决 pending) :信号不会立刻执行,先记录在进程的
task_struct中,标记 "有待处理" -
信号检测与处理 :进程从内核态返回用户态前夕,内核检查 pending 信号,执行对应处理动作
重点:信号不是一产生就立刻处理 。进程只有在内核态切回用户态这个时机,才会检测信号。这是面试必问。
信号产生的 5 种方式
-
终端按键 :
Ctrl+C发送SIGINT;Ctrl+\发送SIGQUIT -
硬件异常 :CPU 检测到除零、非法内存访问,内核主动向当前进程发信号(
SIGFPE/SIGSEGV) -
系统调用发送信号 :
kill()、raise()、abort()-
kill(pid, sig):向指定 pid 进程发送信号 -
raise(sig):给自己发信号 -
abort():给自己发送SIGABRT,终止程序
-
-
定时器 :
alarm(sec),倒计时结束内核发送SIGALRM -
子进程退出 :子进程死亡,父进程收到
SIGCHLD
三、内核如何保存信号:task_struct 里三张表
每个进程内核对象task_struct维护 3 套位图,这是信号机制的底层数据结构:
-
pending(未决信号位图) 比特位标记:对应编号的信号是否已经产生,等待处理。 信号来了,内核把 pending 对应 bit 置 1;信号处理完成,bit 清零。
注意:普通信号不支持排队,同一个信号多次产生,如果还没处理,pending 只会保留 1 次。实时信号可以排队。
-
block(阻塞 / 屏蔽位图,信号掩码) bit=1:代表屏蔽该信号 。 信号来了,pending 依然置位,但是暂时不处理;等解除屏蔽后,再去处理。
阻塞 ≠ 忽略:忽略是处理动作设为
SIG_IGN;阻塞是信号保留在 pending,暂缓处理。 -
handler(信号处理函数表) 数组,下标对应信号编号,存储信号的处理方式:
-
SIG_DFL:默认动作(终止 / 忽略 /core) -
SIG_IGN:忽略这个信号 -
用户自定义函数地址:捕获信号,执行用户写的 handler 回调
-
信号集操作 API sigset_t
sigset_t就是位图类型,用来操作 block 掩码:
sigemptyset(&set); // 清空信号集
sigfillset(&set); // 填满所有信号
sigaddset(&set, sig);// 添加信号
sigdelset(&set, sig);// 删除信号
sigismember(&set,sig);// 判断信号是否在集合中
sigprocmask():修改当前进程的 block 屏蔽字
-
SIG_BLOCK:新增屏蔽信号 -
SIG_UNBLOCK:解除屏蔽 -
SIG_SETMASK:直接替换屏蔽集合
sigpending():获取当前进程 pending 的未决信号集。
四、信号捕捉完整执行流程
当进程发生系统调用 / 中断,进入内核态;内核处理完中断 / 系统调用,准备返回用户态的一瞬间,内核检查 pending 信号:
-
扫描 pending 位图,找到存在、且没有被 block 屏蔽的信号
-
如果是默认动作:内核直接在内核态完成(终止进程、core dump)
-
如果是用户自定义捕获函数:
-
内核在用户态栈上,构造信号栈帧,保存原来程序上下文
-
切换回用户态,执行用户注册的 handler 回调函数
-
handler 执行完毕,触发特殊系统调用
sigreturn(),再次进入内核 -
内核恢复原来的程序上下文,回到主程序继续运行
-
关键点:handler 回调函数是在用户态执行,不是内核态! 为什么不能在内核态跑用户函数?安全隔离,内核态权限过高,防止用户代码破坏内核。

整个过程会发生4 次用户态 ↔ 内核态切换: 用户态 → 内核态(中断 / 系统调用)→ 检测信号 → 用户态(跑 handler)→ 内核态 (sigreturn) → 用户态(回到主代码)
signal 和 sigaction 的区别
-
signal():简易版本,底层封装sigaction,不支持信号掩码精细控制,旧版本不可重入,简单场景使用。typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler); -
sigaction():推荐,功能更强,面试优先答这个int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
-
sa_handler:信号处理函数(和 signal 一样) -
sa_mask:捕获该信号时,额外临时屏蔽的信号集。执行 handler 期间,自动屏蔽 sa_mask 里的信号,handler 结束自动恢复。 -
sa_flags:控制行为,如SA_RESTART:信号处理完后,自动重启被中断的系统调用。
面试考点:
sa_mask是信号处理函数执行期间的临时屏蔽,不是进程全局 mask。
五、Core Dump 核心转储
当收到某些信号(SIGSEGV、SIGFPE),默认动作是终止并生成 core 文件。 core 文件是进程崩溃那一刻的内存镜像,可以用 gdb 加载,定位崩溃位置。
默认很多环境 core 文件关闭,查看 / 修改:
ulimit -c # 查看core文件大小限制
ulimit -c unlimited # 打开core转储
注意:core 文件生成受权限、目录空间、ulimit 参数共同影响。
六、SIGCHLD 回收子进程(实战考点)
子进程退出,会向父进程发送SIGCHLD,默认动作是忽略。 我们可以捕获SIGCHLD,在 handler 内部调用waitpid(),异步回收僵尸子进程,避免父进程阻塞等待。
void handler(int sig)
{
waitpid(-1, NULL, WNOHANG); // 非阻塞回收所有已退出子进程
}
signal(SIGCHLD, handler);
坑点:普通信号不排队,多个子进程同时退出,可能只会触发一次 SIGCHLD,handler 里必须循环 waitpid,一次性收割全部僵尸进程。
七、面试高频问题汇总
-
信号什么时候被检测? 进程从内核态返回用户态的时候。
-
阻塞信号和忽略信号区别? 阻塞:信号保留 pending,暂缓处理;忽略:信号交付后直接丢弃,不做任何动作。
-
为什么 handler 要跑在用户态? 内核态权限极高,用户自定义函数不能在内核执行,存在安全风险。
-
SIGKILL 为什么不能捕获屏蔽? 操作系统保留的强制终止手段,防止进程自己屏蔽所有信号导致无法杀死。
-
普通信号为什么丢失? pending 是位图,同个信号多次到来,bit 只会置 1,无法计数,多次信号会合并丢失。实时信号用队列,不会丢。
八、完整 demo:捕获 SIGINT
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;
void handler(int sig)
{
cout << "收到信号:" << sig << endl;
}
int main()
{
signal(SIGINT, handler);
while(true)
{
cout << "running..." << endl;
sleep(1);
}
return 0;
}
运行后按Ctrl+C,不会直接退出,执行我们自定义 handler。