Contents

从外设IRQ到Linux ISR:RISC V中断机制与Linux中断处理全过程

本文将从外设中断请求(IRQ)如何到达 CPU、CPU 如何决定是否响应中断、Linux 内核如何处理中断以及中断处理完成后如何返回用户态等全过程进行详细解析。通过对 RISC-V 架构的中断机制和 Linux 内核中断处理流程的深入分析,帮助读者理解从外设发出中断请求到 Linux ISR 执行的完整路径。

1. 从外设中断到 hart:中断请求如何到达 CPU

首先我们来了解一些关于 interrupt / exception / trap的基本概念:

  • interrupt: 中断是由外部设备或CPU本身产生的改变处理器正常执行指令顺序的异步事件
  • exception: 异常是指令执行出错改变处理器正常执行指令顺序的同步事件
  • trap: 当中断或异常发生时, CPU通过trap跳转到处理对应中断或异常事件的入口

因此 Interrupt 和 Exception 描述的是事件来源,Trap 描述的是 CPU 对这些事件进行处理时发生的控制流切换.

外设产生 IRQ

外设通常有自己的状态寄存器和 interrupt enable 控制. 例如UART开启 RX interrupt 后, 当接收到数据, RX FIFO 非空, 进而 RX interrupt condition 置位. 如果 RX interrupt enable 为1, UART 向中断控制器 PLIC 发出 IRQ 中断请求.

外设通知中断控制器主要有两种方式: Level-triggered 与 Edge-triggered. Level-triggered IRQ 表示一种持续状态, 例如 UART RX FIFO 只要仍然非空, IRQ 就可能一直保持有效. Edge-triggered IRQ 表示某个瞬间发生了事件. 中断处理程序对这两种类型的中断有不同的处理方式.

PLIC 仲裁并通知 Hart

设备 IRQ 中断信号到达 PLIC 后首先经过 Interrupt Gateway. Gateway 接收到一个中断请求后, 会在 PLIC 中建立对应的 pending 状态, 即 IP[source] = 1. PLIC pending 并不会立即通知 hart, 对于某个 PLIC context, 只有满足 IP[source] = 1 && IE[context][source] = 1 && priority[source] > threshold[context]时, 该中断才是当前 context 的 eligible interrupt, 其中:

  • IP: 该 source 是否存在 pending request
  • IE: 该 context 是否允许接收该 source
  • priority: 该 source 的中断优先级
  • threshold: 该 context 当前接受中断的最低优先级门槛

如果存在符合条件的中断, PLIC 会向对应 hart context 发出 External Interrupt Notification, 最终表现为 hart 的 external interrupt pending 状态有效, 例如 sip.SEIP = 1.

2. hart 如何决定是否响应中断

当 IRQ 已经送到 hart 后, hart 根据CSR寄存器配置决定是否响应中断. 在不支持虚拟化的 RISC-V 中, 只有在 M 和 S 特权等级才可以处理, 默认情况下, 任何特权级别的所有陷阱都在 M 特权级别中处理, 但是也可以通过委托使某些中断/异常由较低特权级别直接处理.

中断寄存器

mstatus 寄存器包含全局中断使能 MIE 和 SIE 分别用于 M 模式和 S 模式, 当 硬件线程 在特权模式 x 下执行时,如果 xIE=1,则中断全局使能;如果 xIE=0,则中断全局禁用。 sstatus 寄存器是 mstatus 的子集.

mip 寄存器是一个 MXLEN 位的读/写寄存器, 包含有关挂起中断的信息, 而 mie 是相应的 MXLEN位读/写寄存器,包含中断使能位.

mip的标准部分(用更美观的方式画出来):

0|LCOFIP|0|MEIP|0|SEIP|0|MTIP|0|STIP|0|MSIP|0|SSIP|0

mie的标准部分(用更美观的方式画出来):

0|LCOFIE|0|MEIE|0|SEIE|0|MTIE|0|STIE|0|MSIE|0|SSIE|0

