RISC-V ARM x86中断机制对比:一条主线看懂三大架构差异
发布时间:2026/9/5 3:33:52 锦皓数字建站

做嵌入式底层或者系统软件开发的朋友估计都有过这种体验今天调RISC-V核的PLIC明天改ARM核的GIC后天又得去抠x86的APIC。每次切换架构都感觉像是重新学了一遍中断。但实际踩过几个坑之后你会发现这三种架构的中断流程本质上就是同一条主线上的不同变体。这篇文章我把这条主线抽出来用我实际调试过的经验带大家走一遍看明白它们到底哪里一样、哪里不一样。1. 中断流程的主线五步走完一次完整中断不管是什么架构一次完整的中断处理从硬件到软件绕不开这五个环节。1.1 主线全貌事件源、控制器、CPU响应、软件分派、现场恢复我先把这个主线画出来。一次中断从发生到处理完毕大致经历这么几个阶段外设产生中断请求信号比如UART收到一个字节、DMA搬运完成。中断控制器对这个请求进行仲裁、优先级排队、屏蔽/使能控制最终决定把哪一个中断送给CPU。CPU响应中断保存当前的执行状态至少是PC和关键状态寄存器关掉中断使能位跳到中断入口地址。软件在中断入口里读取中断控制器搞清楚是谁触发的中断然后跳转到对应的处理函数。处理完毕恢复现场执行中断返回指令CPU接着执行被打断的程序。这条主线RISC-V、ARM、x86全都逃不掉。区别在于每一步的具体实现方式差别很大。为什么我要强调这条主线因为很多初学者看手册最容易懵的就是这一步RISC-V手册里讲PLIC怎么工作ARM手册里讲GIC怎么配寄存器x86文档里讲APIC和IDT怎么设置看起来完全是三套东西。但你心里只要装着这条主线看手册的时候就清楚自己在找什么——无非就是这几个问题谁在管中断源怎么标记优先级中断向量怎么给的现场谁负责保存返回指令长什么样1.2 三个架构在这条主线上各自的位置我用一句话概括三个架构的定位差异x86历史包袱最重但设计最“傻瓜化”。中断控制器和CPU深度绑定CPU自己接管了大量的现场保存和自动跳转工作。ARM中断控制器GIC是独立IPCPU核和中断控制器解耦。现场保存主要靠硬件自动完成一部分但比x86要更依赖软件。RISC-V走极简路线中断控制器PLIC/CLINT也是独立IP但CPU核只提供最基本的中断响应机制现场保存、中断源识别这些几乎全靠软件来做。这个定位差异直接决定了你在三种平台上写中断处理代码时的工作量完全不一样。我刚开始从ARM转RISC-V的时候最不适应的就是这一点以前GIC帮你做了很多事情到了RISC-V上全部要自己来。2. 外设拍门到CPU开工之间中断控制器在忙什么外设产生的中断请求不是直接连到CPU引脚上的——至少现代高性能处理器都不是这样。中断信号会先汇聚到中断控制器由它来做仲裁和分发。2.1 ARM的GIC分层设计的“物业管家”ARM的中断控制器叫GICGeneric Interrupt Controller。从GIC-400到GIC-500系列再到现在ARMv9平台上的GIC-700架构思路一脉相承分成Distributor和CPU Interface两部分。Distributor负责管理所有中断源SPI共享外设中断、PPI私有外设中断、SGI软件触发中断。它处理的工作是使能/屏蔽、优先级设置、触发方式配置电平触发/边沿触发然后把选中的中断送往CPU Interface。CPU Interface往简单了说就是每个CPU核私有的“门铃”。它负责对当前CPU核进行中断屏蔽PRIORITY MASK、抢占控制Preemption当有中断需要处理时拉高CPU核的IRQ信号。实际调试中要注意的一个点是GIC的优先级数值方向和大部分人的直觉相反——数值越小优先级越高。我之前在论坛上看到不少人配置了GICD_PRIORITY寄存器然后发现中断行为不符合预期最后查出来就是优先级搞反了。这个真是经典误区。2.2 RISC-V的PLIC和CLINT麻雀虽小边界清晰RISC-V这边中断控制器不是CPU核自带的而是由SoC设计者自己选。但业界事实标准是SiFive提出的PLICPlatform-Level Interrupt Controller加CLINTCore Local Interrupt器的组合。PLIC管的是外部中断external interrupts负责所有外设中断源的优先级仲裁、使能、claim/complete流程。CLINT则管理定时器中断timer interrupts和软件中断software interrupts也就是机器模式下的mtime比较器触发的中断和跨核触发的软件中断。有意思的是PLIC处理的流程和GIC有个本质区别GIC在硬件上就把中断分发到了目标CPU核而PLIC是所有CPU核共享一套中断输入具体谁来处理取决于软件让哪个核去claim。PLIC的claim机制是这样的当一个外部中断被触发CPU进入中断服务程序后软件去读PLIC的claim寄存器读完这个寄存器PLIC才把该中断标记为“已被接收”然后仲裁下一个最高优先级的中断。2.3 x86的APIC本地APIC和IOAPIC的老夫老妻x86的中断控制器体系看着复杂拆开其实也简单。IOAPIC负责收集来自总线的外部中断相当于GIC的Distributor/PLICLAPICLocal APIC集成在CPU内部相当于GIC的CPU Interface/CLINT。IOAPIC把中断映射成中断向量然后通过总线发给目标CPU的LAPIC。LAPIC收到后结合自身的TPRTask Priority Register、PRIProcessor Priority做最终裁决决定是否向CPU core提交中断。x86这里值得单独拎出来说的是IRQ和中断向量的概念区分。老式的8259A PIC时代IRQ号和向量号是一一对应的IRQ0对应向量0x20这种到了APIC时代IOAPIC的红区表Redirection Table可以自由地将任意IRQ映射到任意向量。这个灵活性很强大但也要求软件在初始化时把这层映射关系建立清楚否则后面驱动申请IRQ时会一片混乱。2.4 三个控制器的横向对照我做了个表格来对照三家中断控制器的架构分工功能ARM GICRISC-V PLIC/CLINTx86 APIC外部中断收集DistributorPLICIOAPICCPU侧中断提交CPU Interface直接送IRQ引脚PLIC无核内接口LAPIC定时器/软件中断SGI via Distributor / Generic TimerCLINTLAPIC Timer / IPI优先级方向数值越小优先级越高数值越小优先级越高默认TPR值越大屏蔽更高级别中断中断源ID获取读GICC_IAR读PLIC claim寄存器读向量号由IDT直接映射中断完成确认写GICC_EOI写PLIC complete寄存器写LAPIC EOI这个表格建议收藏。实际工作中在两个架构之间切换时这几个对应关系最容易搞混。我自己就曾经在RISC-V上顺手写了GIC风格的EOI操作结果发现PLIC的complete寄存器和GIC的EOI语义还不完全一样——GIC是先EOI后做别的事情PLIC是先完成处理再complete顺序别搞反。3. CPU响应这道坎从取向量到硬件现场保存中断控制器决定“该你上了”接下来就看CPU核自己怎么接住这个中断。3.1 RISC-V的mtvec/stvec向量表的极简实现RISC-V的中断入口设置非常直接写mtvec机器模式或stvec监管模式寄存器指定入口地址。这里有两种模式直接模式Direct所有中断都跳到同一个地址软件进来之后再读mcause/scause寄存器判断是中断还是异常、是什么类型。向量模式VectoredPC跳转到mtvec 4 × cause的位置。因为是每个异常原因对应一个入口所以可以做到中断和异常快速分派。向量模式听着不错但在实际SoC设计里用的反而不多。为什么因为外部中断经过PLIC之后CPU看到的只是machine external interrupt这一个cause值所有的外部中断源还是得靠软件去PLIC那边查。所以向量模式省掉的分派工作有限反而因为要保证各个入口之间的距离和跳转逻辑代码布局上更麻烦一些。RISC-V的向量表本身很简单它就是一组跳转指令。真正需要特别注意的反而是RISC-V的现场保存几乎没有硬件辅助。x86的中断响应会自动把EFLAGS、CS、EIP压栈ARM会保存LR和SPSRRISC-V这边只做了两件事关中断mstatus.MIE清0、跳到入口地址。至于通用寄存器、返回地址统统靠软件自己压栈。这就意味着RISC-V的中断入口代码必须手写汇编save/restore逻辑。第一次写的时候很容易漏掉某个寄存器尤其在带FPU或者向量扩展V扩展的核上还得考虑是否保存浮点状态。这个我在后面避坑部分展开讲。3.2 ARM的向量表和硬件自动保存ARM的中断响应在Cortex-A系列上跟RISC-V的逻辑差不多也是查向量表。向量表在VBAR寄存器指向的地址上每个异常类型对应一个入口。不过ARM在中断响应时序上比RISC-V多做了一步自动保存返回地址到LR_irq、保存CPSR到SPSR_irq。这意味着你在ARM中断入口里可以直接用SUBS PC, LR, #4这种指令实现返回因为LR里的值已经被硬件调整过了。写ARM汇编中断服务程序时这是个很实用的特性。到了ARMv8-A 64位时代异步中断IRQ/FIQ的入口逻辑有了变化异常向量表在VBAR_EL1指向的地址每个异常类型占据0x80字节的入口空间。同步异常和异步异常的保存行为也有一些细节差异但返回地址还需要根据异常类型手动决定是否减量。Cortex-M系列做的更彻底。它把整个中断现场保存都硬件化了响应中断时自动压栈xPSR、PC、LR、R12、R3-R0出栈也自动完成。这也是为什么Cortex-M的单片机开发里经常可以直接用C写中断处理函数连汇编都不用碰。3.3 x86的IDT一张表管所有中断和异常x86走的是另一种路线用一张中断描述符表IDT把256个向量全部定义好。每个向量对应一个门描述符Interrupt Gate或Trap Gate里面记录了目标代码段选择子和偏移量。CPU收到中断后做的事很“重”根据向量号查IDT获取入口地址。自动保存当前EFLAGS、CS、EIP到栈上如果是跨特权级还会自动切换栈并保存SS和ESP。如果是Interrupt Gate硬件自动关中断清除IF位。这个响应过程的含金量在于“自动”两个字。尤其跨特权级时的TSS任务切换机制在x86上是由CPU硬件完成的软件不需要管用户态到内核态的栈切换。这也是为什么x86的syscall/interrupt路径可以做得很快。不过x86这里有个历史包袱IDT里的Interrupt Gate会自动清IF位Trap Gate不会。很多人在写内核的时候误用了Trap Gate导致中断嵌套问题排查半天查不出来。这个细节手册里有但不到踩坑时真不会注意。3.4 三种CPU响应机制的差异对照环节RISC-VARM (Cortex-A)x86入口地址配置mtvec/stvec寄存器VBAR寄存器IDTRIDT表项向量表结构直接模式/向量模式每组4种异常类型每类0x80字节256个门描述符硬件自动保存几乎无仅关中断并跳地址保存LR和SPSR异常模式其他靠软件保存EFLAGS、CS、EIP跨特权级还保存SS/ESP关中断时机响应时由硬件清MIE/SIE由软件在入口处清除或者用GIC的优先级掩码控制Interrupt Gate自动清IFTrap Gate不清返回指令mret/sret手动从LR减量返回或eretiret/iretq4. 现场保护的幕后细节为什么说全寄存器保存是个技术活中断处理的另一半江山是现场恢复。很多初学者以内核崩溃就只知道去查中断处理本身其实问题经常出在保存/恢复的不对称上。4.1 RISC-V的汇编保存/恢复模板RISC-V因为没有硬件自动压栈所以需要软件自己保存所有会用到的寄存器。最典型的就是sw t0, offset(sp)这样的指令逐条保存。我见过最简单也最容易出问题的写法是这样的// 错误示例只保存了部分寄存器且没有调整栈帧 save_context: csrr t0, mepc sw t0, (sp) sw ra, 4(sp) // ... 处理中断 ...这个写法的问题很明显如果中断处理过程中函数调用发生ra会被覆盖返回到错误的地方栈指针也没调整很容易把上下文写进同一个位置。正确的做法是先分配栈空间然后按规则保存所有需要保留的寄存器。这里的关键是弄清楚你的编译器和ABI规定了哪些寄存器是caller-saved哪些是callee-saved。中断服务程序不是一个普通函数它要保证被打断的上下文原封不动地恢复。补充一点关于FPU的如果你的RISC-V核带有F扩展或D扩展还要考虑是否保存浮点寄存器以及是否保存fcsr浮点控制状态寄存器。如果中断处理程序根本不用浮点运算可以通过设置mstatus.FS为off来跳过保存但前提是你能保证整个中断路径里任何函数包括库函数都不会碰浮点寄存器。这个保证在实际工程里很难做到所以我一般建议直接全套保存代价是中断延迟增加一些周期。4.2 ARM的banked寄存器设计少压栈的硬件红利Cortex-A系列的IRQ模式和FIQ模式各自有独立的SP和LR。这意味着在IRQ模式下你不需要像RISC-V那样在中断一开始就把原模式的SP和LR压栈保存因为硬件已经帮你备好了专门的IRQ模式的SP和LR。但这个设计也有坑IRQ模式下只有几个banked寄存器通用寄存器R0-R12还是共享的。所以如果中断处理程序要使用R0-R12仍然需要自己保存。FIQ额外多banked了R8-R12所以FIQ的中断延迟可以做到更低——这是FIQ设计之初的目标但现在大部分系统直接用IRQ就够了。Cortex-M就完全不一样了它使用的是线程模式处理模式两套栈指针MSP/PSP中断响应时硬件自动压栈8个寄存器出栈也自动。所以在Cortex-M上写中断大部分时间可以用C直接写不用手刨汇编。4.3 x86的中断栈和TSS从Ring3到Ring0的自动切换x86中断响应里最特殊的就是栈切换逻辑。如果中断发生在用户态Ring3CPU会从TSS中读取Ring0的栈指针SS0:ESP0然后自动压入用户态的SS、ESP、EFLAGS、CS、EIP。这个动作完全硬件化软件不需要参与。这个设计极大简化了内核的入口代码。你在Linux内核的entry_64.S里能看到真正进入C代码之前其实已经有一套完整的pt_regs结构在栈上了。你只需要在这个基础上继续保存其它寄存器R8-R15等因为x86的通用寄存器数量多硬件只自动保存了最核心的几个。另一个x86特有的操作是中断返回时的栈平衡检查。iretq指令会同时弹出RIP、CS、EFLAGS以及可能的RSP和SS如果入口和出口的栈操作不对称iretq会弹出一个非法的RIP直接崩溃。这类问题比ARM上的LR减量错误更隐蔽因为崩溃的症状经常是随机的。5. 中断返回一条指令和它背后的物联网中断返回指令看似简单实际暗藏不少门道。那个老问题——为什么ARM的返回要SUBS PC, LR, #4x86的返回要iretRISC-V的返回要mret——里面装的是历史。5.1 ARM的PCLR−4之谜ARM的经典中断返回是这样SUBS PC, LR, #4。为什么减4因为ARM流水线的原因当CPU在执行指令时PC实际指向的地址是当前指令地址8ARM状态或4Thumb状态。中断发生时LR保存的值可能是PC4或者PC8不同异常类型有差异。IRQ的返回需要减4才能回到被打断的那条指令的正确地址。这个“减多少”的规则是处理器手册里写死的不同模式不一样IRQ减4FIQ减4Prefetch Abort减4Data Abort减8SVC不用减。刚接触ARM汇编的人在写中断返回时经常记混我一开始也是这样后来总结了一个口诀跟流水线有关的都要减跟同步异常有关的按异常类型特定处理。ARMv8-A 64位上eret指令直接从ELR_ELx寄存器恢复PC不再需要手动减量因为ELR_ELx在异常响应时已经被硬件设置成正确的返回地址了。所以新架构上这个历史坑已经没了但如果还在维护32位ARM的代码这个减4的规则还是得烂熟于心。5.2 x86的iret/iretq特权级变换的开关x86的iret指令不只是恢复PC它会同时恢复EFLAGS和CS寄存器因此可以从Ring0回到Ring3反之也行但实际退出到用户态返回时候用的是sysret/sysexit这类更快的指令。iret在恢复过程中有一个很微妙的行为如果栈上的CS值表明要跨特权级返回CPU会同时恢复用户态的SS和ESP并且从内核栈上弹出这些值。这就意味着内核栈上的内容布局必须严格匹配CPU预期不能有半点差错。否则CPU会尝试从一个错误的栈位置弹数据然后产生#GP异常。现代x86内核里中断返回路径通常会检查一些条件来决定用iretq还是优化过的sysretq路径。普通中断结束用iretq系统调用返回用sysretq。这两个路径的栈处理细节不一样别混。5.3 RISC-V的mret/sretCSR三件套的写回RISC-V的中断返回指令mret做三件事从mepc恢复PC恢复mstatus中的MIE中断使能根据mstatus.MPP恢复之前的工作模式Machine/Supervisor/User这三件事是硬件一次性完成的所以你在RISC-V中断里保存和恢复mepc、mstatus这些CSR时要特别小心。如果在中途修改了mstatus却没有正确保存/恢复mret会把一个错误的值写回轻则中断使能状态不对重则模式错乱直接跑飞。这里有一个很实际的经验RISC-V的mret不会自动关中断它只是恢复中断使能状态。所以如果你的中断入口在保存上下文过程中开了中断返回时就会产生嵌套中断的复杂情况。一般建议整个过程保持中断关闭直到所有现场都恢复好最后执行mret一次到位。6. 从零到能跑的中断初始化三种架构的启动差异理论讲完落到实操上。三者的初始化步骤有着完全不同的工作量分配。6.1 ARMGIC初始化向量表配置ARM中断初始化的标准流程大概是设置VBAR指向一组已定义好入口的异常向量表。初始化GICDistributor使能、CPU Interface使能。配置中断路由将SPI路由到目标CPU、优先级和触发方式。在GICD_ISENABLER中使能特定中断源。开CPU的IRQ总中断CPSR的I位清零。这里面最容易遗漏的是GIC的“使能顺序”问题。不同版本的GIC对Distributor和CPU Interface的使能顺序有讲究。GIC-400上通常需要先配好Distributor和CPU Interface寄存器再使能否则某些初始化状态下的脏中断可能会被打进来。这个问题在Linux内核的GIC驱动里有很多注释侧面印证了它确实是经典坑。另一个坑是GIC的SGI踩踏。在多核系统中发送SGI时如果没有保证内存屏障接收核可能读不到发送核之前写好的数据。ARM的内存模型里SGI和你正常的内存访问之间需要显式加屏障否则会出现“中断到了数据还没到”的奇怪时序。6.2 RISC-VPLIC只有一套极简的开关RISC-V的中断初始化步骤上看起来很简单配置mtvec指向入口。初始化PLIC在PLIC的PENDING/ENABLE寄存器里使能对应中断源。设置PLIC优先级寄存器以及目标CPU的阈值寄存器threshold。开全局中断设置mstatus.MIE以及mie寄存器里的MEIE。写mret或直接启动第一个任务时自然使能中断。看起来比ARM少很多但RISC-V的这些步骤在全软件栈上给你挖了额外的坑PLIC的ENABLE寄存器是分上下文的。多核系统里每个hart硬件线程有自己独立的PLIC上下文你在hart0上使能了中断却跑到hart1上去等中断那肯定等不到。还有就是PLIC的优先级和阈值需要在中断使能之前设置好。因为PLIC是根据当前运行的优先级来仲裁的阈值寄存器设置不当可能出现低优先级中断一直无法触发或者高优先级中断被低优先级任务持续打断的极端情况。我在调试一个多核任务系统时遇到过类似问题查了半天PLIC寄存器最后发现是阈值设错了。6.3 x86IDT加载8259A/APIC模式切换x86的初始化流程对现代操作系统来说是这个样子构造IDT把中断门/陷阱门填充好通过lidt加载IDTR。如果是兼容老式8259A的环境需要屏蔽/重映射PIC。初始化Local APIC设置SPIVSpurious Interrupt Vector Register、LDR/DFR逻辑/扁平目的地、TPR。配置IOAPIC的红区表Redirection Table把设备IRQ映射到想要的向量号。写EOI之前保持IRQ线和向量号的映射关系清晰。有意思的是在现代Linux启动早期CPU会先用8259A方式响应中断在切换到APIC模式后再重设IOAPIC。这个切换过程要求时序严格正确否则早期键盘中断或者时钟中断会丢失。我第一次在内核启动早期调试串口时遇到的中断不触发问题最后查出来就是CGA/串口的中断还在走老PIC中断线而PIC已处于屏蔽状态两个控制器之间没有做好overlay。7. 一条主线上的三套具体方案对比中断延迟与嵌套控制最后我们从整体视角再看一眼这条主线上三个架构的核心权衡顺便聊一下几件容易被忽略的事情。7.1 中断延迟的构成差异中断延迟通常指的是从外设触发中断到CPU开始执行第一条中断服务指令的时间。这个时间主要由两部分构成中断控制器的仲裁时间CPU的响应时间。x86走的是高度硬件化的路线。APIC在硬件上可以完成目标CPU的选择和向量生成CPU响应也是自动压栈跳转。这让它的中断延迟下限可以做得很低但代价是硬件复杂度高。ARM在GIC上有一个比较复杂的优先级和抢占模型能做到中断嵌套的硬件控制延迟取决于GIC的时钟和优先级仲裁策略。GIC的抢占机制是通过CPU Interface的Running Priority实现的——高优先级中断可以抢占低优先级中断正在执行的场景但这个能力需要在初始化时明确配置。RISC-V因为极简PLIC本身不处理抢占需要软件配合去实现嵌套控制。裸机环境下RISC-V的中断延迟可以非常短但代码逻辑上必须保证中断服务程序能快速完成claim否则PLIC的仲裁效率会浪费。7.2 嵌套中断的三个架构答案嵌套中断也就是中断里再打断一个中断是实际工程里绕不开的话题。x86的经典答案是Interrupt Gate自动清IF所以默认不嵌套要想支持嵌套得在中断服务程序里自己开IF。这个设计很保守但容易理解。ARM的GIC则提供了硬件级的优先级抢占支持。高优先级中断可以直接抢占当前正在执行的低优先级中断服务程序——只要GICC_CTLR的EnableGrp1配置符合预期。硬件自动完成上下文的压栈/出栈但对软件要求更高每个中断服务程序都必须正确地保存和恢复自己的上下文否则嵌套之后现场就毁了。RISC-V的PLIC本身不提供抢占它只负责仲裁哪一个中断优先给CPU至于CPU是不是在中断里PLIC不关心。所以RISC-V要支持嵌套得这么干中断入口先关全局中断保存上下文然后根据优先级决定是否重新使能全局中断允许高优先级中断打入。这个逻辑全靠自己控制写成通用框架时要非常小心。7.3 实际调试中的经验清单最后分享几个我踩过的、具有广谱参考价值的坑中断服务程序中不能随意调用printf或任何可能触发系统调用的函数。在RISC-V和ARM的裸机环境里这类函数内部可能会开关中断或者调用锁容易造成死锁或时序错乱。调试中断内的日志输出建议用一个环形缓冲区先记录回到主循环再统一输出。多核系统的IPI核间中断数据一致性必须重视内存屏障。不只是ARM有这个问题RISC-V的多核也一样。CPU核A写数据后触发IPICPU核B收到IPI去读数据——如果没有屏障B可能读到旧值。这在三种架构上排查方式不同但根因都一样内存序。电平触发和边沿触发的中断在清除标志时行为完全不一样。电平触发中断必须清除外设硬件的中断标志否则中断线一直有效中断服务程序返回之后立刻再次进入边沿触发中断则要防止漏掉被屏蔽期间到达的边沿脉冲。ARM和x86的中断控制器都支持这两种触发方式RISC-V的PLIC里有的版本也能够配置但很多SoC直接固定成电平触发。设计驱动的时候要提前确认好。中断返回之前的最后一步操作一定要严格跟架构对应。ARM的ERET、x86的IRETQ、RISC-V的MRET这三条指令对处理器状态的恢复范围各不相同误用了任何一种都会产生难以排查的故障。有一条主线做骨架三种架构的中断流程就变成了一套方法论在不同硬件上的具体落地。希望这篇文章能帮大家把知识串起来以后遇到新架构的中断相关问题心里先画这条五步线排查起来思路就清晰很多。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。