Linux 进程信号原理与剖析

一、什么是信号?

信号是操作系统提供的一种异步事件通知机制 。 当进程收到信号,相当于收到一条 "短消息",通知进程发生了某个事件:除零错误、段错误、用户按下Ctrl+C、定时器超时、子进程退出等。

核心特点:异步。信号随时可能抵达,进程代码执行到任意位置都可能被打断,不是函数调用那种同步逻辑。

举几个常见信号:

|-----------|----|---------------|--------------|
| 信号 | 编号 | 含义 | 默认动作 |
| SIGINT | 2 | Ctrl+C 终端中断 | 终止进程 |
| SIGSEGV | 11 | 段错误,访问非法内存 | 终止 + core 转储 |
| SIGFPE | 8 | 浮点异常,除零错误 | 终止 + core 转储 |
| SIGALRM | 14 | alarm 定时器到期 | 终止进程 |
| SIGCHLD | 17 | 子进程退出 | 忽略 |

注意:SIGKILL(9)、SIGSTOP(19) 不能被捕获、不能屏蔽、不能忽略,是操作系统强制杀死 / 暂停进程的最后手段。

二、信号完整生命周期:产生 → 保存 (pending) → 处理

  1. 信号产生:内核给目标进程发送信号

  2. 信号保存(未决 pending) :信号不会立刻执行,先记录在进程的task_struct中,标记 "有待处理"

  3. 信号检测与处理 :进程从内核态返回用户态前夕,内核检查 pending 信号,执行对应处理动作

重点:信号不是一产生就立刻处理 。进程只有在内核态切回用户态这个时机,才会检测信号。这是面试必问。

信号产生的 5 种方式

  1. 终端按键 :Ctrl+C 发送SIGINT;Ctrl+\发送SIGQUIT

  2. 硬件异常 :CPU 检测到除零、非法内存访问,内核主动向当前进程发信号(SIGFPE/SIGSEGV)

  3. 系统调用发送信号 :kill()、raise()、abort()

    1. kill(pid, sig):向指定 pid 进程发送信号

    2. raise(sig):给自己发信号

    3. abort():给自己发送SIGABRT,终止程序

  4. 定时器 :alarm(sec),倒计时结束内核发送SIGALRM

  5. 子进程退出 :子进程死亡,父进程收到SIGCHLD

三、内核如何保存信号:task_struct 里三张表

每个进程内核对象task_struct维护 3 套位图,这是信号机制的底层数据结构:

  1. pending(未决信号位图) 比特位标记:对应编号的信号是否已经产生,等待处理。 信号来了,内核把 pending 对应 bit 置 1;信号处理完成,bit 清零。

    注意:普通信号不支持排队,同一个信号多次产生,如果还没处理,pending 只会保留 1 次。实时信号可以排队。

  2. block(阻塞 / 屏蔽位图,信号掩码) bit=1:代表屏蔽该信号 。 信号来了,pending 依然置位,但是暂时不处理;等解除屏蔽后,再去处理。

    阻塞 ≠ 忽略:忽略是处理动作设为SIG_IGN;阻塞是信号保留在 pending,暂缓处理。

  3. handler(信号处理函数表) 数组,下标对应信号编号,存储信号的处理方式:

    1. SIG_DFL:默认动作(终止 / 忽略 /core)

    2. SIG_IGN:忽略这个信号

    3. 用户自定义函数地址:捕获信号,执行用户写的 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 信号:

  1. 扫描 pending 位图,找到存在、且没有被 block 屏蔽的信号

  2. 如果是默认动作:内核直接在内核态完成(终止进程、core dump)

  3. 如果是用户自定义捕获函数:

    1. 内核在用户态栈上,构造信号栈帧,保存原来程序上下文

    2. 切换回用户态,执行用户注册的 handler 回调函数

    3. handler 执行完毕,触发特殊系统调用sigreturn(),再次进入内核

    4. 内核恢复原来的程序上下文,回到主程序继续运行

关键点:handler 回调函数是在用户态执行,不是内核态! 为什么不能在内核态跑用户函数?安全隔离,内核态权限过高,防止用户代码破坏内核。

整个过程会发生4 次用户态 ↔ 内核态切换: 用户态 → 内核态(中断 / 系统调用)→ 检测信号 → 用户态(跑 handler)→ 内核态 (sigreturn) → 用户态(回到主代码)

signal 和 sigaction 的区别

  1. signal():简易版本,底层封装sigaction,不支持信号掩码精细控制,旧版本不可重入,简单场景使用。

    typedef void (*sighandler_t)(int);
    sighandler_t signal(int signum, sighandler_t handler);

  2. 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,一次性收割全部僵尸进程。

七、面试高频问题汇总

  1. 信号什么时候被检测? 进程从内核态返回用户态的时候。

  2. 阻塞信号和忽略信号区别? 阻塞:信号保留 pending,暂缓处理;忽略:信号交付后直接丢弃,不做任何动作。

  3. 为什么 handler 要跑在用户态? 内核态权限极高,用户自定义函数不能在内核执行,存在安全风险。

  4. SIGKILL 为什么不能捕获屏蔽? 操作系统保留的强制终止手段,防止进程自己屏蔽所有信号导致无法杀死。

  5. 普通信号为什么丢失? 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。

相关推荐
缘友一世1 小时前
开源 Linux截图工具闪构(SnapForge)
linux·运维·ubuntu·开源·截图
iCxhust1 小时前
先想好如何测试:让测试驱动你的程序设计
运维·服务器·软件工程·编程的艺术
Philtell2 小时前
突然连接不上内网服务器的问题
运维·服务器
做运维的阿瑞2 小时前
Docker常见故障全方位排查指南
运维·docker·云原生·容器
木子欢儿2 小时前
Zabbix 7.4 Alpine 版 Docker Compose 部署
android·运维·docker·容器·zabbix
朱包林2 小时前
iTerm2终端配置指南
linux·javascript·python·云原生·容器·kubernetes·云计算
pengdott2 小时前
Rocky linux9安装Jenkins最新版本2.585
运维·jenkins·流水线
祖力553 小时前
Linux系统移植、烧录与环境的搭建
linux·运维·服务器
对讲机数码科普3 小时前
从电路原理到协议栈:模拟对讲机与数字对讲机的技术内核拆解
运维·网络·架构·软件工程