mip.MEIP 和 mie.MEIE 位是机器级外部中断的中断挂起和中断使能位. MEIP 在 mip 中是只读的,由平台特定的中断控制器设置和清除。 mip.MTIP 和 mie.MTIE 位是机器定时器中断的中断挂起和中断使能位。MTIP 在 mip 寄存器中是只读的,通过写入内存映射的机器模式定时器比较寄存器来清除。 mip.MSIP 和 mie.MSIE 位是机器级软件中断的中断挂起和中断使能位。MSIP 在 mip 中是只读的,通过访问内存映射的控制寄存器来写入,这些寄存器由远程 硬件线程 用于提供机器级处理器间中断。

如果没有实现监管模式, mip 的 SEIP、STIP 和 SSIP 位以及 mie 的 SEIE、STIE 和 SSIE 位是只读 0.

如果实现了监管模式,mip.SEIP 和 mie.SEIE 位是监管级外部中断的中断挂起和中断使能位。SEIP在 mip 中是可写的,可以由 M 模式软件写入以向 S 模式指示外部中断挂起. 此外,平台级中断控制器 可能会生成监管级外部中断。监管级外部中断的挂起基于软件可写的 SEIP 位和外部中断控制器的信号的逻辑或.

如果实现了监管模式,mip.STIP 和 mie.STIE 位是监管级定时器中断的中断挂起和中断使能位。STIP 在 mip 中是可写的,可以由 M 模式软件写入以向 S 模式传递定时器中断。 如果实现了监管模式,mip.SSIP 和 mie.SSIE 位是监管级软件中断的中断挂起和中断使能位。SSIP在 mip 中是可写的,也可以由平台特定的中断控制器设置为 1.

sip 和 sie 寄存器是 mip 和 mie 寄存器的子集, 如果通过设置 mideleg 寄存器中的位将中断委托给 S 模式,则它在 sip 寄存器中可见,并且可以使用 sie 寄存器进行屏蔽。 否则,sip 和 sie 中的相应位为只读 0。

机器中断委托(mideleg)寄存器是一个 MXLEN 位的读/写寄存器, 用于指示某些异常和中断应由较低特权级别直接处理。

中断响应判断

以一个已经委托给 S-mode 的 interrupt i 为例, 当 hart 在 S/U mode 时中断到来, 满足以下条件, 中断响应会陷入到 S mode:

  1. sstatus 寄存器中的 SIE 位被设置 或者 当前特权等级低于 S-mode
  2. 位 i 在 sip 和 sie 中均被设置
  3. 位 i 在 mideleg 中设置

全局中断使能 xIE 的含义是: 当前正在 x-mode 执行时,是否允许 x-mode interrupt 打断当前执行. 当前 privilege 比目标 privilege 低时,目标 privilege 的 interrupt 可以直接抢占,不要求 xIE=1

中断打断指令时机

Interrupt 是异步事件,但 CPU 并不会让软件观察到一条指令“执行到一半”的架构状态。为了保证精确中断(precise interrupt)语义,使软件能够在中断处理结束后正确恢复执行,interrupt trap 发生在明确的指令边界:

instruction N 完成 
        ↓ 
Interrupt Trap 
        ↓ 
instruction N+1 尚未执行

trap 发生时会保存返回后应该继续执行的PC.


3. 从 interrupt trap 到 Linux 内核

当 CPU 决定响应一个委托给 S-mode 的 interrupt 时,会进入 Interrupt Trap。为了能够在中断处理完成后恢复原来的执行流,硬件首先保存必要的 Trap 状态,Linux 再保存完整的软件上下文并处理中断。

Trap Entry

进入 S-mode Trap 时,硬件自动完成:

  • 执行位置保存:将中断处理结束后需要继续执行的指令地址保存到 sepc 中;
  • Trap 原因记录:在 scause 中记录这次 Trap 是中断还是异常,以及具体原因;
  • 中断状态保存:将 Trap 发生前的 SIE 状态保存到 SPIE;
  • 关闭同级中断:清除 SIE,避免 Trap 入口尚未完成上下文保存时再次被 S-mode 中断打断;
  • 原特权级保存:将 Trap 发生前的特权级记录到 SPP 中;
  • 特权等级切换:如果 Trap 来自 U-mode,则 CPU 切换到 S-mode;
  • 跳转 Trap 入口:根据 stvec 找到 Trap 入口地址,并从该地址开始执行。

