你一定在 Linux 下用过 Ctrl+C 终止程序、用 kill 命令杀进程,也见过程序崩溃报 Segmentation fault ,这些日常操作背后,都是进程信号在起作用。
信号是 Linux 最基础、最核心的进程间异步通知机制,堪称进程的 "软中断"。本文用生活类比 + 代码实战 + 内核原理,带你从 0 到 1 彻底搞懂 Linux 信号。
先抛开复杂术语,用收快递完美类比信号机制:
- 你 = 进程
- 快递员 = 操作系统(OS)
- 快递 = 信号
- 取件通知 = 信号产生
- 暂时没空取、先记着 = 信号未决(Pending)
- 有空了再去取 = 信号递达(Delivery)
- 拆快递用 / 送人 / 扔一边 = 三种处理方式
由此得出信号4 大核心特性:
- 识别是内置的:进程天生 "认识" 信号,是内核写死的能力
- 处理方式提前定:信号没来,就已经知道该怎么处理
- 不是立即处理:进程可能在忙更高优先级的事,要等 "合适时机"
- 异步通知:进程不知道信号啥时候来,来了就响应
一句话总结:信号 = 内核发给进程的异步事件通知,是进程间最轻量的 "事件提醒"。
所有信号最终都由操作系统发送,来源分 5 类:
1. 终端按键产生(最常用)
Ctrl+C→ 发送 SIGINT(2):终止前台进程Ctrl+→ 发送 SIGQUIT(3):终止并生成 core dumpCtrl+Z→ 发送 SIGTSTP(20):挂起前台进程
注意:
Ctrl+C只作用于前台进程 ,后台进程(加&运行)收不到。
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 种合法处理动作:
1. 默认动作(SIG_DFL)
- 多数信号:终止进程
- 部分信号:终止 + core dump(方便事后调试)
SIGCHLD:默认忽略
2. 忽略信号(SIG_IGN)
- 收到信号直接丢掉,不做任何处理
- 例外 :
SIGKILL(9)、SIGSTOP(19)不能忽略、不能捕获、不能阻塞,是系统 "终极权限" 信号
3. 自定义捕捉(信号捕获)
用 signal()/sigaction() 注册回调函数,信号来了执行你写的逻辑。
极简示例(捕获 Ctrl+C):
#include
#include
#include
void handler(int sig) {
std::cout << "捕获到信号:" << sig << ",我不退出!
"; }
int main() {
signal(SIGINT, handler); // 注册 2 号信号处理函数 while (1) { std::cout << "运行中...
";
sleep(1); }
}
运行后按 Ctrl+C,进程不会退出 ,只会打印提示 ------ 这就是自定义捕捉的威力。
信号不是 "来了就立刻处理",完整生命周期分 3 步:
1. 信号产生
OS 检测到事件,给目标进程发信号。
2. 信号保存(内核层核心)
进程用 3 个结构 管理信号,都在 PCB(task_struct)里:
- Pending 未决信号集:已产生、但还没处理的信号(用位图记录)
- Block 阻塞信号集(信号屏蔽字):被 "屏蔽" 的信号,产生了也暂时不递达
- Handler 处理函数指针:每个信号对应处理方式(默认 / 忽略 / 自定义)
关键结论:
- 阻塞 ≠ 忽略:阻塞只是 "暂缓处理",解除阻塞后照样递达
- 常规信号(1--31)多次产生只记录 1 次,不排队
- 实时信号(34+)支持排队,本章不讨论
3. 信号递达
从内核态 切回用户态的 "合适时机",处理未阻塞的未决信号:
- 系统调用返回
- 中断 / 异常处理完
- 时钟中断返回前
想手动控制阻塞 / 未决,用这 4 个信号集函数 + 2 个系统调用:
1. 信号集操作函数
#include
// 清空信号集 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:直接设置为 set
3. 读取未决信号集
// 获取当前 Pending 信号集 int sigpending(sigset_t *set);
自定义信号处理,是面试最高频考点,完整流程如下:
- 进程在用户态运行 main 函数
- 发生中断 / 系统调用 → 切内核态
- 内核处理完,返回用户态前:检查未决信号
- 发现信号待处理,且是自定义捕捉
- 切回用户态,执行你的 handler 函数
- handler 执行完,自动切回内核态
- 无新信号 → 恢复 main 上下文,继续运行
关键点:
- 信号处理函数在用户态执行,保证内核安全
- 处理信号时,内核会自动阻塞当前信号,防止重入混乱
1. volatile:解决编译器优化导致的 "数据看不见"
// 不加 volatile,O2 优化后 flag 会被放进寄存器,信号修改后主流程看不见 volatile int flag = 0; void handler(int sig) { flag = 1; }
- 作用:保持内存可见性,禁止编译器优化,每次都从内存读最新值
- 场景 :信号处理函数与主流程共享的变量,必须加
volatile
2. 可重入函数
- 可重入:函数被中断后重入,执行结果不乱(只访问局部变量 / 参数)
- 不可重入 :调用
malloc/free、标准 I/O、全局变量等,信号重入会崩溃 - 信号安全 :handler 里只调用异步信号安全函数
父进程不用死循环 waitpid 轮询,靠信号自动回收:
void handler(int sig) { // 非阻塞循环回收所有退出子进程 while (waitpid(-1, NULL, WNOHANG) > 0);
}
int main()
while (1) pause(); // 父进程安心做自己的事
}
SIGCHLD → 父进程回调回收
- 信号是同步还是异步?异步。进程不知道信号何时到来,随机触发处理。
SIGKILL为什么不能捕获 / 忽略?内核保留的终极权限,防止进程 "卡死杀不死"。- 阻塞和忽略的区别?阻塞:暂不处理,保留未决状态;忽略:直接丢弃。
- 信号什么时候被处理?从内核态切回用户态的 "合适时机"。
- 信号处理函数为什么要加 volatile?防止编译器优化,保证主流程能看到信号修改的值。
Linux 信号本质就是:内核给进程发的异步软中断,用 "产生→保存→递达" 完成事件通知,支持默认 / 忽略 / 自定义 3 种处理。
从 Ctrl+C 到进程异常、子进程回收、定时器,信号无处不在。理解它,你就真正看懂了 Linux 进程的 "事件驱动模型",写出更稳定、更优雅的系统程序。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/250724.html