RISCV 中断

Table of Contents

trap 陷阱 可以分为 异常中断 。在 RISC v 下,中断有三种来源:

你可能见过 NMI,但是这是一种中断类型而非中断来源:

你可能接触过 Linux 中的软中断,即 softirq

但是请注意 software interrupt 与 softirq 是完完全全不一样的

接下来将全面介绍 RISC v 下的中断发送与处理、软件中断、用户态中断和特权级转换,并结合 xv6 内核、rcore、Linux 内核等实现进行介绍

中断有关的寄存器

  • M-mode 寄存器:

mstatusmtvecmedelegmidelegmipmiemepcmcausemtval

  • S-mode 寄存器:

sstatusstvecsipsiesepcscausestvalsatp

在后文中,可能会有 xstatus , xtvec 等的写法,其中 x 表示特权级 m 或者 s 或者 u(u 仅仅在实现了用户态中断的 CPU 上存在)

M-mode

mcause

1653979-20230712210012313-359133103.png

RISC-V 定义 mcause 的高位和低位分别表示不同的信息:

  • 最高位 (Interrupt bit)
    • 0:表示 异常 (Exception)
    • 1:表示 中断 (Interrupt)
  • 其余位 (Exception Code / Interrupt Code)
    • 当最高位是 0:剩余位表示 异常原因
    • 当最高位是 1:剩余位表示 中断类型
Table 1: 常见异常代码(当 Interrupt=0)
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)
Table 2: 常见中断代码(当 Interrupt=1)
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

1653979-20230712210012932-1184025042.png

MIESIE全局中断使能位 ,当 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 模式中,使用 mretsret 指令返回陷阱。执行 xret 指令时
    • 将 xIE 设置为 xPIE;将 xPIE 设置为 1
    • 假设 xPP 值为 y,则将特权模式更改为 y
    • 将 xPP 设置为 U(如果不支持用户模式,则为 M)
      • 如果 xPP≠M,则 xRET 还会设置 MPRV=0

mtvec

1653979-20230712210013404-19738400.png

mtvec 记录的是 异常处理函数的起始地址

  • BASE 字段中的值必须始终对齐于 4 字节边界
  • MODE 设置可能会对 BASE 字段中的值施加额外的对齐约束
    • 如果 MODE 为 0,那么所有的异常处理都有同一个入口地址
    • 否则的话异常处理的入口地址是 BASE+4*CAUSE

      cause 记录在 xcause 中
      

1653979-20230712210013684-694188924.png

medeleg 与 mideleg

默认情况下,各个特权级的陷阱都是被捕捉到了 M-mode

可以通过代码实现将 trap 转发到其它特权级进行处理

为了提高转发的性能在 CPU 级别做了改进并提供了 medelegmideleg 两个寄存器:

  • 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

mipmie 是分别用于保存 pending interruptpending interrupt enable bits

每个中断都有中断号 i(定义在 mcause 表中)

每个中断号如果被 pending 了,那么对应的第 i 位就会被置为 1

因为 RISC v spec 定义了 16 个标准的中断,因此低 16bit 是用于标准用途,其它位则自定义

如下图所示是低 16bit 的 mip 与 mie 寄存器

1653979-20230712210014053-1431446270.png

只需要知道 mcause 中的中断源即可

例如 SSIP 就是 supervisor software interrupt pending

SSIE 就是 supervisor software interrupt enable
Table 3: mie 寄存器(Machine Interrupt Enable Register)
位位置 名称 含义
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
Table 4: mip 寄存器(Machine Interrupt Pending Register)
位位置 名称 含义
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 有重叠的位) → 触发中断

也就是说:

  1. 必须 全局中断使能位 mstatus.MIE=1
  2. 必须 mie 里对应的中断源被允许
  3. 必须 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 faultpage fault 时,mtval 记录的是 引发这些问题的虚拟地址

S-mode

sstatus

1653979-20230712210014505-699545048.png

