Linux 进程信号:从生活类比到内核原理
你一定在 Linux 下用过CtrlC终止程序、用kill命令杀进程也见过程序崩溃报Segmentation fault这些日常操作背后都是进程信号在起作用。信号是 Linux 最基础、最核心的进程间异步通知机制堪称进程的 “软中断”。本文用生活类比 代码实战 内核原理带你从 0 到 1 彻底搞懂 Linux 信号。一、信号是什么先从生活看懂本质先抛开复杂术语用收快递完美类比信号机制你 进程快递员 操作系统OS快递 信号取件通知 信号产生暂时没空取、先记着 信号未决Pending有空了再去取 信号递达Delivery拆快递用 / 送人 / 扔一边 三种处理方式由此得出信号4 大核心特性识别是内置的进程天生 “认识” 信号是内核写死的能力处理方式提前定信号没来就已经知道该怎么处理不是立即处理进程可能在忙更高优先级的事要等 “合适时机”异步通知进程不知道信号啥时候来来了就响应一句话总结信号 内核发给进程的异步事件通知是进程间最轻量的 “事件提醒”。二、信号从哪来5 大产生方式全覆盖所有信号最终都由操作系统发送来源分 5 类1. 终端按键产生最常用CtrlC→ 发送SIGINT(2)终止前台进程Ctrl\→ 发送SIGQUIT(3)终止并生成 core dumpCtrlZ→ 发送SIGTSTP(20)挂起前台进程注意CtrlC只作用于前台进程后台进程加运行收不到。2. 系统调用 / 命令产生kill -信号 进程PID手动发信号如kill -9 PID强杀kill()代码中给指定进程发信号raise()自己给自己发信号abort()给自己发SIGABRT(6)强制异常退出3. 软件条件触发alarm(秒)→ 时间到发SIGALRM(14)管道读端关闭写端继续写 →SIGPIPE(13)定时器超时、资源超限等4. 硬件异常转化硬件报错 → 内核解释成信号发给进程除 0 运算 →SIGFPE(8)野指针 / 非法访问 →SIGSEGV(11)MMU 异常、指令非法等5. 子进程退出通知子进程退出 / 停止 → 父进程收到SIGCHLD(17)默认忽略三、信号来了怎么处理3 种处理方式进程对任何信号只有3 种合法处理动作1. 默认动作SIG_DFL多数信号终止进程部分信号终止 core dump方便事后调试SIGCHLD默认忽略2. 忽略信号SIG_IGN收到信号直接丢掉不做任何处理例外SIGKILL(9)、SIGSTOP(19)不能忽略、不能捕获、不能阻塞是系统 “终极权限” 信号3. 自定义捕捉信号捕获用signal()/sigaction()注册回调函数信号来了执行你写的逻辑。极简示例捕获CtrlC#include iostream #include signal.h #include unistd.h void handler(int sig) { std::cout 捕获到信号 sig 我不退出\n; } int main() { signal(SIGINT, handler); // 注册 2 号信号处理函数 while (1) { std::cout 运行中...\n; sleep(1); } }运行后按CtrlC进程不会退出只会打印提示 —— 这就是自定义捕捉的威力。四、信号的一生产生 → 保存 → 递达信号不是 “来了就立刻处理”完整生命周期分 3 步1. 信号产生OS 检测到事件给目标进程发信号。2. 信号保存内核层核心进程用3 个结构管理信号都在 PCBtask_struct里Pending 未决信号集已产生、但还没处理的信号用位图记录Block 阻塞信号集信号屏蔽字被 “屏蔽” 的信号产生了也暂时不递达Handler 处理函数指针每个信号对应处理方式默认 / 忽略 / 自定义关键结论阻塞 ≠ 忽略阻塞只是 “暂缓处理”解除阻塞后照样递达常规信号1–31多次产生只记录 1 次不排队实时信号34支持排队本章不讨论3. 信号递达从内核态切回用户态的 “合适时机”处理未阻塞的未决信号系统调用返回中断 / 异常处理完时钟中断返回前五、核心 API信号集操作实战想手动控制阻塞 / 未决用这 4 个信号集函数 2 个系统调用1. 信号集操作函数#include signal.h // 清空信号集 int sigemptyset(sigset_t *set); // 填满所有信号 int sigfillset(sigset_t *set); // 添加某个信号 int sigaddset(sigset_t *set, int signo); // 删除某个信号 int sigdelset(sigset_t *set, int signo); // 判断是否包含该信号 int sigismember(const sigset_t *set, int signo);2. 读取 / 修改阻塞信号集// 操作 Block 表 int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);SIG_BLOCK屏蔽 set 中的信号SIG_UNBLOCK解除屏蔽SIG_SETMASK直接设置为 set3. 读取未决信号集// 获取当前 Pending 信号集 int sigpending(sigset_t *set);六、信号捕捉完整流程用户态 ↔ 内核态切换自定义信号处理是面试最高频考点完整流程如下进程在用户态运行 main 函数发生中断 / 系统调用 → 切内核态内核处理完返回用户态前检查未决信号发现信号待处理且是自定义捕捉切回用户态执行你的 handler 函数handler 执行完自动切回内核态无新信号 → 恢复 main 上下文继续运行关键点信号处理函数在用户态执行保证内核安全处理信号时内核会自动阻塞当前信号防止重入混乱七、进阶关键volatile 与可重入函数1. volatile解决编译器优化导致的 “数据看不见”// 不加 volatileO2 优化后 flag 会被放进寄存器信号修改后主流程看不见 volatile int flag 0; void handler(int sig) { flag 1; }作用保持内存可见性禁止编译器优化每次都从内存读最新值场景信号处理函数与主流程共享的变量必须加volatile2. 可重入函数可重入函数被中断后重入执行结果不乱只访问局部变量 / 参数不可重入调用malloc/free、标准 I/O、全局变量等信号重入会崩溃信号安全handler 里只调用异步信号安全函数八、SIGCHLD优雅回收子进程告别僵尸进程父进程不用死循环waitpid轮询靠信号自动回收void handler(int sig) { // 非阻塞循环回收所有退出子进程 while (waitpid(-1, NULL, WNOHANG) 0); } int main() { signal(SIGCHLD, handler); // 注册子进程退出信号 if (fork() 0) { sleep(2); exit(0); } while (1) pause(); // 父进程安心做自己的事 }子进程退出 → 发SIGCHLD→ 父进程回调回收无僵尸进程无轮询消耗九、高频面试题一文吃透信号是同步还是异步异步。进程不知道信号何时到来随机触发处理。SIGKILL为什么不能捕获 / 忽略内核保留的终极权限防止进程 “卡死杀不死”。阻塞和忽略的区别阻塞暂不处理保留未决状态忽略直接丢弃。信号什么时候被处理从内核态切回用户态的 “合适时机”。信号处理函数为什么要加 volatile防止编译器优化保证主流程能看到信号修改的值。十、总结Linux 信号本质就是内核给进程发的异步软中断用 “产生→保存→递达” 完成事件通知支持默认 / 忽略 / 自定义 3 种处理。从CtrlC到进程异常、子进程回收、定时器信号无处不在。理解它你就真正看懂了 Linux 进程的 “事件驱动模型”写出更稳定、更优雅的系统程序。