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,返回用户态继续执行
地址空间布局
启用分页模式下,内核代码的访存地址也会被视为一个虚拟地址并需要经过 MMU 的地址转换,因此也需要为内核对应构造一个地址空间,除了仍然需要允许内核的各数据段能够被正常访问之后,还需要包含所有应用的内核栈以及一个 跳板 Trampoline
值得注意的是,下面是是 rCore 的内核地址空间分布,不同的 OS 设计不同
高 256GB 内核地址空间:
低 256GB 内核地址空间
应用程序高 256GB 地址空间
应用程序低 256GB 地址空间
跳板页
使能了分页机制之后,必须在 trap 过程中同时完成地址空间的切换。具体来说:
- 当 _alltraps 保存 Trap 上下文的时候,必须通过修改 _satp 从应用地址空间切换到内核地址空间,因为 trap handler 只有在内核地址空间中才能访问
- 同理,在 __restore 恢复 Trap 上下文的时候,也必须从内核地址空间切换回应用地址空间,因为应用的代码和数据只能在它自己的地址空间中才能访问,应用是看不到内核地址空间的
为何将应用的 Trap 上下文放到应用地址空间的次高页面而不是内核地址空间中的内核栈中呢?
原因在于,在保存 Trap 上下文到内核栈中之前,必须完成两项工作:
- 先切换到内核地址空间,这就需要将 内核地址空间的页表地址 写入 satp 寄存器
- 还需要保存 应用的内核栈栈顶的位置 ,这样才能以它为基址保存 Trap 上下文
这两步需要用寄存器作为临时周转,然而无法在不破坏任何一个通用寄存器的情况下做到这一点
因为事实上需要用到内核的两条信息:内核地址空间的页表地址,以及应用的内核栈栈顶的位置 RISC-V 却只提供一个 sscratch 寄存器可用来进行周转 所以,不得不将 Trap 上下文保存在应用地址空间的一个虚拟页面中,而不是切换到内核地址空间去保存
Page Fault
当 CPU 无法将虚拟地址转换为物理地址时,CPU 会生成页面错误异常。RISC-V 有三种不同类型的页面错误:
- 加载页面错误(当加载指令无法转换其虚拟地址时)
- 存储页面错误(当存储指令无法转换其虚拟地址时)
- 指令页面错误(当指令的地址不转换时)
Cow(copy on write) Fork 中的基本方案是: 让父子进程在最开始时共享所有物理页面,但将它们映射为只读 因此,当子进程或父进程执行存储指令时,RISC-V CPU 会引发页面错误异常 作为对此异常的响应,内核会复制包含错误地址的页面 它将一个副本映射到子进程的地址空间中,并将另一个副本映射到父进程的地址空间中 在更新页表之后,内核在导致错误的指令处恢复出错进程 因为内核已经更新了相关的 PTE 以允许写入,所以出错指令现在将正常执行
scause 寄存器中的值指示页面错误的类型,而 stval 寄存器中包含无法转换的地址
如何设置 stvec 的
stvec 寄存器保存的是中断服务程序的首地址 在 U 模式下,stvec 必须指向的是 uservec,在 S 模式下,stvec 必须指向的是 kernelvec 这样做的原因是需要在 uservec 切换页表,那么 xv6 是如何设置 stvec 的呢?
uservec 例程中除了执行保存上下文、切换页表等操作之外,还会在 usertrap 中将 stvec 指向 kernelvec
当需要返回 usertrap 时,usertrap 会调用 usertrapret,usertrapret 会重新设置 stvec 的值使其指向 uservec,之后跳转到 userret,恢复上下文和切换页表。
第一次的 stvec 是如何设置的
在 main 中:
- cpu0 调用了 userinit() 创建了第一个用户进程,并在 scheduler 中会切换到该进程
- 该进程的上下文中的 ra(返回地址)被设置成了 forkret()
- 当 scheduler 执行 swtch 函数时,会将进程上下文中的 ra 写入到 ra 寄存器中
- 这样当要从 swtch() 中返回时,就会返回到了 forkret()
- 在 forkret() 中会直接调用 usertrapret 以实现 stvec 的设置和页表的切换
与中断有关的硬件单元
在 RISC v 中,与中断有关的硬件单元主要有 ACLINT 、 CLINT 、 PLIC 、 CLIC
- CLINT 的全称是 Core Local Interrupt
- ACLINT 的全称是 Advanced Core Local Interrupt,
- CLIC 的全称是 Core-Local Interrupt Controller
- PLIC 的全称 Platform-Level Interrupt Controller
尽管 CLIC 与 PLIC 名称相似,但是 CLIC 其实是为取代 CLINT 而设计的,而ACLINT 是为了取代 SiFive CLINT 而设计的 本质上讲,ACLINT 相比于 CLINT 的优势就在于进行了模块化设计,将定时器和 IPI 功能分开了,同时能够支持 NUMA 系统 但是 ACLINT 和 CLINT 都还是 RISC-V basic local Interrupts 的范畴
PLIC 和 CLIC 的区别在于,前者负责的是整体的外部中断,CLIC 负责的是每个 HART 的本地中断
PLIC
它把 全局外设中断 按优先级分发到各个 hart 的特权上下文(M/S 模式)的控制器,提供 优先级 、 屏蔽 、 仲裁 和 索取/完成 (claim/complete)流程
PLC 的组成:
- 中断源(source):来自 外设 或 MSI 的全局中断,编号从 1 开始
- ID=0 表示“无中断” 同优先级下用 更小的 ID 抢占 单个源在 PLIC 内 同时最多挂起 1 个请求
目标/上下文(target/context):通常是“某个 hart 的某个特权级”
是否给 S 模式提供目标由实现与委派决定
- 网关(gateway):把外设的电平/边沿信号或 MSI 转成 PLIC 能理解的请求,并控制同一中断源的流量
寄存器与内存映射
PLIC 规定了统一的 操作参数块,但 基地址 由平台决定:
- Priority:每个中断源一个 32-bit 优先级寄存器;0=永不触发;最大优先级级数由实现给出
- Pending(IP):挂起位数组,表示哪些源在等服务
- Enable(IE):对每个 上下文 的允许位数组;一个源可被多个上下文使能
- Threshold:每个上下文一个阈值寄存器;仅当“最高优先级挂起源的优先级 > 阈值”才会给该上下文发通知
- Claim/Complete:同一个地址既可读(claim)也可写(complete),按上下文 4 KB 对齐+4 偏移布置。
规范给出从 priority → pending → enable → threshold/claim 的 标准偏移布局
工作流程与时序要点
- 通知:当某上下文存在“已使能且优先级高于阈值”的挂起源时,PLIC 在该上下文的 xEIP(meip/seip) 上置位,通知 hart
- PLIC 天然做“多播”:所有被使能且满足条件的上下文都会收到通知,谁先 claim 到就算谁
- 索取(claim):读取 claim 寄存器得到“该上下文可见的最高优先级挂起源 ID”(无则返回 0),并原子地清除此源的挂起位。之后更低优先级的挂起源才会“浮现”
- claim 不受阈值影响,允许用于“轮询”
- 完成(complete):中断服务完毕后,把 刚得到的 ID 写回同一寄存器,PLIC 会把“完成消息”转交对应 网关,随后该源才允许进入下一次请求
- 写一个未使能/无效的 ID 会被静默忽略
电平 vs. 边沿(在网关侧)
- 电平触发:
- 第一次置位会形成一次请求
- 在收到 完成 之前即使电平一直保持也不会再转发第二个请求
- 若完成时电平仍为高,立即转发新的请求(等价“再挂起一次”)
- 边沿触发/MSI:
- 第一次边沿形成请求
- 在“请求→完成”间,网关可选择忽略掉后续边沿或做计数
- 只有完成后才把下一次请求送入 PLIC
时钟中断
定时器中断 是由一个独立的计时器电路发出的信号,表示预定的时间间隔已经结束。计时器子系统将中断当前正在执行的代码
- 定时器中断可以由操作系统处理,用于实现时间片多线程
但是对于 MTIME 和 MTIMECMP 的读写只能由 M-mode 的代码实现
因此内核需要调用 SBI 的服务
时钟中断相关的寄存器
mtime 需要以固定的频率递增,并在发生溢出时回绕
- 当 mtime 大于或等于 mtimecmp 时,由核内中断控制器 (CLINT, Core-Local Interrupt Controller) 产生 timer 中断
- 中断的使能由 mie 寄存器中的 MTIE 和 STIE 位控制,mip 中的 MPIE 和 SPIE 则指示了 timer 中断是否处于 pending
- 读取 mtimecmp 结果为低 32 位, mtimecmp 的高 32 位需要 mtimecmph 得到
由于 mtimecmp 只能在 M 模式下访问,对于 S/HS 模式下的内核和 VU/VS 模式下的虚拟机需要通过 SBI 才能访问,会造成较大的中断延迟和性能开销
为了解决这一问题,RISC-V 新增了 Sstc 拓展支持
Sstc 扩展为 HS 模式和 VS 模式分别新增了 stimecmp 和 vstimecmp 寄存器,当 time>=stimecmp 或者 time+htimedelta>=vstimecmp 会产生 timer 中断,不再需要通过 SBI 陷入到其它模式
时钟中断的基本处理过程
下图所示是时钟中断的基本过程(xv6 的处理过程):
回顾一下有关 timer 的寄存器
首先要明确的是,timer 的寄存器在 timer 设备里,不在 CPU 中,是通过 MMIO 的方式 映射 到内存中的:
- mtime 寄存器是一个同步计数器。它从处理器上电开始运行,并以 tick 单位提供当前的实时时间
- mtimecmp 寄存器用于存储定时器中断应该发生的时间间隔,mtimecmp 的值与 mtime 寄存器进行比较
- 当 mtime 值变得大于 mtimecmp 时,就会产生一个定时器中断
- mtime 和 mtimecmp 寄存器都是 64 位内存映射寄存器,因此可以直接按照内存读写的方式修改这两个寄存器的值
xv6 实现
xv6 对于时钟中断的处理方式是这样的:
- 在 M-mode 设置好时钟中断的处理函数
- 当发生时钟中断:
- 由 M-mode 的代码读写 mtime 和 mtimecmp
- 激活 sip:SSIP 以软件中断的形式通知内核
- 内核在收到软件中断之后会递增 ticks 变量,并调用 wakeup 函数唤醒沉睡的进程。内核本身也会收到时钟中断:
- 此时内核会判断当前运行的是不是进程号为 0 的进程,如果不是就会调用 yield() 函数使当前进程放弃 CPU 并调度下一个进程
- 如果使进程号为 0 的进程,那就不做处理
timer_init
// core local interruptor (CLINT), which contains the timer. #define CLINT 0x2000000L #define CLINT_MTIMECMP(hartid) (CLINT + 0x4000 + 8*(hartid)) #define CLINT_MTIME (CLINT + 0xBFF8) // cycles since boot. void timerinit() { // each CPU has a separate source of timer interrupts. int id = r_mhartid(); // ask the CLINT for a timer interrupt. int interval = 1000000; // cycles; about 1/10th second in qemu. // 我已经提过,mtimecmp 是映射到了物理地址中的,因此可以直接按照内存读写的方式 // 修改寄存器的值 // MTIME 寄存器映射到了 0x2000_BFF8 // 一块CPU有一个MTIME,所有的hart都共用这一个 MTIME // MTIMECMP 的内存基地址是 0x2000000L // 每个寄存器占 8个字节,每个hart都有一个MTIMECMP寄存器 // 因此呢,第id个(从0开始计数)的hart对应的 MTIMECMP 的寄存器的物理地址就是 // 0x2000000L + 8 * id // 因此呢就容易理解下面的操作了,实际上就是根据 MTIME 初始化 MTIMECMP *(uint64*)CLINT_MTIMECMP(id) = *(uint64*)CLINT_MTIME + interval; // prepare information in scratch[] for timervec. // scratch[0..2] : space for timervec to save registers. // scratch[3] : address of CLINT MTIMECMP register. // scratch[4] : desired interval (in cycles) between timer interrupts. uint64 *scratch = &timer_scratch[id][0]; scratch[3] = CLINT_MTIMECMP(id);//记录当前hart对应的 MTIMECMP 寄存器映射到的物理地址 scratch[4] = interval; w_mscratch((uint64)scratch);//将数组指针写入mscratch // set the machine-mode trap handler. w_mtvec((uint64)timervec); // enable machine-mode interrupts. w_mstatus(r_mstatus() | MSTATUS_MIE); // enable machine-mode timer interrupts. w_mie(r_mie() | MIE_MTIE); }
时钟中断处理函数
将 mscratch 与 a0 寄存器交换了值,此时 a0 保存的值就是个数组指针(这一点在前面的 timer_init 中已经分析了)
timervec: # start.c has set up the memory that mscratch points to: # scratch[0,8,16] : register save area. # scratch[24] : address of CLINT's MTIMECMP register. # scratch[32] : desired interval between interrupts. csrrw a0, mscratch, a0 # 保存寄存器的上下文 sd a1, 0(a0) sd a2, 8(a0) sd a3, 16(a0) # schedule the next timer interrupt # by adding interval to mtimecmp. # 实际上执行的就是 MTIMECMP = MTIME + INTERVAL ld a1, 24(a0) # CLINT_MTIMECMP(hart) ld a2, 32(a0) # interval ld a3, 0(a1) add a3, a3, a2 sd a3, 0(a1) # arrange for a supervisor software interrupt # after this handler returns. # 通过supervisor software 中断的方式通知 S-mode 的内核处理时钟中断 # 实际上呢,时钟中断已经在M-mode被处理掉了 # 之所以还要通知S-mode的内核是因为内核的进程调度器依赖于对时间的掌握 # S-mode只是根据时钟变化去做进程调度器相关的处理 li a1, 2 csrw sip, a1 # 恢复上下文 ld a3, 16(a0) ld a2, 8(a0) ld a1, 0(a0) csrrw a0, mscratch, a0 mret