与中断相关的字段是 SIESPIESPP

  • 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 是一个 设备中断 并且 sstatusSIE bit 为 0,那么不再执行下述过程
  • 开始执行下面过程:
    1. 通过 置零 SIE 禁用中断

      SIE = 0
      
    2. 将 pc 拷贝到 sepc

      sepc = pc 
      
    3. 保存当前的特权级到 sstatus 的 SPP 字段

      sstaus.SPP = U
      
    4. 将 scause 设置成 trap 的原因

      scause = trap.reason 
      
    5. 设置当前特权级为 supervisor
    6. 拷贝 stvec(中断服务程序的首地址)到 pc

      pc = stvec 
      
    7. 开始执行中断服务程序

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 时,要 手动

  1. 清除 sstatus 的 SPP 字段,将其置为零
  2. 将 sstatus 的 SPIE 字段置为 1,启用用户中断
  3. 设置 sepc 为用户进程的 PC 值

    可能疑惑在 U 转换到 S 时不是已经将用户进程的保存在了 sepc 了吗?
    
    因为在 S-mode 也会发生中断呀,那么 sepc 就会被用来保存发生中断位置时的 PC 了
    
  4. 如果启用了页表,就需要还原用户进程的页表,即将用户进程的页表地址写入 satp
  5. 之后恢复上下文
  6. 最后 sret 指令,硬件会自动完成以下操作:
    • 从 sepc 寄存器中取出要恢复的下一条指令地址,将其复制到程序计数器 pc 中,以恢复现场
    • 从 sstatus 寄存器中取出用户模式的相关状态,包括中断使能位、虚拟存储模式等,以恢复用户模式的状态
    • 将当前特权模式设置为用户模式,即取消特权模式,回到用户模式

S 与 M 间的切换

S 切换到 M

切换到 M 与从 U 切换到 M 类似,都是从低特权级到高特权级的切换

在 S 运行的代码,也可以通过 ecall 指令陷入到 M 中:

  1. S-mode 的代码执行一个指令触发了异常或陷阱

    例如调用(ECALL)指令
    
  2. 处理器将当前的 S-mode 上下文的状态保存下来,包括 程序计数器 PCS-mode 特权级别 和其他相关寄存器,保存在 当前特权级别堆栈中的 S-MODE 陷阱帧 (trap frame,其实就是一个页面)中
  3. 处理器通过将 mstatus 寄存器中的 MPP 字段设置为 0b11(表示先前的模式是 S 模式), 将特权级别设置为 M-mode
  4. 处理器将程序计数器设置为在 M-mode 中的陷阱处理程序例程的地址
  5. 处理器还在 mstatus 寄存器中设置 M-mode 中断使能位 (MIE) 为 0,以在陷阱处理程序中禁用中断

系统调用的实现

系统调用是利用异常机制实现的。在 mcause 中看到有 Environment call from U-mode 和 Environment call from S-mode 两个异常类型

那么如何触发这两个异常呢?

分别在 U-mode 和 S-mode 执行 ecall 指令就能触发这两个异常了

执行 ecall 后,CPU 进行如下操作:

  1. 记录异常原因
    • 把异常类型写入 mcause/scause 寄存器(在用户态发起时通常是 scause = 8,表示 "Environment call from U-mode")
  2. 保存异常现场
    • 把触发 ecall 的 PC 保存到 sepc
    • 修改 sstatus,进入 S-mode
  3. 跳转到异常处理入口
    • stvec 寄存器保存了陷入入口地址,CPU跳转到该地址执行内核 trap handler

内核 trap handler(位于 S-mode)做的事情是:

  1. 检查 scause,确认是 ecall from U-mode
  2. 读取用户寄存器里的系统调用号和参数(从 trap frame 里取出 a7, a0–a5)
  3. 根据系统调用号,跳转到相应的内核服务例程
  4. 把返回值写回 a0
  5. 调用 sret 指令恢复到 sepc,返回用户态继续执行

地址空间布局