Linux信号——信号捕捉

目录

一信号捕捉的流程

二操作系统是怎么运行的?

2-1硬件中断

2-1-1系统源码(Linux0.11版本)

2-2时钟中断

2-2-1系统源码(Linux0.11版本)

2-2-2操作系统是一个死循环

2-3软中断

2-3硬件出错,操作系统怎么知道错误类型?

三用户态?内核态?

CPL是如何定义用户态和内核态的?

四sigaction()

可重入函数?不可重入函数?

五volatile

六SIGCHLD信号


当我们在终端按下 Ctrl+C 终止程序,或是在代码中捕获进程退出的通知时,背后都离不开 Linux 信号 这一核心机制。它是操作系统与进程之间异步通信的 "信使",既能处理硬件中断、系统异常,也能实现进程间的通知与调度。但信号的运行逻辑远不止 "发送 - 接收" 这么简单:它如何被内核捕捉?用户态与内核态的切换在其中扮演了什么角色?sigactionvolatileSIGCHLD 这些关键接口和概念,又该如何理解与实践?

本文将从信号捕捉的完整流程切入,结合硬件中断、时钟中断、软中断的底层原理,一步步拆解 Linux 信号的运行机制,同时厘清用户态 / 内核态、可重入函数、信号处理等核心知识点,带你从原理到实践,彻底搞懂 Linux 信号的底层逻辑。

当前阶段:

一信号捕捉的流程

如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。

由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下:

• 用户程序注册了SIGQUIT 信号的处理函数sighandler 。

• 当前正在执行 main 函数,这时发生中断或异常切换到内核态。

• 在中断处理完毕后要返回用户态的main 函数之前检查到有信号SIGQUIT 递达。

• 内核决定返回用户态后不是恢复main 函数的上下文继续执行,而是执行 sighandler 函

数,sighandler 和main 函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个
独立的控制流程。

• sighandler 函数返回后自动执行特殊的系统调用 sigreturn 再次进入内核态。

• 如果没有新的信号要递达,这次再返回用户态就是恢复main 函数的上下文继续执行了。

两个区域:

  • 上半部分(用户态) :这是应用程序(如浏览器、编辑器、游戏)平时运行的区域。在这里,程序只能执行普通指令,访问自己的内存空间。权限最低

  • 下半部分(内核态) :这是操作系统核心(Kernel)运行的区域。只有在这里,程序才能访问硬件(如键盘、硬盘、网卡)、管理内存分配、处理中断。权限最高

一个动作:

  • 红圈 :标记了**"上下文切换"**(Context Switch)的时刻。当轨迹穿过中间的水平线时,说明程序从用户态进入了内核态(或反过来)。

  • 穿越的动作:每次跨越这条线,操作系统都需要保存当前任务的状态(寄存器、栈指针等),加载另一个状态,这会消耗CPU时间。

检查pending表:

程序在内核态执行完任务返回用户态之前,内核会必须检查这个表。如果发现有未处理的信号或中断,内核可能会让程序先执行信号处理函数(这往往导致额外的上下文切换),然后再继续。

捕捉信号要切换四次状态,检查两次pending表。

二操作系统是怎么运行的?

操作系统启动后,就不停地执行一个无限循环 。它有个核心机制**时钟中断(心跳)**来维持运作:时

  • 是什么 :计算机主板上有一个可编程间隔定时器,它会每隔几毫秒(比如 1ms 或 10ms)就产生一个 硬件中断

  • 作用 :这个中断是操作系统的心跳 。每次中断发生时,CPU 会暂停当前运行的程序,强制跳转到操作系统内核的 中断处理程序

  • 为什么重要 :如果没有这个中断,操作系统就不知道时间过去了多久,也无法公平地控制每个程序运行多长时间。它是操作系统进行抢占式调度的物理基础。

2-1硬件中断

由外部设备触发的,中断系统运行流程,叫做硬件中断

这张图揭示了操作系统响应外界事件的底层原理:

  1. 硬件是主动的,CPU是被动的:CPU从来不会主动去"检查键盘有没有人按",而是键盘硬件的信号被中断控制器放大后,"打断"CPU当前的节奏。

  2. 中断是CPU"被迫"换脑子的时刻:通过中断号查表,CPU能快速找到对应的处理函数。

  3. 保存现场是"负责任"的表现:中断处理是以"原子"的方式完成的,必须保证处理完回来时原程序环境不损坏。

通过外部硬件中断,操作系统就不需要对外设进行任何周期性的检测或者轮询

2-1-1系统源码(Linux0.11版本)

中断向量表:

rs_init 是填中断向量表的

2-2时钟中断

时钟中断 就是 由硬件计时器(比如 PIT、HPET、APIC 定时器)每隔固定时间(比如 1毫秒、10毫秒)向 CPU 发送一次的中断信号

问题:

• 进程可以在操作系统的指挥下,被调度,被执行,那么操作系统自己被谁指挥,被谁推动执行呢?

• 外部设备可以触发硬件中断,但是这个是需要用户或者设备自己触发,有没有自己可以定期触发的设备?

