找回密码
立即注册
搜索
发回帖 发新帖

6311

积分

0

好友

796

主题
发表于 2 小时前 | 查看: 3| 回复: 0

中断管理系列

你写了个中断处理函数,注册到内核里。外设触发中断,GIC 把信号送给 CPU。然后你的函数就被调用了。中间发生了什么?

从 CPU 收到 IRQ 信号,到 C 语言函数第一行代码执行之间,硬件和汇编联手完成了一整套「现场保护 + 状态切换」。这套机制你看不见,但每一条中断都走一遍。

中断处理的第一行代码不是你写的,是硬件写的。

ARM64 中断处理全景图:VBAR_EL1 异常向量表与 pt_regs 栈帧结构

01 ARM64 的异常等级

ARM64 有四个异常等级(Exception Level),从低到高是 EL0、EL1、EL2、EL3。

  • EL0——用户态,应用程序跑在这里,权限最低
  • EL1——内核态,操作系统内核跑在这里,能访问所有硬件资源
  • EL2——虚拟化层,Hypervisor 跑在这里,管理虚拟机
  • EL3——安全监控层,TrustZone 安全世界切换的入口,权限最高

中断属于异步异常。IRQ 和 FIQ 都是异步的——它们随时可能来,和当前执行的指令没有关系。

当 CPU 在 EL0 跑用户程序时收到 IRQ,硬件会自动切到 EL1,然后跳转到内核指定的异常向量入口。

异常等级只能升不能降。

低等级收到异常,跳到高等级处理。处理完用 eret 指令返回原等级。硬件保证你在用户态不能直接跳到内核代码里执行——除非通过异常入口。

02 硬件自动做的五件事

IRQ 信号到达 CPU 的那一瞬间,硬件自动完成五件事。不需要一条指令,纯硬件行为。

序 动作 存到哪
1 保存当前 PSTATE 状态 SPSR_EL1 寄存器
2 保存返回地址 ELR_EL1 寄存器(下一条指令地址)
3 PSTATE.DAIF 全部置 1 自动关 Debug / SError / IRQ / FIQ
4 切换异常等级 + 换栈 切到 EL1,使用 EL1 的栈指针
5 跳转到异常向量表 VBAR_EL1 + 对应偏移地址

第三件事最容易被忽略——硬件自动把 PSTATE 的 D、A、I、F 四位全部置 1。这意味着中断一进来,IRQ 和 FIQ 就已经被关掉了。

不需要软件手动关中断。硬件在跳转到异常向量之前就已经关了。

很多人以为中断处理函数开头需要自己关中断。

不用。硬件已经关了。你在中断处理入口处看到的那些 enable_da_f 之类的指令,是开 D(调试异常)、A(SError)、F(FIQ),I(IRQ)位仍然保持关闭——IRQ 还是关着的。

03 异常向量表

VBAR_EL1 是异常向量表基地址寄存器。内核启动时把向量表的地址写进这个寄存器。

向量表里每一项占 128 字节(用 .align 7 对齐),正好能放几条跳转指令。总共 16 个入口,分成四组。

组别 Sync IRQ FIQ Error
EL1t(EL1 用 EL0 栈) +0x000 +0x080 +0x100 +0x180
EL1h(EL1 用 EL1 栈) +0x200 +0x280 +0x300 +0x380
EL0 64 位 +0x400 +0x480 +0x500 +0x580
EL0 32 位 +0x600 +0x680 +0x700 +0x780

内核态(EL1)触发的 IRQ,走的是 EL1h IRQ 入口,偏移量 0x280。

用户态(EL0)触发的 IRQ,走的是 EL0 64 位 IRQ 入口,偏移量 0x480。

为什么分 EL1t 和 EL1h?t 代表 SP_EL0(用 EL0 的栈指针),h 代表 SP_ELx(用当前 EL 的栈指针)。Linux 内核在 EL1 始终用 SP_EL1,所以只关心 EL1h 那一组。

04 el1_irq 汇编流程

