Linux信号解析

大爱仙尊 发布于 2026-04-29 296 次阅读


1.认识信号

1.1信号的概念

信号是进程之间事件异步通知的一种方式,属于软中断

就是内核可以随时给进程发信号,进程收到后要么默认处理、要么忽略、要么自己写代码处理。生活例子:你正在写文档(正常执行),突然:

有人拍你肩膀(中断)→ 你回头看一下(默认动作:终止/暂停)

手机闹钟响了(闹钟信号)→ 你关掉它(可能自定义处理)

有人喊“着火了”(紧急信号)→ 你立刻跑(不可忽略的强信号)

这些都像 Linux 的 SIGINTSIGALRMSIGKILL

在Linux中使用kill -l就可以看到所有的信号,我们把前31个称为普通信号(也称为不可靠信号),这些信号都是大写的宏,前面的数字就是宏的对应值

使用man 7 signal往下翻就可以看到一张表,包含了信号的默认动作,大多数都是终止。这里说到了man,man就是Linux的手册,而7号手册就可以用来查信号。

2.信号的产生

2.1通过终端按键产生信号

很多人可能都使用过ctrl+c直接退出一个进程,但是很多人不知道原理,其实这就是信号。我们来梳理一下这个过程,这个过程如下:

1.键盘按键产生硬件中断,终端驱动程序捕获到中断;

2.终端驱动程序将你所输入的按键转化为对应的信号;

3.终端将信号发送给了操作系统,告诉操作系统,你要向该进程发送某个信号;

4.操作系统接收到请求后,找到当前进程,修改PCB中的未决信号集,完成信号产生;

这里有一个关键规则:终端组合键产生的信号,只能发送给当前前台进程。后台进程(通过&启动的进程)无法接收终端组合键产生的信号,这是为了避免后台进程被用户误操作中断。

通过终端按键产生信号就是:ctrl+c(信号2),ctrl+\(信号3),内核向进程发送信号,然后进程做出对应的处理,这两个信号都是终止进程用的

2.2使用系统命令向进程发送信号

kill命令底层实际调用kill()函数来向进程发送信号,比如就是 kill -9 pid,这个pid就是进程的编号,就像是每个人(比喻为进程)上学都有一个学号,这个pid就像是我们每个学生的学号。kill -9 pid就是终止这个pid对应的进程

2.3使用函数产生信号

除了通过终端和命令,我们还可以在 C/C++ 代码中调用特定函数,主动产生信号并发送给进程。Linux 系统提供了三个核心函数:kill()raise()abort(),分别用于 “向指定进程发送信号”、“向当前进程发送信号”、“强制当前进程异常终止”。

2.4由软件条件产生信号

软件条件触发是指:信号的产生源于程序的运行状态或软件逻辑,而非用户操作或硬件异常。

这里举个例子:就是设计一个定时器,然后时间一到,直接给对应的进程发送信号,这就是由软件条件产生信号

2.5硬件异常产生信号

 硬件异常触发是指:信号的产生源于 CPU 或其他硬件设备的错误,如除零操作、非法内存访问、总线错误等。硬件检测到错误后,会通知 OS,OS 将其映射为对应的信号,发送给当前进程。

        这类信号的本质是:硬件错误通过 OS 转换为软件层面的信号,让进程有机会处理错误(如打印日志、保存数据),若不处理则执行默认动作(通常是终止进程并生成 Core Dump)。

这里举个例子:除0操作->触发SIGFPE信号(8号信号)

  当进程执行 “除以零” 的算术运算时,CPU 的运算单元会检测到该错误,通知 OS,OS 将其映射为SIGFPE信号(Floating-point exception,浮点异常),默认处理动作是 “终止进程并生成 Core Dump”。

#include 
#include 
#include 
using namespace std;
// 自定义SIGFPE信号处理函数
void sigfpe_handler(int signum)
{
    cout << "捕获到信号:" << signum << "(SIGFPE),发生除零错误!" << endl;
    // 退出进程(避免无限循环触发信号)
    exit(1);
}
int main()
{
    cout << "进程PID:" << getpid() << ",尝试执行除零操作..." << endl;
    // 注册SIGFPE信号的处理函数
    signal(SIGFPE, sigfpe_handler);
    sleep(1);  // 延迟1秒,便于观察
    // 执行除零操作
    int a = 10;
    int b = 0;
    int c = a / b;  // 除零错误,触发SIGFPE信号
    // 以下代码不会执行
    cout << "计算结果:" << c << endl;
    return 0;
}