这样,操作系统不就在硬件的推动下,自动调度了么!!

2-2-1系统源码(Linux0.11版本)

时钟中断绑定的处理函数void sched_init(void),每次收到时钟中断都会调用该函数。

调用timer_interrupt(void)

调用do_timer()

调用shedule()进行进程调度

2-2-2操作系统是一个死循环

操作系统自己不做任何事情,需要什么功能,就向中断向量表里面添加方法即可.操作系统的本质:就是一个死循环!

这样,操作系统,就可以在硬件时钟的推动下,自动调度了.

主频可以作为OS调度执行速度的参考之一

1.所以CPU为什么会有主频?

就像乐队的节拍器 ,让CPU内部几十亿个晶体管同步工作,避免信号混乱。

2.为什么主频越快,CPU越快?

CPU的主频就像是它的"心跳"或"节拍器"。每一个节拍,CPU都能完成一小步最基本的操作。节拍越快,单位时间内能完成的步数就越多,整体速度自然就越快。

2-3软中断

中断是内核中的一种软件模拟的中断机制

为了让操作系统支持进行系统调用,CPU也设计了对应的汇编指令(int 或者 syscall),可以让CPU内

部触发中断逻辑。

系统调用的过程,其实就是先int 0x80、syscall陷入内核,本质就是触发软中断,CPU就会自动执

行系统调用的处理方法,而这个方法会根据系统调用号,自动查表,执行对应的方法。

系统调用函数指针表源代码

用于系统调用中断处理程序(int 0x80),作为跳转表。

系统调用号就是数组sys_call_table\[\]的下标。

调用系统调用的本质:

系统调用号+软中断

2-3硬件出错,操作系统怎么知道错误类型?

缺页中断?内存碎片处理?除零野指针错误?这些问题,全部都会被转换成为CPU内部的软中断,

然后走中断处理例程,完成所有处理。有的是进行申请内存,填充页表,进行映射的。有的是用来

处理内存碎片的,有的是用来给目标进行发送信号,杀掉进程等等。

三用户态?内核态?

操作系统无论怎么切换进程,都能找到同一个操作系统!换句话说操作系统系统调用方法的执行,
是在进程的地址空间中执行的。

  • 内核是"公共的",但属于每一个进程的"高地址映射"。

  • 系统调用是在"当前进程的地址空间内",由"操作系统(唯一副本)"利用"特权级切换(CPL 改变)"来完成的执行过程。

  • 这种设计让内核能够兼顾高效(直接访问用户数据)与安全(通过特权级隔离)。

CS(Code Segment,代码段寄存器)是 x86 架构 CPU 中的一个寄存器。CS 寄存器中隐含了一个标记位,叫做 CPL (Current Privilege Level,当前特权级)

CPL是如何定义用户态和内核态的?

在 x86 保护模式下,特权级分为 0 到 3 四级。

  • CPL = 0 (Ring 0): 这就是所谓的 内核态。操作系统内核运行在此级别,可以访问所有硬件资源(如直接写 CR3 寄存器刷新页表、访问 I/O 端口、执行特权指令)。

  • CPL = 3 (Ring 3): 这就是所谓的 用户态。普通用户进程运行在此级别,无法执行敏感指令,访问受限。

四sigaction()

类似sighandler()

特性 sighandler (通过 signal()) sighandler (通过 sigaction())
本质 单纯的函数指针 包含函数指针的结构体
信号屏蔽 (Mask) 不保证,可能导致重入 ✅ 通过 sa_mask 明确指定
系统调用重启 默认不重启 (返回 EINTR) ✅ 通过 sa_flagsSA_RESTART 控制
额外信息传递 无 (仅有 int sig) ✅ 通过 sa_sigaction 传递上下文
可靠性 低 (跨平台行为可能不一致) 高 (POSIX 标准,完全可控)

1.如果进程收到大量的重复信号会怎么样?

如果收到大量重复的普通信号,操作系统会将它们合并为一个未决信号 ,导致大量信号丢失,你的处理函数只会被调用很少的次数。

2.处理信号时,其他信号会怎么样?

在处理一个信号期间,其他信号的处理行为取决于你注册信号时的方式(sigactionsa_mask)以及该信号是否被阻塞。

可重入函数?不可重入函数?

以链表头插为例子:

cpp 复制代码
// 全局链表头
Node *list_head = NULL;

// 头插法:非线程/信号安全的版本
void insert_head(Node *new_node) {
    // 步骤 1: 读取当前头
    Node *old_head = list_head;
    
    // 步骤 2: 将新节点指向旧头
    new_node->next = old_head;
    
    // 步骤 3: 更新头指针
    list_head = new_node; 
}

假设主流程正在执行 insert_head刚刚执行完步骤 2 ,正准备执行步骤 3(更新 list_head)。

此时,一个信号来了,触发了你的信号处理函数 sighandler,它也要调用 insert_head 插入一个新的节点。

