【C++与Linux进阶】详解信号的捕获:内核态和用户态的转换

本系列主要旨在帮助初学者学习和巩固Linux系统。也是笔者自己学习Linux的心得体会。


个人主页: 爱装代码的小瓶子
文章系列: Linux
2. C++


文章目录

  • [1. 前言](#1. 前言)
  • [2. 软中断:](#2. 软中断:)
    • [2-1 系统调用(典型的软中断)](#2-1 系统调用(典型的软中断))
    • [2-2 中断的分类:](#2-2 中断的分类:)
  • [3. 用户态和核心态:](#3. 用户态和核心态:)
  • [4. 回到信号的捕捉:](#4. 回到信号的捕捉:)
  • 总结:

1. 前言

在上一篇文章中【C++与Linux进阶】信号的处理和Linux系统的调度我们以及讲述了什么是Linux操作系统调度的本质:是躺在时间中断上的死循环。

  1. 硬件中断,是理解时间中断的前提
  2. 时间中断
  3. 死循环
  4. 软中断

今天的内容主要讲述软中断,内核态和用户态,深入理解什么是内核态和用户态。以及虚拟地址的划分。

2. 软中断:

在前面的文章中,我们已经讲了什么是硬件中断,那么可以不可以通过软件来达到因为软件原因,也触发上⾯的逻辑。答案是可以的。这就是我们这一节所讲的软件中断。

那什么是"软件中断"(Software Interrupt)呢?

软件中断通常被定义为:由正在执行的指令(即软件本身)同步触发的一种中断信号。它主要用于请求操作系统的服务,或者处理程序运行时的异常情况。

联系硬件中断,我们来猜测:当我们出现一些系统函数调用的时候,发送中断号应该会触发中断向量表指定的地址,跳转至指定的地方,完成本次任务。

那我们来看看,软件中断其核心是 软中断向量表 ,这与之前中提到的硬件"中断向量表"在逻辑上同构,但完全由软件维护。

在Linux内核源码(例如 kernel/softirq.c)中,软中断机制的核心元素被明确定义:

  1. 软中断向量表 (softirq_vec):这是一个静态定义的数组,每个元素代表一种类型的软中断。
c 复制代码
static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;
  1. 软中断描述符 (struct softirq_action) :该结构体定义了软中断的行为,其中最关键的是一个函数指针 action,指向该软中断的具体处理函数。
c 复制代码
struct softirq_action {
    void (*action)(struct softirq_action *);
};

2-1 系统调用(典型的软中断)

系统调用(如 kill、read、write)是通过触发软中断(如 int 0x80 或 syscall 指令)实现的。这个过程在源码中通过 系统调用表 来完成映射。

  1. 触发软中断 :当用户程序调用 glibc 封装的库函数(如 write())时,库函数会执行 int 0x80 或 syscall 指令。这用软件方式模拟硬件中断,实现宏观上的异步执行效果,CPU会因此陷入内核态。
  2. 查表与分发 :CPU根据软中断号,查找中断描述符表(IDT) ,跳转到统一的系统调用入口程序。该入口程序会从用户指定的寄存器(如EAX)中取出系统调用号。
  3. 执行系统调用 :内核利用系统调用号作为索引,查询 sys_call_table 系统调用表。这张表本质上是一个函数指针数组,每个位置对应一个内核函数(如 sys_write、sys_kill)。通过查表,内核就能精确地跳转到并执行对应的系统调用服务例程。其实系统调用号的本质:数组下标!"。

其实本质我们调用一些系统调用kill、read、write这些函数,其实本质都是通过中断去执行的。 中断处理程序根据系统调用号 (如 __NR_read)跳转到对应的内核处理函数。

几个问题:

  1. 用户层怎么把系统调用号给操作系统?寄存器(⽐如EAX)
  2. 系统调用的过程,其实就是先int0x80、syscall陷入内核,本质就是触发软中断,CPU就会自动执行系统调⽤的处理方法,⽽这个方法会根据系统调用号,自动查表,执行对应的方法。
  3. 当你陷入内核态的时候,如何将值放回用户态:寄存器或者用户传入的缓冲区地址。
  4. 系统调用号的本质:数组下标!

这个软中断,建议联系之前的硬件中断一起来看,软中断(以系统调用为例) :是进程主动 通过执行特定指令(如syscall)触发的一种中断。其目的是请求操作系统内核提供服务。触发后,CPU会从用户态切换到内核态,并根据系统调用号执行内核中对应的服务例程。

或者怎么讲:CPU会根据中断号或系统调用号,跳转到内核中预设的、对应的处理函数去执行。可以理解成为一种受控的,有程序主动发起的的陷阱或者门。

2-2 中断的分类:

我们主要分成两类:

  1. 外部中断(硬件中断 / 异步中断)。
  2. 内部中断(异常 / 同步中断)。

而第二个大类,是比较复杂的,我们详细的讲述了软件中断,其实这里还有:

  • 大儿子: 故障 (Fault) ------ 也就是你说的除以0、缺页错误。
  • 二儿子: 陷阱 (Trap) / 软件中断 ------ 也就是你说的申请调取文件(系统调用)。
  • 还有个小儿子叫 终止/Abort,系统崩溃级别的,咱们先不管它

其实两个大类还是很好分辨的:

  1. 硬件中断:这类中断是由CPU外部的硬件设备产生的,和CPU当前正在执行的代码没有任何直接关系(所以叫异步)。你的"敲击键盘"就属于这一类。这类中断就像是你正在专心看书时,门铃突然响了(外部事件打断)。
  2. 内部中断这类中断是由CPU内部执行当前指令时产生的。也就是说,是因为执行了某行具体的代码才导致的中断(所以叫同步)。

接下来,我们就继续分辨:故障 (Fault) 和陷阱 (Trap) / 软件中断:

  1. 在我的理解中,陷阱通常理解为是有意的进入系统调用。是程序员在代码里写了类似 syscall 的指令,主动把控制权交给操作系统,为了获取高权限的服务(比如读写文件、发网络请求)。
  2. 异常 / 故障 (Fault): 绝对是意外的。程序本来想顺畅跑下去,但指令执行出错了(比如除以0、指针越界读了不该读的内存)。

核心技术区别也是两点,看到怎么放回,带回了什么:

  1. 软件中断 / 陷阱 (Trap): 操作系统帮你办完事后,CPU会回到触发指令的下一条指令继续执行。
  2. 异常 / 故障 (Fault): 操作系统处理完后(如果能处理好的话),CPU会回到刚才发生错误的那条指令,重新执行一次。

3. 用户态和核心态:

回顾之前的过程:

先给出一段话:代码如果调用了open或者write之类的就会陷入内核,去执行中断向量表中的软中断表的服务(是一个数组,指定的下标)去执行,执行完毕就会回到用户态,通过寄存器拿到返回值,继续在用户态继续执行。

  1. "调用了open或者write之类的就会陷入内核" : C语言里调用的 open() 其实是C标准库(glibc)提供的一个包装函数(Wrapper)。这个函数内部藏着一条极其关键的汇编指令(比如老x86的 int 0x80,或者现代CPU的 syscall / sysenter)。正是这条指令,像一个开关一样,瞬间把CPU的执行权限从用户态(Ring 3)切到了内核态(Ring 0)。

  2. 去执行中断向量表中的软中断表的服务(是一个数组,指定的下标)去执行":

    • 第一次查表: CPU根据中断指令,去查"中断向量表(IDT)",找到专门负责系统调用的"总管家"(系统调用处理程序 system_call)。
    • 第二次查表: 这个"总管家"会去查你说的那个数组------系统调用表(System Call Table,sys_call_table) 。在陷入内核之前,C库已经悄悄把一个"系统调用号"(也就是你说的数组下标 )放进了一个特定的寄存器(比如 eax 或 rax)。总管家拿着这个下标,在数组里一指,精准找到了真正的 sys_open 或 sys_write 内核函数去执行。
  3. 内核干完活后,会把结果(比如 open 成功返回的文件描述符 fd,比如 3)塞进约定的寄存器(通常还是 eax/rax)。然后执行一条特殊的返回指令(如 iret 或 sysret),CPU权限降级,穿透回用户态。

那么上面这么一大段话:什么是用户态,什么是内核态:

用户态 (User Mode) 和 内核态 (Kernel Mode) 是现代操作系统(如 Linux)为保护系统稳定与安全而设计的两种 CPU 运行级别 或 特权级别。

对比维度 用户态 (User Mode) 内核态 (Kernel Mode)
权限级别 低特权级(Ring 3)。 高特权级(Ring 0)。
执行者 普通应用程序的代码。 操作系统内核的代码。
能执行的指令 受限。不能直接执行特权指令(如操纵硬件、修改页表)。 可以执行所有指令,包括特权指令,直接访问所有硬件和内存空间。
访问的内存空间 只能访问分配给该进程的用户空间内存。 可以访问整个系统内存,包括所有进程的用户空间和内核空间。
触发场景 应用程序的绝大部分代码都在此态运行。 当发生系统调用 、中断 (硬/软)或异常时,CPU 会切入此态,执行内核代码。
目标 运行用户程序,隔离错误。一个进程崩溃通常不会导致系统崩溃。 管理系统资源,为所有进程提供服务,确保系统安全和协调。

核心比喻 :可以把内核态想象成系统的"管理员模式"或"上帝模式",而用户态则是"受限用户模式"。应用程序平时在"用户模式"下运行,当需要操作系统提供关键服务(如读写文件、创建进程)时,就必须通过 系统调用 这道"门"(触发软中断),临时切换到"管理员模式"去执行,执行完毕后再返回。

3-1聊聊虚拟内存地址和核心态的关系:

在我们的虚拟内存中的高地址(总共 2 32 2^{32} 232 字节,即 4GB 虚拟地址空间)这 3G-4G 的虚拟内存空间,是专属给操作系统内核使用的(内核空间),并且它是被所有进程共享的,但只有当 CPU 处于"内核态"时才有权限访问它。
3G 到 4G(内核空间 / Kernel Space): 这是操作系统的专属 地盘。操作系统的代码(内核代码)、各种设备的驱动程序、用来管理所有进程的表格(如 PCB)都在这里。

那么这个意思就是说:这是虚拟内存设计中最精妙的地方:所有进程的 3G-4G 内核空间,映射的都是同一块物理内存!

为什么必须共享? 因为操作系统只有一个!当微信想要写文件(触发软中断进入内核态),和 QQ 想要写文件时,它们调用的必须是同一套内核代码,修改的必须是同一个系统级的文件管理表。如果不共享,那每个进程都要拷贝一份完整的操作系统在自己的内存里,内存早爆了。

既然微信的虚拟地址空间里包含了 3G-4G 的内核代码,那微信的程序员写一句代码,直接去读取 3G 以上的地址,把系统搞崩溃行不行?
不行!这就是"用户态/内核态"发挥作用的时候了。

  • 虽然这 1GB 的内核空间就在每个进程的虚拟地址范围里,但它被贴上了**"Ring 0 专属"**的封条。
  • 当你的程序在 用户态 (Ring 3) 运行时,CPU 内部的硬件(MMU,内存管理单元)会死死盯着你。一旦发现你想访问 3G 以上的地址,MMU 会立刻打断你,报出一个"段错误(Segmentation Fault)",操作系统会直接把你的程序杀掉。
  • 只有当你老老实实地调用 open() 或 write(),触发了软中断 ,CPU 切换到了 内核态 (Ring 0),此时封条解除,CPU 才可以合法地在 3G-4G 这片区域里读取数据、执行内核代码帮你办事。

4. 回到信号的捕捉:

穿插了这么多知识,先是硬件中断,后面又是时间中断,最后面又是死循环加软中断。最后我们着重分辨了什么是内核态,什么是用户态。

这里其实还是为了详细的理解信号的捕捉:

这个图片就把捕捉信号最详细的流程讲述完毕了。正是 Linux 里面非常经典的**"信号捕捉的 8 字型执行流"**(因为它的执行轨迹在用户态和内核态之间穿梭,画出来刚好是一个无穷大符号 ∞ \infty ∞ 或横着的"8")。

  1. 你的 main 函数正快乐地跑着。突然,你敲击了键盘(如 Ctrl+C 触发硬件中断),或者程序发生异常,又或者发生了一次普通的系统调用。CPU 瞬间切入内核态。
  2. 进入内核态,在内核态查看pending表格和block表格:内核把刚才的中断处理完了,准备回用户态之前(这是处理信号的唯一时机) ,顺手查了一下你说的 block 和 pending 位图。
  3. 发现任务: 内核发现有个未被阻塞的信号,并且你为它注册了自定义的 sighandler。降级执行: 内核修改了返回地址,脱下"管理员马甲",切换回用户态,强制程序跳转去执行你写的 sighandler 函数。
  4. 执行我们的自定义函数,执行完成之后需要回到内核态,找到main函数中执行到哪一步了。 因此,进入内核态,求助于内核:编译器会在 handler 结束的地方,悄悄插入一段特殊的系统调用代码------sigreturn。它再次触发软中断,主动陷入内核。这相当于在喊:"报告内核,我的自定义任务跑完了,请帮我恢复现场!"
    • 内核收到 sigreturn 请求,从自己的小本本里拿出最初被打断时 main 函数的上下文(各种寄存器的值、程序计数器 PC)。完美回归: 内核最后一次切换回用户态,把你送回 main 函数中断前的下一条指令。程序就像做了一场梦,啥也不知道,继续往下执行。

这个就是一个完整的信号流程,我们在面试的时候可以自己在脑海中画出这副图画。用来帮助自己理解。

总结:

这篇文章深入讲解了Linux信号处理与系统中断的核心机制。首先,它从软中断 切入,以系统调用为例,解释了应用程序如何通过指令(如int 0x80)主动触发中断,陷入内核以请求服务,并强调了系统调用号本质上是内核函数指针数组的下标。接着,文章区分了中断的两大类别:外部硬件中断(异步)与内部异常/故障(同步),并细化说明了陷阱(如系统调用)与故障(如除零错误)在处理逻辑上的关键差异------陷阱执行后返回下一条指令,而故障则可能重试原指令。

然后,文章核心阐明了用户态与内核态 的权限隔离:用户态(Ring 3)限制应用程序直接访问硬件和内核内存,而内核态(Ring 0)拥有最高特权,可执行一切操作;所有进程共享同一内核地址空间(3G-4G),但仅在通过软中断陷入内核态时方可访问。最后,文章整合这些概念,清晰描绘了信号捕捉的完整流程 ------即著名的"8字型执行流":从用户态陷入内核,内核在返回前检查并递送信号,切换回用户态执行信号处理函数,处理函数结束时通过sigreturn再次陷入内核恢复现场,最终安全返回到原用户程序。这一闭环过程深刻体现了操作系统通过状态切换和中断机制实现控制权安全转移的精妙设计。

虽然本章内容涉及底层硬件与内核交互,较为抽象,但却是理解Linux系统调度、进程管理和信号机制的核心基础。这些知识将为你打开系统编程的大门,让你不仅知其然,更知其所以然。坚持下去,你就能真正看懂程序与操作系统协同工作的脉络,加油!:

感谢各位对本篇文章的支持。谢谢各位点个三连吧!



相关推荐
qetfw4 分钟前
Debian iptables 防火墙:默认拒绝、SNAT 与 DNAT 验证
linux·服务器·debian·php
老王爱玩车23 分钟前
文件操作从打开到缓冲区
c语言·开发语言·学习
忆挽篱笙歌24 分钟前
冯诺依曼+操作系统+进程
linux·ubuntu
SunnyDays101140 分钟前
Java 合并 PDF 文件:完整合并、指定页面合并与流合并
java·开发语言·合并pdf
INGNIGHT41 分钟前
944 · 最大子矩阵(前缀和)
数据结构·c++·算法
kimnoic1 小时前
Python操作xlwings的实例详解
开发语言·python
Apipi*1 小时前
30天速通Linux 第九章Linux网络编程
linux·运维·网络
All for pursuit.1 小时前
【动态规划-6】96.不同的二叉搜索树
数据结构·c++·算法·leetcode·动态规划
栗子~~2 小时前
java - logstash-logback-encoder 集成 demo
java·开发语言·logback
报错小能手2 小时前
Channel面试题。Sync面试题
linux·服务器·windows