运行这段代码,终端输出如下

 这里需要主义的是,为何会一直触发信号?

如果我们不在处理函数中退出进程,会发现 SIGFPE 信号会被无限触发。原因是:除零错误发生后,CPU 的状态寄存器会记录该错误状态,若不清理该状态,OS 会持续检测到错误,不断发送 SIGFPE 信号。

因此,在处理 SIGFPE 信号时,通常需要在处理函数中调用exit()_exit()终止进程,避免无限循环。

2.6总结思考

 1.上⾯所说的所有信号产⽣,最终都要有OS来进⾏执⾏,为什么?因为OS是进程的管理者

2.信号的处理是否是⽴即处理的?不是,是在合适的时候

3.信号如果不是被⽴即处理,那么信号是否需要暂时被进程记录下来?记录在哪⾥最合适呢?需要被记录下来,这个会记录到进程的PCB中的未决信号集合,我们可以把他理解为一个待办事项薄,专门用于挂起那些已经到达,但尚未传递给进程的信号。

4.⼀个进程在没有收到信号的时候,能否能知道,⾃⼰应该对合法信号作何处理呢?能知道,在信号没到来之前,对每个信号如何处理,进程都是知道的,作何处理也是知道的

5.如何理解OS向进程发送信号?能否描述⼀下完整的发送处理过程?

这个过程我们来详细梳理一下:

1.信号的产生,可以是

硬件异常:缺页,非法指令,除零->内核生成对应信号

终端按键:Ctrl+C->驱动产生SIGINT信号

系统调用:kill(),tkill(),raise()

2.内核找到目标进程的task_struct(PCB)

通过进程的pid找到PCB,然后调查调用者权限

3.信号的过渡

如果信号是SIGKILL或者是SIGSTOP->强制处理,不能忽略或者堵塞

如果信号是忽略,那么直接丢弃信号,不记录

4.将信号记录到未决信号集

每个进程的PCB中都有位图,用来记录那些普通信号正在未决,这一步之后,信号就是处于未决状态,从用户态看起来,此时信号还没被处理,只是内核知道了。

5.如果进程是沉睡的(可中断睡眠),且信号是不会被堵塞的,需要唤醒进程。到此发送动作在内核中已经就完成了。

6.到这里就是信号的递达和处理阶段(发生在进程返回用户态前)

时机是:内核态--->用户态切换时

内核查看blocked掩码:每个进程都有一个blocked掩码

根据信号的处理动作执行信号

3.信号的保存

一个信号从产生到消失,会经过几个状态:

  1. 生成:内核在目标进程的 pending 位图中记录(并可能唤醒进程)。
  2. 未决(pending):信号被“保存”在内核中,尚未递达到进程。
  3. 递达(delivered):内核将信号交给进程处理(执行默认动作或用户函数)。
  4. 清理:处理后,内核清除 pending 中对应的位(实时信号出队)。

所以“信号的保存”实质上就是指在未决阶段暂存在内核数据结构中

信号的保存位置是每个进程内核控制块中的未决位图和队列(pending),以及阻塞掩码(blocked);用户通过 sigpending 查看“已保存但未递达”的信号,通过 sigprocmask 控制信号的“保存期”。

4.可重入函数

什么是可重入?一个函数如果满足一下条件,就是可重入的:

只使用局部变量(栈上分配),或对全局/静态变量的访问是原子的或已经通过锁保护(但在信号处理函数中锁本身也危险)。

不调用任何不可重入的函数(例如 mallocprintffopen 等)。

不修改自己的代码(现代系统很少见),也不修改其他共享数据结构除非通过原子操作。

为啥需要可重入函数呢?你之前已经了解了信号捕捉:当进程收到信号时,内核会中断主程序,转去执行信号处理函数。而信号处理函数与主程序共享同一进程地址空间,所以它们可能访问相同的全局变量、堆区或文件描述符。

问题:如果主程序正在执行一个不可重入函数(比如 malloc 正在修改堆的空闲链表),此时信号来了,处理函数里又调用了 malloc,就会导致堆链表崩溃 —— 因为 malloc 内部维护的静态状态被第二次调用破坏。

因此:信号处理函数中只能调用“可重入”且“异步信号安全”的函数。

这里有一句话:一个线程安全函数不一定是可重入的;但一个可重入函数一定是线程安全的,说明可重入不等于线程安全

此作者没有提供个人介绍。
最后更新于 2026-04-29