Linux 进程信号:从生活类比到内核原理

你一定在 Linux 下用过 Ctrl+C 终止程序、用 kill 命令杀进程,也见过程序崩溃报 Segmentation fault ,这些日常操作背后,都是进程信号在起作用。

信号是 Linux 最基础、最核心的进程间异步通知机制,堪称进程的 "软中断"。本文用生活类比 + 代码实战 + 内核原理,带你从 0 到 1 彻底搞懂 Linux 信号。


一、信号是什么?先从生活看懂本质

先抛开复杂术语,用收快递完美类比信号机制:

  • 你 = 进程
  • 快递员 = 操作系统(OS)
  • 快递 = 信号
  • 取件通知 = 信号产生
  • 暂时没空取、先记着 = 信号未决(Pending)
  • 有空了再去取 = 信号递达(Delivery)
  • 拆快递用 / 送人 / 扔一边 = 三种处理方式

由此得出信号4 大核心特性

  1. 识别是内置的:进程天生 "认识" 信号,是内核写死的能力
  2. 处理方式提前定:信号没来,就已经知道该怎么处理
  3. 不是立即处理:进程可能在忙更高优先级的事,要等 "合适时机"
  4. 异步通知:进程不知道信号啥时候来,来了就响应

一句话总结:信号 = 内核发给进程的异步事件通知,是进程间最轻量的 "事件提醒"。


二、信号从哪来?5 大产生方式全覆盖

所有信号最终都由操作系统发送,来源分 5 类:

1. 终端按键产生(最常用)

  • Ctrl+C → 发送 SIGINT(2):终止前台进程
  • Ctrl+\ → 发送 SIGQUIT(3):终止并生成 core dump
  • Ctrl+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 种处理方式

进程对任何信号,只有3 种合法处理动作

1. 默认动作(SIG_DFL)

  • 多数信号:终止进程
  • 部分信号:终止 + core dump(方便事后调试)
  • SIGCHLD:默认忽略

2. 忽略信号(SIG_IGN)

  • 收到信号直接丢掉,不做任何处理
  • 例外SIGKILL(9)SIGSTOP(19) 不能忽略、不能捕获、不能阻塞,是系统 "终极权限" 信号

3. 自定义捕捉(信号捕获)

signal()/sigaction() 注册回调函数,信号来了执行你写的逻辑。

极简示例(捕获 Ctrl+C):

cpp 复制代码
#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);
    }
}

运行后按 Ctrl+C,进程不会退出 ,只会打印提示 ------ 这就是自定义捕捉的威力。


四、信号的一生:产生 → 保存 → 递达

信号不是 "来了就立刻处理",完整生命周期分 3 步:

1. 信号产生

OS 检测到事件,给目标进程发信号。

2. 信号保存(内核层核心)

进程用 3 个结构 管理信号,都在 PCB(task_struct)里:

  1. Pending 未决信号集:已产生、但还没处理的信号(用位图记录)
  2. Block 阻塞信号集(信号屏蔽字):被 "屏蔽" 的信号,产生了也暂时不递达
  3. Handler 处理函数指针:每个信号对应处理方式(默认 / 忽略 / 自定义)

关键结论:

  • 阻塞 ≠ 忽略:阻塞只是 "暂缓处理",解除阻塞后照样递达
  • 常规信号(1--31)多次产生只记录 1 次,不排队
  • 实时信号(34+)支持排队,本章不讨论

3. 信号递达

内核态 切回用户态的 "合适时机",处理未阻塞的未决信号:

  • 系统调用返回
  • 中断 / 异常处理完
  • 时钟中断返回前

五、核心 API:信号集操作实战

想手动控制阻塞 / 未决,用这 4 个信号集函数 + 2 个系统调用:

1. 信号集操作函数

cpp 复制代码
#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. 读取 / 修改阻塞信号集

cpp 复制代码
// 操作 Block 表
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
  • SIG_BLOCK:屏蔽 set 中的信号
  • SIG_UNBLOCK:解除屏蔽
  • SIG_SETMASK:直接设置为 set

3. 读取未决信号集

cpp 复制代码
// 获取当前 Pending 信号集
int sigpending(sigset_t *set);

六、信号捕捉完整流程:用户态 ↔ 内核态切换

自定义信号处理,是面试最高频考点,完整流程如下:

  1. 进程在用户态运行 main 函数
  2. 发生中断 / 系统调用 → 切内核态
  3. 内核处理完,返回用户态前:检查未决信号
  4. 发现信号待处理,且是自定义捕捉
  5. 切回用户态,执行你的 handler 函数
  6. handler 执行完,自动切回内核态
  7. 无新信号 → 恢复 main 上下文,继续运行

关键点:

  • 信号处理函数在用户态执行,保证内核安全
  • 处理信号时,内核会自动阻塞当前信号,防止重入混乱

七、进阶关键:volatile 与可重入函数

1. volatile:解决编译器优化导致的 "数据看不见"

cpp 复制代码
// 不加 volatile,O2 优化后 flag 会被放进寄存器,信号修改后主流程看不见
volatile int flag = 0;

void handler(int sig) {
    flag = 1;
}
  • 作用:保持内存可见性,禁止编译器优化,每次都从内存读最新值
  • 场景 :信号处理函数与主流程共享的变量,必须加 volatile

2. 可重入函数

  • 可重入:函数被中断后重入,执行结果不乱(只访问局部变量 / 参数)
  • 不可重入 :调用 malloc/free、标准 I/O、全局变量等,信号重入会崩溃
  • 信号安全 :handler 里只调用异步信号安全函数

八、SIGCHLD:优雅回收子进程,告别僵尸进程

父进程不用死循环 waitpid 轮询,靠信号自动回收:

cpp 复制代码
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 → 父进程回调回收
  • 无僵尸进程,无轮询消耗

九、高频面试题(一文吃透)

  1. **信号是同步还是异步?**异步。进程不知道信号何时到来,随机触发处理。
  2. **SIGKILL 为什么不能捕获 / 忽略?**内核保留的终极权限,防止进程 "卡死杀不死"。
  3. **阻塞和忽略的区别?**阻塞:暂不处理,保留未决状态;忽略:直接丢弃。
  4. **信号什么时候被处理?**从内核态切回用户态的 "合适时机"。
  5. **信号处理函数为什么要加 volatile?**防止编译器优化,保证主流程能看到信号修改的值。

十、总结

Linux 信号本质就是:内核给进程发的异步软中断,用 "产生→保存→递达" 完成事件通知,支持默认 / 忽略 / 自定义 3 种处理。

Ctrl+C 到进程异常、子进程回收、定时器,信号无处不在。理解它,你就真正看懂了 Linux 进程的 "事件驱动模型",写出更稳定、更优雅的系统程序。

相关推荐
jinglong.zha2 小时前
AScript + Cursor:让 AI 直接操控你的设备,一句话完成自动化编程(源代码)
运维·人工智能·自动化·ascript·openclaw
运维行者_2 小时前
MSP网络管理破局者:IPAM+SPM插件终结IP冲突与安全威胁
运维·服务器·开发语言·网络·安全·web安全·php
网硕互联的小客服2 小时前
CentOS 7 系统开通后如何修改数据盘挂载目录?
运维·服务器·网络·安全·自动化
【骠姚校尉】2 小时前
Makefile核心教程(六) --- 一文吃透 Makefile 通配符
linux·makefile·通配符·核心教程
牛奶咖啡132 小时前
DevOps自动化运维实践_ansible-playbook的使用
运维·自动化·ansible·devops·playbook·playbook模块及其示例
jiuri_12152 小时前
OpenHarmony 移植 OpenSSH/sshd
linux·sshd·ohos
慕诗客2 小时前
英伟达Jetson Agx Orin更换开机Logo
linux
yy_xzz2 小时前
【Linux开发】01多线程编程:线程的创建与运行
linux·运维·服务器
蜕变的土豆2 小时前
ABB1200系列机器人配置
运维·服务器·机器人