zl程序教程

您现在的位置是:首页 >  IT要闻

当前栏目

rust写操作系统 rCore tutorial 学习笔记:实验指导一 中断

2023-03-07 09:08:36 时间

这是 os summer of code 2020 项目每日记录的一部分: 每日记录github地址(包含根据实验指导实现的每个阶段的代码):https://github.com/yunwei37/os-summer-of-code-daily

这里参考的是rCore tutorial的第三版:https://github.com/rcore-os/rCore-Tutorial

lab1 学习报告

  • RISC-V 中有关中断处理的寄存器和相关流程
  • 如何保存上下文,使得中断处理流程前后,原本正在执行的程序感知不到发生了中断
  • 处理最简单的断点中断和时钟中断

中断的概念:

  • 异常:执行指令时产生的,通常无法预料的错误。
  • 陷阱:一系列强行导致中断的指令
  • 硬件中断:由 CPU 之外的硬件产生的异步中断

RISC-V 与中断相关的寄存器和指令

Machine mode:

  • 是 RISC-V 中的最高权限模式,一些底层操作的指令只能由机器态进行使用。
  • 是所有标准 RISC-V 处理器都必须实现的模式。
  • 默认所有中断实际上是交给机器态处理的,但是为了实现更多功能,机器态会将某些中断交由内核态处理。这些异常也正是我们编写操作系统所需要实现的。

Supervisor mode:

  • 通常为操作系统使用,可以访问一些 supervisor 级别的寄存器,通过这些寄存器对中断和虚拟内存映射进行管理。
  • Unix 系统中,大部分的中断都是内核态的系统调用。机器态可以通过异常委托机制(machine interrupt delegation)将一部分中断设置为不经过机器态,直接由内核态处理

关注:内核态可以使用的一些特权指令和寄存器

与中断相关的寄存器

自动填写:

  • sepc:用来记录触发中断的指令的地址。
  • scause:记录中断是否是硬件中断,以及具体的中断原因。
  • stval:需要访问但是不在内存中的地址,包含信息

指导硬件处理中断:

  • stvec:设置内核态中断处理流程的入口地址。分为基址 BASE 和模式 MODE:
    • MODE 为 0 表示 Direct 模式:跳转至 BASE 进行执行
    • MODE 为 1 表示 Vectored 模式:BASE + 4 * cause
  • sstatus:控制全局中断使能等
  • sie:控制具体类型中断的使能
  • sip:记录每种中断是否被触发
  • sscratch:sscratch 在用户态保存内核栈的地址

与中断相关的指令

  • ecall:触发中断
  • sret:从内核态返回用户态,同时将 pc 的值设置为 sepc
  • ebreak:触发一个断点
  • mret:从机器态返回内核态
  • csrrw dst, csr, src:CSR Read Write
  • csrr dst, csr:CSR Write
  • csrc(i) csr, rs1:CSR Clear
  • csrs(i) csr, rs1:CSR Set

程序运行状态

在处理中断之前,必须要保存所有可能被修改的寄存器,并且在处理完成后恢复。我们需要保存所有通用寄存器,sepc、scause 和 stval 这三个会被硬件自动写入的 CSR 寄存器,以及 sstatus。

在 os/src/interrupt/context.rs 中添加:

use riscv::register::{sstatus::Sstatus, scause::Scause};

#[repr(C)]
pub struct Context {
    pub x: [usize; 32],     // 32 个通用寄存器
    pub sstatus: Sstatus,
    pub sepc: usize
}

在 os/Cargo.toml 中添加依赖:

[dependencies]
riscv = { git = "https://github.com/rcore-os/riscv", features = ["inline-asm"] }

状态的保存与恢复

  • 保存:先用栈上的一小段空间来把需要保存的全部通用寄存器和 CSR 寄存器保存在栈上,保存完之后在跳转到 Rust 编写的中断处理函数;
  • 恢复:直接把备份在栈上的内容写回寄存器。由于涉及到了寄存器级别的操作,我们需要用汇编来实现。

新建:

os/src/interrupt.asm

# 宏:将寄存器存到栈上
.macro SAVE reg, offset
    sd  \reg, \offset*8(sp)
.endm

