跳转到内容

Trap 处理与时钟中断机制

本节将介绍中断和异常处理机制(Trap),以及如何利用时钟中断实现任务的抢占式调度。

RISC-V 架构定义了三种主要的特权级(由高到低):

特权级名称说明
M-modeMachine机器态。处理最高权限的操作,如 SBI(OpenSBI)运行在此模式。本实验不涉及 M-mode 编程。
S-modeSupervisor监管者态。操作系统内核(rCore)运行在此模式,拥有较高的硬件控制权。
U-modeUser用户态。应用程序运行在此模式,权限受限。

当 U-mode 的应用程序执行 ecall 指令发起系统调用、触发非法指令异常,或者 CPU 接收到硬件中断(如时钟中断)时,控制权会转移到 S-mode 的操作系统内核。这一过程称为 Trap。Trap 机制是操作系统接管 CPU 控制权、处理突发事件的关键。


Trap 的处理过程涉及硬件机制与软件逻辑的紧密配合,主要分为以下三个阶段:

当 Trap 发生时,CPU 自动跳转到 stvec 寄存器指向的地址,即汇编函数 __alltraps(位于 trap.S)。 在此阶段,内核会:

  • 将 32 个通用寄存器以及必要的 CSR 状态保存到内核栈上,形成 TrapContext
  • 切换栈指针 sp 到内核栈。
  • 调用 Rust 编写的 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,并跳转回应用程序继续执行。
  │  (发生 Trap)

__alltraps (Assembly)
  │  保存 TrapContext

trap_handler (Rust)
  │  分发处理:Syscall / Interrupt / Exception

__restore (Assembly)
  │  恢复 TrapContext
  │  sret

(应用程序继续执行)

为了让 CPU 知道 Trap 发生时跳转到哪里,我们需要在内核初始化时设置 stvec 寄存器:

pub fn init() {
    extern "C" { fn __alltraps(); }
    unsafe {
        // 将 __alltraps 的地址写入 stvec,模式设为 Direct
        stvec::write(__alltraps as usize, TrapMode::Direct);
    }
}

TrapMode::Direct 表示所有 Trap 都会跳转到同一个入口地址 __alltraps


TrapContext 结构体用于保存 Trap 发生时的寄存器状态:

#[repr(C)]
pub struct TrapContext {
    pub x: [usize; 32],   // x0–x31 通用寄存器
    pub sstatus: Sstatus, // S-mode 状态寄存器 CSR
    pub sepc: usize,      // Trap 发生时的程序计数器 (PC)
}

__alltraps 会将这 34 个字(共 272 字节)压入内核栈。

其中有些关键 CSR 状态,sstatus 寄存器中包含两个重要的状态位:

位域名称含义
SPPSupervisor Previous Privilege记录 Trap 发生前的特权级(0=U-mode, 1=S-mode)。sret 指令根据此位决定返回后的特权级。
SPIESupervisor Previous Interrupt Enable记录 Trap 发生前的中断使能状态。sret 指令返回时会恢复此状态。

在创建新任务时(TrapContext::app_init_context),我们将 SPP 设置为 0(指向 U-mode),将 SPIE 设置为 1(开启中断),以确保任务启动后处于用户态且可以响应中断。

具体的内容可以查看 RISC-V 的设计规范。

RISC-V 架构包含一个名为 mtime 的 64 位硬件计数器,它以固定频率单调递增。我们可以通过读取 time CSR 来获取当前计数值:

pub fn get_time() -> usize {
    time::read()
}

mtime 的递增频率取决于硬件平台(或模拟器)的时钟频率 CLOCK_FREQ。在本实验环境(QEMU)中,该频率通常为 12.5 MHz,即每秒增加 12,500,000 次。

我们可以通过设置比较寄存器来定义软件层面的“时间片”长度。

通过 SBI 调用,我们可以设定一个未来的时间点 mtimecmp。当 mtime 计数器的值增加到大于或等于 mtimecmp 时,硬件会自动触发时钟中断。

pub fn set_next_trigger() {
    // 设置下一次触发时间 = 当前时间 + 一个时间片的滴答数
    set_timer(get_time() + CLOCK_FREQ / TICKS_PER_SEC);
}

为了实现系统时钟,我们将 TICKS_PER_SEC 设定为 100。意味着 1 秒钟将被切分为 100 个时间片,每个时间片约为 10 ms(对应 125,000 个时钟周期)。

默认情况下,中断可能是被屏蔽的。我们需要手动开启 S-mode 的时钟中断使能位,以便 CPU 能够响应时钟中断信号。

pub fn enable_timer_interrupt() {
    unsafe {
        sie::set_stimer(); // 设置 SIE 寄存器的 STIE 位,开启 S-mode 处理时钟中断的大门
    }
}

因此,在 main.rs 的初始化阶段,我们需要按正确的顺序执行这些操作:

trap::init();                   // 1. 设置 stvec,将 Trap 入口指向 __alltraps
mm::init();                     // 2. 初始化堆内存管理
app_loader::load_apps();        // 3. 加载用户程序到内存
trap::enable_timer_interrupt(); // 4. 开启内核的时钟中断响应
timer::set_next_trigger();      // 5. 设置第一次时钟中断的触发时间
task::run_first_task();         // 6. 启动第一个任务,进入用户态

无论是由于时钟中断(抢占式),还是应用程序主动调用 sys_yield(协作式),任务切换最终都在内核中汇聚为同一个入口及其处理逻辑:

// 路径一:时钟中断触发 preemptive scheduling
Trap::Interrupt(Interrupt::SupervisorTimer) => {
    set_next_trigger();                  // 立即设置下一次中断,保证时间片连续
    suspend_current_and_run_next();      // 挂起当前任务并调度下一个
}

// 路径二:系统调用触发 cooperative scheduling
pub fn sys_yield() -> isize {
    suspend_current_and_run_next();      // 挂起当前任务并调度下一个
    0
}

这里体现了 机制与策略分离(Separation of Mechanism and Policy)的设计思想:

// task/mod.rs
pub fn suspend_current_and_run_next() {
    mark_current_suspended();   // 策略:将当前任务状态修改为 Ready,放入就绪队列
    run_next_task();            // 机制:从就绪队列中选择下一个任务,执行上下文切换 (__switch)
}