RISCV 中断
Table of Contents
trap 陷阱 可以分为 异常 与 中断 。在 RISC v 下,中断有三种来源:
- software interrupt
- timer interrupt(顾名思义,时钟中断)
- external interrupt
你可能见过 NMI,但是这是一种中断类型而非中断来源:
- Non-maskable interrupt,不可屏蔽中断,与之相对的就是可屏蔽中断
- NMI 都是硬件中断,只有在发生严重错误时才会触发这种类型的中断
你可能接触过 Linux 中的软中断,即 softirq 但是请注意 software interrupt 与 softirq 是完完全全不一样的
接下来将全面介绍 RISC v 下的中断发送与处理、软件中断、用户态中断和特权级转换,并结合 xv6 内核、rcore、Linux 内核等实现进行介绍
中断有关的寄存器
- M-mode 寄存器:
mstatus , mtvec , medeleg , mideleg , mip , mie , mepc , mcause , mtval
- S-mode 寄存器:
sstatus , stvec , sip , sie , sepc , scause , stval , satp
在后文中,可能会有 xstatus , xtvec 等的写法,其中 x 表示特权级 m 或者 s 或者 u(u 仅仅在实现了用户态中断的 CPU 上存在)
M-mode
mcause
RISC-V 定义 mcause 的高位和低位分别表示不同的信息:
- 最高位 (Interrupt bit)
- 0:表示 异常 (Exception)
- 1:表示 中断 (Interrupt)
- 其余位 (Exception Code / Interrupt Code)
- 当最高位是 0:剩余位表示 异常原因
- 当最高位是 1:剩余位表示 中断类型
Code | 异常 (Exception) 类型 |
0 | 指令地址不对齐 (Instruction address misaligned) |
1 | 指令访问错误 (Instruction access fault) |
2 | 非法指令 (Illegal instruction) |
3 | 断点 (Breakpoint) |
4 | 加载地址不对齐 (Load address misaligned) |
5 | 加载访问错误 (Load access fault) |
6 | 存储/AMO 地址不对齐 (Store/AMO address misaligned) |
7 | 存储/AMO 访问错误 (Store/AMO access fault) |
8 | 环境调用 U 模式 (Environment call from U-mode) |
9 | 环境调用 S 模式 (Environment call from S-mode) |
11 | 环境调用 M 模式 (Environment call from M-mode) |
12 | 指令页错误 (Instruction page fault) |
13 | 加载页错误 (Load page fault) |
15 | 存储/AMO 页错误 (Store/AMO page fault) |
Code | 中断 (Interrupt) 类型 |
0 | 用户软件中断 (User software interrupt) |
1 | 管理员软件中断 (Supervisor software interrupt) |
3 | 机器软件中断 (Machine software interrupt) |
4 | 用户定时器中断 (User timer interrupt) |
5 | 管理员定时器中断 (Supervisor timer interrupt) |
7 | 机器定时器中断 (Machine timer interrupt) |
8 | 用户外部中断 (User external interrupt) |
9 | 管理员外部中断 (Supervisor external interrupt) |
11 | 机器外部中断 (Machine external interrupt) |
mstatus
MIE 与 SIE 是 全局中断使能位 ,当 xIE 为 1 时,允许在 x 特权级发生中断,否则不允许中断。
- 当 hart 处于 x 特权级时
- 当 xIE 为 0 时,x 特权级的中断被全部禁用, xIE 为1 时被全部启用
- 当 xIE 为 0 时
- 对于任意的 w<x,w 特权级的中断都是处于全局禁用状态
- 对于任意的 y>x,y 特权级的中断默认处于全局启用状态,无论 xIE 是否为 1
- 为支持嵌套陷阱,每个可以响应中断的特权模式 x 都有一个 两级中断使能位 和 特权模式堆栈
- xPIE 保存陷阱之前活动的中断使能位的值
- xPP 保存之前的特权模式
- xPP 字段只能保存 x 及以下特权模式,因此 MPP 为 两位 宽, SPP 为 一位 宽
- 当从特权模式 y 进入特权模式 x 时
- xPIE 设置为 xIE 的值,xIE 设置为 0
- xPP 设置为 y:对于 MPP,可以设置的值有 0b00(用户模式),0b01(S-mode),0b10(reserved),0b11(M-mode)
- 在 M 模式或 S 模式中,使用 mret 或 sret 指令返回陷阱。执行 xret 指令时
- 将 xIE 设置为 xPIE;将 xPIE 设置为 1
- 假设 xPP 值为 y,则将特权模式更改为 y
- 将 xPP 设置为 U(如果不支持用户模式,则为 M)
- 如果 xPP≠M,则 xRET 还会设置 MPRV=0
mtvec
mtvec 记录的是 异常处理函数的起始地址 :
- BASE 字段中的值必须始终对齐于 4 字节边界
- MODE 设置可能会对 BASE 字段中的值施加额外的对齐约束
- 如果 MODE 为 0,那么所有的异常处理都有同一个入口地址
否则的话异常处理的入口地址是 BASE+4*CAUSE
cause 记录在 xcause 中
medeleg 与 mideleg
默认情况下,各个特权级的陷阱都是被捕捉到了 M-mode 可以通过代码实现将 trap 转发到其它特权级进行处理
为了提高转发的性能在 CPU 级别做了改进并提供了 medeleg 和 mideleg 两个寄存器:
- medeleg machine exception delegation 用于指示转发哪些异常到 S-mode
- mideleg machine interrupt delegation 用于指示转发哪些中断到 S-mode
当将陷阱委托给 S 模式时
- scause 寄存器会写入 陷阱原因
- sepc 寄存器会写入 引发陷阱的指令的虚拟地址
- stval 寄存器会写入 特定于异常的数据
- mstatus 的 SPP 字段会写入 发生陷阱时的活动特权级
- mstatus 的 SPIE 字段会写入 发生陷阱时的 SIE 字段的值
- mstatus 的 SIE 字段会被 清除
- mcause、mepc 和 mtval 寄存器以及 mstatus 的 MPP 和 MPIE 字段不会被写入
被委托的中断会导致该中断在委托者所在的特权级 屏蔽
比如说 M-mode 将一些中断委托给了 S-mode,那么 M-mode 就无法捕捉到这些中断了
mip 与 mie
mip 与 mie 是分别用于保存 pending interrupt 和 pending interrupt enable bits
每个中断都有中断号 i(定义在 mcause 表中) 每个中断号如果被 pending 了,那么对应的第 i 位就会被置为 1 因为 RISC v spec 定义了 16 个标准的中断,因此低 16bit 是用于标准用途,其它位则自定义
如下图所示是低 16bit 的 mip 与 mie 寄存器
只需要知道 mcause 中的中断源即可 例如 SSIP 就是 supervisor software interrupt pending SSIE 就是 supervisor software interrupt enable
位位置 | 名称 | 含义 |
0 | USIE | 用户软件中断使能 |
1 | SSIE | 管理员(S 模式)软件中断使能 |
3 | MSIE | 机器(M 模式)软件中断使能 |
4 | UTIE | 用户定时器中断使能 |
5 | STIE | S 模式定时器中断使能 |
7 | MTIE | M 模式定时器中断使能 |
8 | UEIE | 用户外部中断使能 |
9 | SEIE | S 模式外部中断使能 |
11 | MEIE | M 模式外部中断使能 |
如果要允许 “机器定时器中断” ,就需要把 mie.MTIE 位置 1
位位置 | 名称 | 含义 |
0 | USIP | 用户软件中断挂起 |
1 | SSIP | S 模式软件中断挂起 |
3 | MSIP | M 模式软件中断挂起 |
4 | UTIP | 用户定时器中断挂起 |
5 | STIP | S 模式定时器中断挂起 |
7 | MTIP | M 模式定时器中断挂起 |
8 | UEIP | 用户外部中断挂起 |
9 | SEIP | S 模式外部中断挂起 |
11 | MEIP | M 模式外部中断挂起 |
当定时器溢出时,硬件会把 mip.MTIP 置 1,表示有一个 M 模式定时器中断等待处理
mie 和 mip 的关系
- mie: 允许哪些中断
- mip: 哪些中断正在等待
CPU 是否真正响应中断取决于:
if (mstatus.MIE == 1) and (mip & mie 有重叠的位) → 触发中断
也就是说:
- 必须 全局中断使能位 mstatus.MIE=1
- 必须 mie 里对应的中断源被允许
- 必须 mip 里对应的中断源正在挂起
举个例子:机器定时器中断 1. 硬件定时器达到设定值 → mip.MTIP = 1 2. 软件提前打开了 mie.MTIE = 1 3. 且全局 mstatus.MIE = 1 → 4. CPU 立即跳转到 mtvec 指定的中断入口 5. 处理中断时,硬件会自动清 mstatus.MIE=0,防止嵌套 6. 返回时执行 mret,恢复原始 MIE 状态
mpec
当 trap 陷入到 M-mode 时,mepc 会被 CPU 自动写入 引发 trap 的指令的虚拟地址 或者是 被中断的指令的虚拟地址
mtval
当 trap 陷入到 M-mode 时,mtval 会被 置零 或者被写入与 异常相关的信息 来辅助处理 trap
- 当触发 硬件断点 、 地址未对齐 、 access fault 、 page fault 时,mtval 记录的是 引发这些问题的虚拟地址
S-mode
sstatus
与中断相关的字段是 SIE 、 SPIE 、 SPP :
- SPP 位:指示处理器 进入 supervisor 模式之前 的 特权级别
- 当发生陷阱时,如果该陷阱来自用户模式,则 SPP 设置为 0;否则设置为 1
- 当执行 SRET 指令从陷阱处理程序返回时
- 如果 SPP 位为 0,则特权级别设置为用户模式
- 如果 SPP 位为 1,则特权级别设置为 supervisor 模式,然后将 SPP 设置为 0
- SIE 位:在 supervisor 模式下 启用 或 禁用 所有中断
- 当 SIE 为零时,在 supervisor 模式下不会进行中断处理
- 当处理器在用户模式下运行时,忽略 SIE 的值,并启用 supervisor 级别的中断
- 可以使用 sie 寄存器 来禁用单个中断源
- SPIE 位:指示 陷入 supervisor 模式 之前是否 启用 了 supervisor 级别的中断
当执行跳转到 supervisor 模式的陷阱时,将 SPIE 设置为 SIE,并将 SIE 设置为 0
SPIE = SIE; SIE = 0;
当执行 SRET 指令时,将 SIE 设置为 SPIE,然后将 SPIE 设置为 1
SIE = SPIE; SPIE = 1;
其它 s 特权级寄存器
stvec, sip, sie,sepc, scause, stval 与 m-mode 的相应寄存器区别不大 可自行参阅 RISC v 的 spec
satp 比较特殊,在 M-mode 没有对应的寄存器,因为 M-mode 没有分页,satp 记录的是 根页表物理地址的页帧号
在从 U 切换到 S 时,需要切换页表,也即是切换 satp 的根页表物理地址的页帧号
特权级转换
U 与 S 间的切换
在这里只介绍了 U 和 S 之间的切换
其实 S 和 M 之间的切换过程也是一样的,只不过使用到的寄存器不一样了而已 比如说保存 pc 的寄存,S 保存 U 的 pc 值使用的是 sepc,M 保存 S 的 pc 使用的是 mepc 此外,U 切换到 S 时一般需要切换页表,而从 S 切换到 M 时不需要切换页表 因为 M 没有实现分页,也没有 matp 寄存器(页表根地址存储在 satp 寄存器中,所以这里胡诌了个 matp)
U 切换到 S
当执行一个 trap 时,除了 timer interrupt,所有的过程都是相同的,硬件会自动完成下述过程:
- 如果该 trap 是一个 设备中断 并且 sstatus 的 SIE bit 为 0,那么不再执行下述过程
- 开始执行下面过程:
通过 置零 SIE 禁用中断
SIE = 0
将 pc 拷贝到 sepc
sepc = pc
保存当前的特权级到 sstatus 的 SPP 字段
sstaus.SPP = U
将 scause 设置成 trap 的原因
scause = trap.reason
- 设置当前特权级为 supervisor
拷贝 stvec(中断服务程序的首地址)到 pc
pc = stvec
- 开始执行中断服务程序
CPU 不会自动切换到内核的页表,也不会切换到内核栈,也不会保存除了 pc 之外的寄存器的值,内核需要自行完成
对于Linux而言,内核空间与用户态空间是使用的同一套页表,不需要切换页表 内核空间一般位于进程的高虚拟地址空间
如果启用了分页,当陷入到 S 模式时,CPU 没有切换页表(换出进程的页表,换入内核页表),内核需要自行切换页表。其实切换页表的过程也很简单,只需要将内核的页表地址写入 satp 寄存器即可
在执行中断服务例程时还需要首先判断 sstatus 的 SPP 字段是不是 0
- 如果是 0 表示之前是 U 模式,否则表示 S 模式
如果 SPP 是 1 那就出现了严重错误
因为既然是从 U 切换到 S 的过程,怎么可以 SPP 是 S 模式呢? 当然,如果是内核执行时发生了中断 SPP 是 1 那自然是对的 内核执行时发生中断时如果检查 SPP 是 0 那也是严重的错误
S 切换到 U
在从 S 切换到 U 时,要 手动
- 清除 sstatus 的 SPP 字段,将其置为零
- 将 sstatus 的 SPIE 字段置为 1,启用用户中断
设置 sepc 为用户进程的 PC 值
可能疑惑在 U 转换到 S 时不是已经将用户进程的保存在了 sepc 了吗? 因为在 S-mode 也会发生中断呀,那么 sepc 就会被用来保存发生中断位置时的 PC 了
- 如果启用了页表,就需要还原用户进程的页表,即将用户进程的页表地址写入 satp
- 之后恢复上下文
- 最后 sret 指令,硬件会自动完成以下操作:
- 从 sepc 寄存器中取出要恢复的下一条指令地址,将其复制到程序计数器 pc 中,以恢复现场
- 从 sstatus 寄存器中取出用户模式的相关状态,包括中断使能位、虚拟存储模式等,以恢复用户模式的状态
- 将当前特权模式设置为用户模式,即取消特权模式,回到用户模式
S 与 M 间的切换
S 切换到 M
切换到 M 与从 U 切换到 M 类似,都是从低特权级到高特权级的切换
在 S 运行的代码,也可以通过 ecall 指令陷入到 M 中:
S-mode 的代码执行一个指令触发了异常或陷阱
例如调用(ECALL)指令
- 处理器将当前的 S-mode 上下文的状态保存下来,包括 程序计数器 PC 、 S-mode 特权级别 和其他相关寄存器,保存在 当前特权级别堆栈中的 S-MODE 陷阱帧 (trap frame,其实就是一个页面)中
- 处理器通过将 mstatus 寄存器中的 MPP 字段设置为 0b11(表示先前的模式是 S 模式), 将特权级别设置为 M-mode
- 处理器将程序计数器设置为在 M-mode 中的陷阱处理程序例程的地址
- 处理器还在 mstatus 寄存器中设置 M-mode 中断使能位 (MIE) 为 0,以在陷阱处理程序中禁用中断
系统调用的实现
系统调用是利用异常机制实现的。在 mcause 中看到有 Environment call from U-mode 和 Environment call from S-mode 两个异常类型
那么如何触发这两个异常呢? 分别在 U-mode 和 S-mode 执行 ecall 指令就能触发这两个异常了
执行 ecall 后,CPU 进行如下操作:
- 记录异常原因
- 把异常类型写入 mcause/scause 寄存器(在用户态发起时通常是 scause = 8,表示 "Environment call from U-mode")
- 保存异常现场
- 把触发 ecall 的 PC 保存到 sepc
- 修改 sstatus,进入 S-mode
- 跳转到异常处理入口
- stvec 寄存器保存了陷入入口地址,CPU跳转到该地址执行内核 trap handler
内核 trap handler(位于 S-mode)做的事情是:
- 检查 scause,确认是 ecall from U-mode
- 读取用户寄存器里的系统调用号和参数(从 trap frame 里取出 a7, a0–a5)
- 根据系统调用号,跳转到相应的内核服务例程
- 把返回值写回 a0
- 调用 sret 指令恢复到 sepc,返回用户态继续执行