其中 stvec.MODE=Direct 时所有 Trap 都进入 BASEVectored 模式下 Interrupt 进入 BASE + 4 × cause

Linux 中断入口

Linux 会将 stvec 配置为自己的 Trap 入口。当 CPU 跳转到该入口后,首先由汇编代码保存当前通用寄存器以及 sepc、sstatus、scause、stval 等状态,形成完整的 Trap 上下文。

随后 Linux 读取 scause 判断当前发生的是中断还是异常:

  • 如果是 Interrupt,则进入 Linux IRQ 处理路径;
  • 如果是 Exception,则根据具体的 Exception Code 进入 Page Fault、Illegal Instruction、ECALL 等对应的异常处理路径。

硬件负责保存最基本的 Trap 状态并跳转到 stvec,Linux 负责保存完整上下文、读取 scause 并进入具体的中断处理程序。

中断完成后返回

中断处理完成后,Linux 会按照进入 Trap 时保存的内容恢复通用寄存器以及相关 CSR,最后执行 sret 返回。

执行 sret 时,硬件会:

  • 根据 SPP 恢复 Trap 前的特权级;
  • 将 SPIE 中保存的状态恢复为 SIE;
  • 将 PC 恢复为 sepc 中保存的地址;
  • CPU 从中断发生前被打断的位置继续执行。

4. 中断的软件处理程序

CPU 进入 External Interrupt Trap 后,软件首先需要确定具体是哪一个外设产生了中断,然后根据 IRQ 的触发方式选择合适的处理流程。中断处理中经常出现以下操作:

操作作用
mask暂时禁止该 IRQ 继续通知 CPU
ack告诉中断控制器当前中断已经被软件接收,通常会清除控制器内部记录的 pending 状态
claimPLIC 特有的确认操作,读取当前最高优先级 Interrupt ID,同时清除对应的 PLIC IP
clear source操作设备本身,清除真正产生中断的设备状态
complete / EOI告诉中断控制器这一轮中断已经处理完成,可以继续接收该 source 的后续请求
unmask重新允许该 IRQ 向 CPU产生中断通知

需要注意的是: ack/claim 清除的是 PLIC 中的 pending;clear source 清除的是外设内部产生中断的条件.

PLIC Claim / Complete

当 CPU 因 External Interrupt 进入中断处理程序后,会读取 PLIC 的 claim 寄存器。PLIC 从当前 eligible interrupt 中选择优先级最高的一个,将对应的 Interrupt ID 返回给 CPU,同时清除该 source 的 IP pending 位。

因此 claim 在 PLIC 中同时完成了两个工作:

  1. 中断识别: 告诉软件具体是哪一个 IRQ;
  2. 中断确认:清除 PLIC 已经记录的这一轮 pending request。

但是 claim 并不意味着这个设备已经处理完成。软件执行 ISR 处理完设备后,还要将 Interrupt ID 写回 claim/complete 寄存器执行 complete。在收到 complete 之前,PLIC Gateway 不会继续向 PLIC Core 转发同一个 source 的下一轮 interrupt request。

所以对于 PLIC,可以理解为:

  • claim 负责接收当前这一轮请求;
  • complete 负责释放这个 source,让 Gateway 可以继续提交下一轮请求。

Level IRQ 和 Edge IRQ 的区别,主要就体现在这两个操作之间软件需要做什么,以及什么时候适合重新允许新的中断请求。

Level-triggered IRQ:防止同一个状态反复触发

Level IRQ 表示的是一个持续存在的设备状态, 如果设备内部的中断条件还没有消失,就重新允许新的中断请求,那么 CPU 会因为同一个状态再次进入中断.

一个典型的 Level IRQ 处理过程可以拆成以下几个动作。

  1. 确认当前中断:CPU 读取 PLIC claim,取得 Interrupt ID,同时清除 PLIC 中这一轮 IP pending.
  2. 阻止同一个 IRQ 立即重新进入:对于通用 level interrupt controller,软件通常会先 mask 当前 IRQ,再执行 ack。Linux 的通用 Level IRQ flow 也是先执行 mask_ack,然后才调用设备 handler。