# 宏:将寄存器从栈中取出
.macro LOAD reg, offset
    ld  \reg, \offset*8(sp)
.endm

    .section .text
    .globl __interrupt
# 进入中断
# 保存 Context 并且进入 rust 中的中断处理函数 interrupt::handler::handle_interrupt()
__interrupt:
    # 在栈上开辟 Context 所需的空间
    addi    sp, sp, -34*8
    
    # 保存通用寄存器,除了 x0(固定为 0)
    SAVE    x1, 1
    # 将原来的 sp(sp 又名 x2)写入 2 位置
    addi    x1, sp, 34*8
    SAVE    x1, 2
    # 其他通用寄存器
    SAVE    x3, 3
    SAVE    x4, 4
    SAVE    x5, 5
    SAVE    x6, 6
    SAVE    x7, 7
    SAVE    x8, 8
    SAVE    x9, 9
    SAVE    x10, 10
    SAVE    x11, 11
    SAVE    x12, 12
    SAVE    x13, 13
    SAVE    x14, 14
    SAVE    x15, 15
    SAVE    x16, 16
    SAVE    x17, 17
    SAVE    x18, 18
    SAVE    x19, 19
    SAVE    x20, 20
    SAVE    x21, 21
    SAVE    x22, 22
    SAVE    x23, 23
    SAVE    x24, 24
    SAVE    x25, 25
    SAVE    x26, 26
    SAVE    x27, 27
    SAVE    x28, 28
    SAVE    x29, 29
    SAVE    x30, 30
    SAVE    x31, 31

    # 取出 CSR 并保存
    csrr    s1, sstatus
    csrr    s2, sepc
    SAVE    s1, 32
    SAVE    s2, 33

    # 调用 handle_interrupt,传入参数
    # context: &mut Context
    mv      a0, sp
    # scause: Scause
    csrr    a1, scause
    # stval: usize
    csrr    a2, stval
    jal  handle_interrupt

    .globl __restore
# 离开中断
# 从 Context 中恢复所有寄存器,并跳转至 Context 中 sepc 的位置
__restore:
    # 恢复 CSR
    LOAD    s1, 32
    LOAD    s2, 33
    csrw    sstatus, s1
    csrw    sepc, s2

    # 恢复通用寄存器
    LOAD    x1, 1
    LOAD    x3, 3
    LOAD    x4, 4
    LOAD    x5, 5
    LOAD    x6, 6
    LOAD    x7, 7
    LOAD    x8, 8
    LOAD    x9, 9
    LOAD    x10, 10
    LOAD    x11, 11
    LOAD    x12, 12
    LOAD    x13, 13
    LOAD    x14, 14
    LOAD    x15, 15
    LOAD    x16, 16
    LOAD    x17, 17
    LOAD    x18, 18
    LOAD    x19, 19
    LOAD    x20, 20
    LOAD    x21, 21
    LOAD    x22, 22
    LOAD    x23, 23
    LOAD    x24, 24
    LOAD    x25, 25
    LOAD    x26, 26
    LOAD    x27, 27
    LOAD    x28, 28
    LOAD    x29, 29
    LOAD    x30, 30
    LOAD    x31, 31

    # 恢复 sp(又名 x2)这里最后恢复是为了上面可以正常使用 LOAD 宏
    LOAD    x2, 2
    sret

进入中断处理流程:

新建 os/src/interrupt/handler.rs

use super::context::Context;
use riscv::register::stvec;

global_asm!(include_str!("./interrupt.asm"));

/// 初始化中断处理
///
/// 把中断入口 `__interrupt` 写入 `stvec` 中,并且开启中断使能
pub fn init() {
    unsafe {
        extern "C" {
            /// `interrupt.asm` 中的中断入口
            fn __interrupt();
        }
        // 使用 Direct 模式,将中断入口设置为 `__interrupt`
        stvec::write(__interrupt as usize, stvec::TrapMode::Direct);
    }
}

/// 中断的处理入口
/// 
/// `interrupt.asm` 首先保存寄存器至 Context,其作为参数和 scause 以及 stval 一并传入此函数
/// 具体的中断类型需要根据 scause 来推断,然后分别处理
#[no_mangle]
pub fn handle_interrupt(context: &mut Context, scause: Scause, stval: usize) {
    panic!("Interrupted: {:?}", scause.cause());
}

os/src/interrupt/mod.rs:

//! 中断模块
//! 
//! 

mod handler;
mod context;

/// 初始化中断相关的子模块
/// 
/// - [`handler::init`]
/// - [`timer::init`]
pub fn init() {
    handler::init();
    println!("mod interrupt initialized");
}

编译:cargo build

报错: error[E0412]: cannot find typeScausein this scope

在 handler.rs 中添加:

use riscv::register::scause::Scause;

运行:

PMP0: 0x0000000080000000-0x000000008001ffff (A)
PMP1: 0x0000000000000000-0xffffffffffffffff (A,R,W,X)
Hello rCore-Tutorial!
mod interrupt initialized
panic: 'Interrupted: Exception(Breakpoint)'

时钟中断

RISC-V 中将中断分为三种:

  • 软件中断(Software Interrupt)
  • 时钟中断(Timer Interrupt)
  • 外部中断(External Interrupt)

新建:os/src/interrupt/timer.rs

//! 预约和处理时钟中断

use crate::sbi::set_timer;
use riscv::register::{time, sie, sstatus};

/// 初始化时钟中断
/// 
/// 开启时钟中断使能,并且预约第一次时钟中断
pub fn init() {
    unsafe {
        // 开启 STIE,允许时钟中断
        sie::set_stimer(); 
        // 开启 SIE(不是 sie 寄存器),允许内核态被中断打断
        sstatus::set_sie();
    }
    // 设置下一次时钟中断
    set_next_timeout();
}

设置时钟中断:

os/src/sbi.rs

/// 时钟中断的间隔,单位是 CPU 指令
static INTERVAL: usize = 100000;

/// 设置下一次时钟中断
/// 
/// 获取当前时间,加上中断间隔,通过 SBI 调用预约下一次中断
fn set_next_timeout() {
    set_timer(time::read() + INTERVAL);
}

/// 触发时钟中断计数
pub static mut TICKS: usize = 0;

/// 每一次时钟中断时调用
/// 
/// 设置下一次时钟中断,同时计数 +1
pub fn tick() {
    set_next_timeout();
    unsafe {
        TICKS += 1;
        if TICKS % 100 == 0 {
            println!("{} tick", TICKS);
        }
    }
}

实现时钟中断的处理流程

os/src/interrupt/handler.rs:

/// 中断的处理入口
/// 
/// `interrupt.asm` 首先保存寄存器至 Context,其作为参数和 scause 以及 stval 一并传入此函数
/// 具体的中断类型需要根据 scause 来推断,然后分别处理
#[no_mangle]
pub fn handle_interrupt(context: &mut Context, scause: Scause, stval: usize) {
    match scause.cause() {
        // 断点中断(ebreak)
        Trap::Exception(Exception::Breakpoint) => breakpoint(context),
        // 时钟中断
        Trap::Interrupt(Interrupt::SupervisorTimer) => supervisor_timer(context),
        // 其他情况,终止当前线程
        _ => fault(context, scause, stval),
    }
}

/// 处理 ebreak 断点
/// 
/// 继续执行,其中 `sepc` 增加 2 字节,以跳过当前这条 `ebreak` 指令
fn breakpoint(context: &mut Context) {
    println!("Breakpoint at 0x{:x}", context.sepc);
    context.sepc += 2;
}

/// 处理时钟中断
/// 
/// 目前只会在 [`timer`] 模块中进行计数
fn supervisor_timer(_: &Context) {
    timer::tick();
}

/// 出现未能解决的异常
fn fault(context: &mut Context, scause: Scause, stval: usize) {
    panic!(
        "Unresolved interrupt: {:?}\n{:x?}\nstval: {:x}",
        scause.cause(),
        context,
        stval
    );
}

还需要在头部加上:

use super::timer;
use riscv::register::{
    scause::{Exception, Interrupt, Scause, Trap},
    sie, stvec,
};

最后:

  • 给 Context 结构体加上 Debug trait;
  • 在 os/interrupt/mod.rs 中引入 mod timer 并在 初始化 handler::init() 语句的后面加入 timer::init();
  • 在 main.rs 中间引入 loop{}; 循环

就能跑起来啦!