从向量表入口跳到 el1_irq 标签后,汇编代码按以下顺序执行:

  1. kernel_entry——把所有通用寄存器压栈保存
  2. enable_da_f——打开 D、A、F 位(IRQ 仍关)
  3. irq_handler——调用 C 语言层面的中断处理函数
  4. el1_preempt——如果支持内核抢占,检查是否需要调度
  5. kernel_exit——恢复所有寄存器,执行 eret 返回

enable_da_f 不是开中断。

它只开 D(调试异常屏蔽)、A(SError 异步异常屏蔽)、F(FIQ 屏蔽)这三位。I 位(IRQ 屏蔽)保持为 1,也就是 IRQ 仍然关闭。整个硬中断处理期间,本地 CPU 的 IRQ 都是关着的。

05 pt_regs 栈框结构

kernel_entry 把寄存器全部压到内核栈上,形成一个 pt_regs 结构体。栈从高地址向低地址增长。

这个栈框里存了中断发生时的完整 CPU 状态,C 代码里能通过指针直接读。

字段 来源 说明
x0 ~ x30 通用寄存器 共 31 个,全部压栈
sp 栈指针 中断前的栈地址
pc ELR_EL1 中断返回地址(下一条指令)
pstate SPSR_EL1 中断前的程序状态
orig_x0 x0 原始值 系统调用时保存,中断里也保留
syscallno 系统调用号 中断路径上通常为 -1

pt_regs 是中断上下文和进程上下文的桥梁。Oops、信号投递、系统调用返回值,都靠修改栈框里的值来完成。

06 保存与恢复

kernel_entry 做两件事:

  1. 把 x0 到 x30 全部 stp 压栈。
  2. 从 ELR_EL1 和 SPSR_EL1 读出返回地址和状态,也存进栈框。

kernel_exit 做对应的逆操作:

  1. 从栈框里取出返回地址和状态,写回 ELR_EL1 和 SPSR_EL1。
  2. 把 x0 到 x30 从栈里 ldp 恢复。
  3. 最后执行 eret 指令——硬件把 SPSR_EL1 写回 PSTATE,跳到 ELR_EL1 指向的地址,同时切回原异常等级。

eret 不是普通的 ret。

普通 ret 只改 PC。eret 同时恢复 PC、PSTATE 和异常等级。三件事一起做,原子完成。中间不可能被打断。

07 三个常见坑

坑 1:以为中断能嵌套

IRQ 一进来硬件就关了本地中断。同一个 CPU 上,硬中断处理函数执行期间不会再响应新的 IRQ。只有在中断处理中显式调用 local_irq_enable 打开中断,才可能发生嵌套。Linux 默认不这么做。

坑 2:在中断处理里改了 PSTATE 却没恢复

中断返回时 kernel_exit 从栈里的 pt_regs 恢复 pstate。如果你在 C 代码里直接改了 PSTATE 寄存器(比如用 msr 指令),返回时会被栈里的值覆盖掉。想改中断返回后的状态,要改 pt_regs 里的 pstate 字段,不是改当前 PSTATE 寄存器。

坑 3:中断栈溢出

ARM64 有四个异常等级(Exception Level),但内核栈通常只有 16KB 或 32KB。中断来了,pt_regs 要占一大块,再加中断处理函数的栈帧,如果嵌套调用深了很容易溢出。所以中断处理里别开大数组,别搞深度递归。重活丢给软中断或工作队列。

本篇小结

阶段 谁做的 核心动作
中断到达 硬件 存 SPSR/ELR、关 DAIF、切 EL1 + EL1 栈、跳 VBAR_EL1+偏移
入口汇编 kernel_entry x0-x30 + ELR + SPSR 全部压栈,形成 pt_regs
开部分异常 enable_da_f 开 D/A/F,IRQ 仍关
C 层处理 irq_handler 调用 gic_handle_irq,走 irq_desc → action 链
返回 kernel_exit + eret 恢复寄存器,写回 ELR/SPSR,eret 原子返回

硬件做的事永远比你以为的多。你写的中断处理函数,只是整条链路上最后一环。更多底层系统知识,欢迎到 云栈社区 一起交流。




上一篇:格雷厄姆排列猜想破解:数学家如何用随机性补齐55年证明
下一篇:Paperless-ngx 自托管文档管理:发票合同 OCR 归档与全文检索
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-10-6 03:39 , Processed in 0.065506 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表