Trap 处理与时钟中断机制
本节将介绍中断和异常处理机制(Trap),以及如何利用时钟中断实现任务的抢占式调度。
RISC-V 特权级与 Trap
Section titled “RISC-V 特权级与 Trap”RISC-V 架构定义了三种主要的特权级(由高到低):
| 特权级 | 名称 | 说明 |
|---|---|---|
| M-mode | Machine | 机器态。处理最高权限的操作,如 SBI(OpenSBI)运行在此模式。本实验不涉及 M-mode 编程。 |
| S-mode | Supervisor | 监管者态。操作系统内核(rCore)运行在此模式,拥有较高的硬件控制权。 |
| U-mode | User | 用户态。应用程序运行在此模式,权限受限。 |
当 U-mode 的应用程序执行 ecall 指令发起系统调用、触发非法指令异常,或者 CPU 接收到硬件中断(如时钟中断)时,控制权会转移到 S-mode 的操作系统内核。这一过程称为 Trap。Trap 机制是操作系统接管 CPU 控制权、处理突发事件的关键。
Trap 处理流程
Section titled “Trap 处理流程”Trap 的处理过程涉及硬件机制与软件逻辑的紧密配合,主要分为以下三个阶段:
1. 硬件触发与状态保存 __alltraps
Section titled “1. 硬件触发与状态保存 __alltraps”当 Trap 发生时,CPU 自动跳转到 stvec 寄存器指向的地址,即汇编函数 __alltraps(位于 trap.S)。
在此阶段,内核会:
- 将 32 个通用寄存器以及必要的 CSR 状态保存到内核栈上,形成
TrapContext。 - 切换栈指针
sp到内核栈。 - 调用 Rust 编写的
trap_handler函数。
2. 内核分发与处理 trap_handler
Section titled “2. 内核分发与处理 trap_handler”trap_handler(位于 kernel/src/trap/mod.rs) 根据 Trap 的原因(scause 寄存器)进行分发处理:
- 系统调用 (System Call):
Exception::UserEnvCall。调用syscall()分发系统调用,如sys_yield。 - 时钟中断 (Timer Interrupt):
Interrupt::SupervisorTimer。调用set_next_trigger()设置下一次中断,并调用suspend_current_and_run_next()挂起当前任务,实现抢占。 - 异常 (Exception): 如
StoreFault(访存错误)。表示应用程序出现严重错误,内核将调用exit_current_and_run_next()终止该任务。 - 未知 Trap: 对于未处理的 Trap 类型,内核通常会
panic!。
3. 恢复现场与返回用户态 __restore
Section titled “3. 恢复现场与返回用户态 __restore”处理完成后,执行流返回到汇编函数 __restore(位于 trap.S)。
在此阶段,内核会:
- 从内核栈上的
TrapContext恢复所有通用寄存器和 CSR 状态。 - 执行
sret指令。该指令会将 CPU 特权级从 S-mode 切换回 U-mode,并跳转回应用程序继续执行。
stvec 的初始化
Section titled “stvec 的初始化”为了让 CPU 知道 Trap 发生时跳转到哪里,我们需要在内核初始化时设置 stvec 寄存器:
TrapMode::Direct 表示所有 Trap 都会跳转到同一个入口地址 __alltraps。
TrapContext 上下文结构
Section titled “TrapContext 上下文结构”TrapContext 结构体用于保存 Trap 发生时的寄存器状态:
__alltraps 会将这 34 个字(共 272 字节)压入内核栈。
其中有些关键 CSR 状态,sstatus 寄存器中包含两个重要的状态位:
| 位域 | 名称 | 含义 |
|---|---|---|
| SPP | Supervisor Previous Privilege | 记录 Trap 发生前的特权级(0=U-mode, 1=S-mode)。sret 指令根据此位决定返回后的特权级。 |
| SPIE | Supervisor Previous Interrupt Enable | 记录 Trap 发生前的中断使能状态。sret 指令返回时会恢复此状态。 |
在创建新任务时(TrapContext::app_init_context),我们将 SPP 设置为 0(指向 U-mode),将 SPIE 设置为 1(开启中断),以确保任务启动后处于用户态且可以响应中断。
具体的内容可以查看 RISC-V 的设计规范。
RISC-V 时钟中断机制
Section titled “RISC-V 时钟中断机制”时钟计数器 mtime
Section titled “时钟计数器 mtime”RISC-V 架构包含一个名为 mtime 的 64 位硬件计数器,它以固定频率单调递增。我们可以通过读取 time CSR 来获取当前计数值:
mtime 的递增频率取决于硬件平台(或模拟器)的时钟频率 CLOCK_FREQ。在本实验环境(QEMU)中,该频率通常为 12.5 MHz,即每秒增加 12,500,000 次。
我们可以通过设置比较寄存器来定义软件层面的“时间片”长度。
设置定时器触发点
Section titled “设置定时器触发点”通过 SBI 调用,我们可以设定一个未来的时间点 mtimecmp。当 mtime 计数器的值增加到大于或等于 mtimecmp 时,硬件会自动触发时钟中断。
为了实现系统时钟,我们将 TICKS_PER_SEC 设定为 100。意味着 1 秒钟将被切分为 100 个时间片,每个时间片约为 10 ms(对应 125,000 个时钟周期)。
使能时钟中断
Section titled “使能时钟中断”默认情况下,中断可能是被屏蔽的。我们需要手动开启 S-mode 的时钟中断使能位,以便 CPU 能够响应时钟中断信号。
因此,在 main.rs 的初始化阶段,我们需要按正确的顺序执行这些操作:
统一的调度入口
Section titled “统一的调度入口”无论是由于时钟中断(抢占式),还是应用程序主动调用 sys_yield(协作式),任务切换最终都在内核中汇聚为同一个入口及其处理逻辑:
这里体现了 机制与策略分离(Separation of Mechanism and Policy)的设计思想: