Linux 系统编程 06:信号机制 前言承接上一篇进程全生命周期管理本篇进入 Linux 系统编程中异步事件处理的核心 —— 信号机制。信号是内核向进程通知异步事件的标准手段也是轻量化的进程间通信方式广泛应用于进程终止、子进程回收、定时器、异常处理等场景是笔试面试的高频核心考点也是写出健壮后台与嵌入式程序的必备基础。一、信号核心概念1. 信号的本质信号是操作系统向进程发送的异步通知事件用于告知进程发生了某类事件本质上是软件层面的中断。进程不需要主动等待信号到来信号可以在任意时刻到达触发对应的处理逻辑。Linux 中的信号分为两类常规信号1-31 号不支持排队多个相同信号同时到达只会处理一次是日常开发最常用的类型实时信号34 号及以上支持排队保证多个信号按顺序递送多用于高精度实时场景每个信号都有唯一的整数编号和宏名称比如SIGINT对应 2 号信号代表终端中断。2. 高频标准信号清单信号名编号触发场景默认行为SIGINT2CtrlC 终端中断终止进程SIGQUIT3Ctrl\ 终端退出终止并生成 core 转储SIGKILL9强制终止命令终止进程不可捕捉忽略SIGSEGV11访问非法内存段错误终止进程SIGALRM14alarm 闹钟超时终止进程SIGCHLD17子进程退出 / 暂停忽略SIGSTOP19暂停进程暂停进程不可捕捉忽略SIGTSTP20CtrlZ 终端暂停暂停进程核心特例SIGKILL 和 SIGSTOP 是两个特殊信号不能被捕捉、不能被忽略、不能被阻塞是操作系统保留的终极管控手段确保系统总能终止失控进程。3. 信号的完整生命周期产生通过键盘、命令、系统调用、硬件异常等方式生成信号未决信号产生后、递送到进程之前的暂存状态记录在未决信号集中递送内核将信号交付给进程准备执行处理动作处理进程执行默认动作、忽略信号或执行用户注册的自定义处理函数二、信号的产生方式1. 键盘终端触发最常见的交互触发方式仅对前台进程生效Ctrl C → 发送 SIGINT中断前台进程Ctrl \ → 发送 SIGQUIT退出并生成核心转储文件Ctrl Z → 发送 SIGTSTP暂停前台进程2. 系统调用生成信号kill向指定进程发送信号#include sys/types.h #include signal.h int kill(pid_t pid, int sig);pid 0向指定 PID 的进程发送信号pid 0向同进程组的所有进程发送信号pid -1向所有有权限的进程发送信号init 进程除外sig 0不发送实际信号可用于检测进程是否存在raise向自身发送信号int raise(int sig);等价于kill(getpid(), sig)用于进程主动给自己发信号。alarm设置单次闹钟定时器#include unistd.h unsigned int alarm(unsigned int seconds);功能设置seconds秒后向当前进程发送SIGALRM信号返回值上一个闹钟剩余的秒数参数为 0 表示取消已有闹钟注意每个进程只有一个闹钟重复调用会覆盖之前的设置3. 软硬件异常触发软件异常读端关闭后写入管道触发SIGPIPE、定时器超时触发SIGALRM硬件异常访问非法内存触发SIGSEGV、整数除零触发SIGFPE三、信号的捕捉与处理进程对每个信号有三种处理策略执行默认动作绝大多数信号默认终止进程部分为暂停、忽略忽略信号信号到来后直接丢弃不做任何处理自定义捕捉注册信号处理函数信号到来时执行用户自定义逻辑1. signal简化版信号注册#include signal.h typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler);handler可选值SIG_IGN忽略该信号SIG_DFL执行默认动作自定义函数指针信号触发时执行该函数示例捕获 CtrlC 中断信号#include stdio.h #include signal.h #include unistd.h void sig_int_handler(int sig) { printf(捕获到信号%dCtrlC已被拦截\n, sig); } int main(void) { signal(SIGINT, sig_int_handler); while (1) { printf(进程运行中...\n); sleep(1); } return 0; }注意signal属于历史遗留函数不同平台行为有差异可移植性差工程开发优先使用标准的sigaction。2. sigaction标准信号注册函数sigaction是 POSIX 标准定义的信号注册接口行为明确可控支持配置更多细节是工业级开发的首选。#include signal.h int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);核心结构体 sigactionstruct sigaction { void (*sa_handler)(int); // 基础信号处理函数 void (*sa_sigaction)(int, siginfo_t *, void *); // 带附加信息的处理函数 sigset_t sa_mask; // 处理函数执行期间额外阻塞的信号集 int sa_flags; // 行为控制标志 void (*sa_restorer)(void); // 系统保留无需关注 };sa_mask执行信号处理函数期间自动阻塞指定信号处理完成后恢复防止信号嵌套打断sa_flags常用SA_RESTART表示被信号中断的慢速系统调用自动重启示例用 sigaction 注册信号处理#include stdio.h #include signal.h #include unistd.h void handler(int sig) { printf(捕获到信号 %d\n, sig); } int main(void) { struct sigaction act; act.sa_handler handler; sigemptyset(act.sa_mask); // 处理期间不额外阻塞其他信号 act.sa_flags 0; sigaction(SIGINT, act, NULL); while (1) sleep(1); return 0; }四、信号阻塞与未决信号集1. 两个核心信号集阻塞信号集信号屏蔽字记录当前进程屏蔽不递送的信号。被阻塞的信号产生后会停留在未决状态直到解除阻塞才会递送。未决信号集记录已经产生但还没有被递送处理的信号。信号产生后先进入未决信号集如果该信号未被阻塞就会立刻递送处理如果被阻塞则一直停留在未决集直到解除阻塞。2. 信号集操作函数sigset_t是信号集的专用类型使用前必须初始化#include signal.h int sigemptyset(sigset_t *set); // 清空信号集所有信号置0 int sigfillset(sigset_t *set); // 填满信号集所有信号置1 int sigaddset(sigset_t *set, int sig); // 向集合中添加指定信号 int sigdelset(sigset_t *set, int sig); // 从集合中删除指定信号 int sigismember(const sigset_t *set, int sig); // 判断信号是否在集合中3. sigprocmask设置进程阻塞信号集int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);how操作模式SIG_BLOCK将 set 中的信号加入当前阻塞集相当于 阻塞集 | setSIG_UNBLOCK将 set 中的信号从阻塞集移除相当于 阻塞集 ~setSIG_SETMASK直接用 set 替换当前整个阻塞集oldset传出参数保存修改前的阻塞集用于后续恢复4. sigpending获取未决信号集int sigpending(sigset_t *set);读取当前进程的未决信号集存入set中常用于验证信号是否被成功阻塞。五、可重入函数1. 什么是可重入函数可重入函数是指可以被多个执行流并发调用不会出现数据错乱、结果错误的函数。简单来说函数执行到一半时被打断打断的执行流再次调用该函数返回后原执行流的结果依然正确。2. 不可重入的典型特征函数内部使用了全局变量、静态局部变量调用了malloc/free内存分配的链表结构是全局共享的调用了标准 IO 函数如printf标准库有全局缓冲区状态维护了非原子的复杂状态机3. 信号处理函数的编程铁则信号处理函数是异步执行的可能在任意时刻打断主程序流程因此信号处理函数中只能调用可重入函数否则可能出现数据破坏、死锁等未定义行为。常见安全可重入函数read、write、_exit、open、close等纯系统调用 常见不安全不可重入函数printf、malloc、free、strtok等。六、实战SIGCHLD 异步回收子进程子进程退出时会自动向父进程发送SIGCHLD信号利用该特性可以实现异步回收父进程不需要阻塞等待信号触发时批量回收子进程彻底避免僵尸进程是工程中的标准写法。#include stdio.h #include unistd.h #include signal.h #include sys/wait.h #include stdlib.h void sig_chld_handler(int sig) { // 循环回收 非阻塞处理多个子进程同时退出的场景 // 常规信号不排队多个子进程退出只发一次信号必须循环清完 while (waitpid(-1, NULL, WNOHANG) 0) { printf(成功回收一个子进程\n); } } int main(void) { // 注册SIGCHLD信号处理 struct sigaction act; act.sa_handler sig_chld_handler; sigemptyset(act.sa_mask); act.sa_flags 0; sigaction(SIGCHLD, act, NULL); // 创建10个子进程 for (int i 0; i 10; i) { pid_t pid fork(); if (pid 0) { printf(子进程 %d 启动即将退出\n, getpid()); sleep(1); _exit(0); } } // 父进程继续主业务无需阻塞等待 while (1) { printf(父进程执行业务逻辑...\n); sleep(2); } return 0; }关键细节必须用while循环配合WNOHANG不能只调用一次waitpid。因为常规信号不排队多个子进程同时退出只会触发一次信号一次调用只能回收一个循环才能将所有已退出子进程全部回收干净。七、面试高频考点与易错坑点1. 经典面试问答Q1什么是信号信号和硬件中断有什么区别答 信号是操作系统向进程发送的异步通知事件属于软件层面的中断用于告知进程发生了特定事件。 区别在于硬件中断由硬件触发由内核中断处理程序响应信号是软件机制由内核或进程发送由进程自身处理。两者都是异步机制但层级、触发源和处理主体均不同。Q2哪些信号不能被捕捉和忽略为什么答 SIGKILL9 号和 SIGSTOP19 号不能被捕捉、忽略、阻塞。 这是操作系统为自己保留的终极管控手段确保系统总能终止或暂停失控进程防止进程完全脱离系统管控。Q3什么是可重入函数为什么信号处理函数只能调用可重入函数答 可重入函数可以被多个执行流并发调用不会出现数据错乱执行结果始终正确。 信号处理函数是异步执行的可能在任意时刻打断主程序。如果处理函数内调用不可重入函数就可能打断主流程中同一个函数的执行破坏全局状态导致不可预期的错误。Q4用 SIGCHLD 回收子进程为什么要用 while 循环而不是 if 判断答 因为常规信号不支持排队如果多个子进程同时退出只会向父进程发送一次 SIGCHLD 信号。 如果只用 if 判断一次只能回收一个子进程剩余的会变成僵尸进程用 while 循环配合 WNOHANG可以一次性把所有已退出的子进程全部回收。Q5信号阻塞和信号忽略有什么本质区别答 忽略是信号的一种处理方式信号正常递送过来后直接丢弃不会停留在未决状态。 信号阻塞是不让信号递送信号产生后会一直停留在未决信号集中解除阻塞后依然会被递送处理。 简单总结忽略是收到了直接扔掉阻塞是根本不让信号送过来。2. 常见易错坑点试图捕捉 SIGKILL 或 SIGSTOP以为可以屏蔽强制终止实际这两个信号无法被捕捉、阻塞、忽略信号处理函数内调用printf、malloc等不可重入函数导致偶发数据错乱、死锁SIGCHLD 回收子进程只调用一次 waitpid多子进程同时退出时产生僵尸进程误以为 alarm 可以设置多个定时器实际每个进程只有一个闹钟重复调用会覆盖混淆信号阻塞和忽略以为阻塞就是丢弃信号解除阻塞后信号依然会被递送工程场景使用 signal 注册信号不同平台行为不一致可移植性差信号处理函数中执行复杂耗时逻辑导致主业务流程被长时间阻塞以上就是 Linux 信号机制的全部核心内容信号是系统编程中异步事件处理的基础也是后续进程间通信、并发控制的重要辅助手段。下一篇我们将正式进入进程间通信模块讲解匿名管道与命名管道的底层原理与实战用法。制作不易如果对你有用希望能点赞收藏支持一下。