对于 PLIC,这一部分有所不同。PLIC Gateway 在收到 complete 之前本身就不会继续向 Core 提交同一个 source 的下一轮请求,因此很多情况下不需要额外依靠 mask 来实现这一层流控。

  1. 处理设备并 clear source:驱动读取设备状态,完成实际的设备处理,并消除产生 Level IRQ 的条件。
  2. 结束当前中断:设备状态已经处理完成后,软件向 PLIC 执行 complete,告诉 Gateway 当前这一轮 IRQ 已经结束。
  3. 重新允许 IRQ:如果前面显式执行过 mask,此时再执行 unmask,允许下一轮 IRQ 到达 CPU。

这里最关键的是 clear source 必须真正处理掉产生 Level IRQ 的设备状态。如果软件已经 claim 了 IRQ,却没有清除 UART RX FIFO,那么 UART IRQ 仍然保持有效。当 PLIC 收到 complete、重新允许该 source 产生请求后,Gateway 会发现 Level 仍然有效,于是再次向 PLIC Core 提交 interrupt request。PLIC 规范明确规定了这种行为。

Level IRQ 的核心处理思想是:先控制当前 IRQ 不要重复进入,在 ISR 中真正清除设备的中断条件,然后再允许下一轮中断。

Edge-triggered IRQ:防止新的事件被遗漏

Edge IRQ 表示的不是一个持续状态,而是某个瞬间发生过一次事件。例如 IRQ 信号出现一次 rising edge。这个 Edge 很快就会消失,因此中断控制器必须把这个瞬时事件锁存为 pending,否则等 CPU 开始处理时,物理上的 Edge 早已经不存在了。

Edge IRQ 面临的主要问题也因此变成:CPU 正在处理第一个 Edge 时,如果第二个 Edge 又来了,怎么保证第二个事件不会丢失?

典型 Edge IRQ 的处理过程与 Level IRQ不同。

  1. 确认已经记录的 Edge:软件首先执行 ack,清除中断控制器已经锁存的当前 pending event。

这么做的目的不是清除设备的持续状态,而是告诉中断控制器:第一个 Edge 我已经收到了,可以准备记录后面的 Edge

  1. 尽量不要长期 mask IRQ:对于能够在 handler 执行期间继续锁存新 Edge 的中断控制器,通常希望尽早完成 ack,而不是像 Level IRQ 那样一直把 IRQ mask 到 ISR 结束。

Linux 的通用 Edge IRQ flow 也是先 ack 当前 Edge,再执行 handler,从而允许硬件继续记录之后发生的新 Edge。

  1. 执行设备 ISR:驱动根据设备状态处理第一个 Edge 对应的事件。如果设备内部还有额外的 interrupt status latch,同样需要清除相应的设备状态。
  2. 处理期间的新 Edge:如果 ISR 执行期间再次发生 Edge,中断控制器会重新设置 pending。这样当前 handler 结束后,软件仍然能够发现还有新的事件等待处理,而不会因为第一个 handler 执行时间较长而直接遗漏第二个 Edge
  3. 完成当前中断:对于需要 EOI/complete 的中断控制器,最后还需要执行相应操作,结束当前 interrupt transaction。

PLIC Gateway 在前一个 interrupt request 收到 complete 以前,不会向 PLIC Core提交同一 source 的下一轮请求。对于这段时间发生的新 Edge,Gateway 可以选择忽略,也可以由硬件额外计数保存,具体取决于 Gateway 实现。PLIC 规范并没有要求所有实现都必须保存无限多个 Edge。

因此对于 Edge IRQ,complete 的时机可能比 Level IRQ更加重要:越晚重新 arm Gateway,期间发生的新 Edge 越可能依赖硬件额外的 latch/counter 能力。

这也是为什么某些实际 PLIC 实现会针对 Edge IRQ采用不同处理方式。当前 Linux PLIC 驱动针对部分支持 Edge 的实现,甚至直接把 plic_irq_eoi() 作为 irq_ack 使用,也就是在 handler 前就完成当前 PLIC request,使 Gateway 尽早能够接受后续 Edge。

Level IRQ 重点解决“同一个持续状态不要反复打断 CPU”,因此强调 mask 和 clear source。

Edge IRQ 重点解决“处理旧事件期间不要漏掉新事件”,因此强调尽早 ack、重新允许硬件记录新的 Edge。