时间轴如下:

  1. 主流程old_head 读到了当前的 list_head(假设是 Node A)。

  2. 主流程new_node->next 指向了 Node A。(此时 list_head 还没变 ,还是 Node A)。

  3. 信号打断! 暂停主流程,跳转到信号处理函数。

  4. 信号处理函数 :调用 insert_head,要插入 Node B

    • 它读取 list_head(还是 Node A)。

    • 它将 Node B->next 指向 Node A

    • 它将 list_head 更新为 Node B

  5. 信号处理函数返回

  6. 主流程恢复:它继续执行步骤 3。

    • 它将 list_head 更新为 new_node(即 Node X

结局: 链表现在变成了 Node X -> Node ANode B 丢失了! 因为主流程的步骤 3 直接覆盖了信号处理函数对 list_head 的更新。

这就是非重入函数:它依赖全局或静态数据,并且这些数据在操作过程中可能被外部打断修改,导致数据不一致。相反的:可重入函数 (Reentrant Function) 是指:在函数执行过程中,如果被中断(例如被信号处理函数打断,或被多线程调度打断),并在中断期间被再次调用(即"重入"),两次执行的结果都不会出现数据破坏、逻辑错误或死锁的函数。

这就是sigaction()和sighandler()的一个区别。

五volatile

该关键字在C当中我们已经有所涉猎,作用是禁止编译器优化,今天我们站在信号的角度重新理解一下

编译器优化后,发现 while(!flag) 里的 flag 没有在循环体内被修改,于是把 flag 的值缓存到 CPU 寄存器里。信号处理函数虽然修改了内存中的 flag,但循环检查的是寄存器里的旧值,所以陷入死循环。

没有被编译器优化为寄存器变量,所以直接退出。

加上volatile

禁止了编译器优化,所以都成功退出。

volatile 作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该

变量的任何操作,都必须在真实的内存中进行操作

六SIGCHLD信号

子进程退出有反应吗?子进程退出会给父进程发送SIGCHLD信号。该信号的默认处理动作是忽略,父进程可以自 定义SIGCHLD信号的处理函数。

用wait和waitpid函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻 塞地查询是否有子进程结束等待清理(也就是轮询的方式)。采用第一种方式,父进程阻塞了就不 能处理自己的工作了;采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一 下,程序实现复杂。

父进程可以自 定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程 终止时会通知父进程,父进程在信号处理函数中调用wait清理子进程即可。

这段代码的目的是:

  1. 父进程 :注册了 SIGCHLD 信号处理函数 handler。然后进入一个无限循环,每秒打印一次"正在工作"。

  2. 子进程fork 出来的子进程,打印自己的 PID,睡 3 秒,然后调用 exit(1) 退出。

  3. 信号处理 :当子进程退出时,内核会给父进程发送 SIGCHLD 信号。父进程收到后,调用 handler,通过 waitpid 回收子进程。

父进程必须等待子进程吗?

是必须的。不过有特殊情况:把SIGCHILD的处理置为SIGIGN不会产生僵尸进程。

七结语

从信号捕捉的流程,到操作系统中断的底层实现;从用户态与内核态的权限划分,到 sigactionvolatileSIGCHLD 的实践细节,我们终于走完了 Linux 信号的完整知识链路。

信号的本质,是操作系统对 "异步事件" 的响应与调度,它的设计既体现了硬件中断的底层逻辑,也包含了进程管理的设计思想。理解信号,不仅能帮我们写出更健壮的信号处理代码,更能让我们窥见操作系统 "如何管理硬件、调度进程、处理异常" 的核心逻辑。

希望这篇文章能帮你打通 Linux 信号的知识脉络,未来在处理信号相关的问题时,不仅能知其然,更能知其所以然。如果你对其中的某个部分有更深入的疑问,或是想探讨信号处理的工程实践,也欢迎在评论区交流讨论~

相关推荐
.千余1 小时前
【Linux】 TCP进阶详解:字节流、粘包问题、异常情况与UDP完整对比2
linux·运维·c语言·开发语言·经验分享·笔记·php
PascalMing1 小时前
从零实现一款 Windows 下的 SSH 批量运维工具:LinuxSshTools 技术详解
运维·windows·ssh
Bert.Cai1 小时前
Linux chown命令详解
linux·运维·服务器
XMAIPC_Robot1 小时前
基于RK3588 ARM+FPGA电火花数控机床控制系统设计,兼顾ethercat软硬件实时
linux·arm开发·人工智能·嵌入式硬件·fpga开发
青梅橘子皮1 小时前
Linux---进程切换与调度
linux·运维·服务器
底层开发智库1 小时前
C1-Ultra FVP调试并运行Linux kernel全程记录,硬核演示如何解决启动问题
linux·arm开发·内核·嵌入式·arm
utf8mb4安全女神1 小时前
【forwarding】怎么把客户端的日志转发到服务器【日志转发】【rsyslog服务】
运维·服务器
承渊政道2 小时前
Linux系统学习【进程控制:进程创建、终止与等待、进程程序替换、自主shell命令行解释器详解】
linux·服务器·c++·学习·ubuntu·bash·远程工作
志起计算机编程2 小时前
挖掘单节点Clickhouse极致性能上限
服务器·开发语言·python