rust 实现 rCore lab1

lab1

本实验目标是实现中断系统。
lab1实验指导书
实验完成后目录结构如下:

Project
│  rust-toolchain
│
└─os
    │  .gitignore
    │  Cargo.lock
    │  Cargo.toml
    │  Makefile
    │
    ├─.cargo
    │      config
    │
    └─src
        │  console.rs
        │  entry.asm
        │  linker.ld
        │  main.rs
        │  panic.rs
        │  sbi.rs
        │
        └─interrupt
                context.rs
                handler.rs
                interrupt.asm
                mod.rs
                timer.rs

一,中断原理简介

1.1 中断简介

中断是操作系统所有功能的基础,其决定了操作系统的模式切换以及各种资源的调度实现。

中断主要分为三种

  • 异常(Exception) : 执行指令时产生的,通常无法预料的错误。例如:访问无效内存地址、执行非法指令(除以零)等。
  • 陷阱(Trap): 陷阱是一系列强行导致中断的指令,例如:系统调用(Syscall)等。
  • 硬件中断(Hardware Interrupt): 硬件中断是由 CPU 之外的硬件产生的异步中断,例如:时钟中断、外设发来数据等。

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

在次只列举部分实验常用的寄存器与指令,更多信息请查阅官方文档。

寄存器

线程相关寄存器

  1. sscratch : 在用户态,sscratch 保存内核栈的地址;在内核态,sscratch 的值为 0。

发生中断时,硬件自动填写的寄存器

  1. sepc : 即 Exception Program Counter,用来记录触发中断的指令的地址。
  2. scause: 记录中断是否是硬件中断,以及具体的中断原因。
  3. stval :scause 不足以存下中断所有的必须信息。例如缺页异常,就会将 stval 设置成需要访问但是不在内存中的地址,以便于操作系统将这个地址所在的页面加载进来。

指导硬件处理中断的寄存器

  1. stvec: 设置内核态中断处理流程的入口地址。存储了一个基址 BASE 和模式 MODE:
    * MODE 为 0 表示 Direct 模式,即遇到中断便跳转至 BASE 进行执行。
    * MODE 为 1 表示 Vectored 模式,此时 BASE 应当指向一个向量,存有不同处理流程的地址,遇到中断会跳转至 BASE + 4 * cause 进行处理流程。
  2. sstatus: 具有许多状态位,控制全局中断使能等。
  3. sie :即 Supervisor Interrupt Enable,用来控制具体类型中断的使能,例如其中的 STIE 控制时钟中断使能。
  4. sip :即 Supervisor Interrupt Pending,和 sie 相对应,记录每种中断是否被触发。仅当 sie 和 sip 的对应位都为 1 时,意味着开中断且已发生中断,这时中断最终触发。

1.3 与中断相关的指令

进入和退出中断

  1. ecall:触发中断,进入更高一层的中断处理流程之中。用户态进行系统调用进入内核态中断处理流程,内核态进行 SBI 调用进入机器态中断处理流程,使用的都是这条指令。
  2. sret:从内核态返回用户态,同时将 pc 的值设置为 sepc。(如果需要返回到 sepc 后一条指令,就需要在 sret 之前修改 sepc 的值)
  3. ebreak:触发一个断点。
  4. mret 从机器态返回内核态,同时将 pc 的值设置为 mepc。

操作 CSR(读写重置)

  1. csrrw dst, csr, src(CSR Read Write):同时读写的原子操作,将指定 CSR 的值写入 dst,同时将 src 的值写入 CSR。
  2. csrr dst, csr(CSR Read):仅读取一个 CSR 寄存器。
  3. csrw csr, src(CSR Write):仅写入一个 CSR 寄存器。
  4. csrc(i) csr, rs1(CSR Clear):将 CSR 寄存器中指定的位清零,csrc 使用通用寄存器作为 mask,csrci 则使用立即数。
  5. csrs(i) csr, rs1(CSR Set):将 CSR 寄存器中指定的位置 1,csrc 使用通用寄存器作为 mask,csrci 则使用立即数。

二,程序运行状态

3.1 上下文设计

在程序运行中,各种寄存器存储着当前程序的运行时信息,包括PC,返回值等,这些信息被统称为程序的上下文。当中断发生时,操作系统必须将当前程序的上下文保存,以便于中断完成后会恢复现场;接着会将中断程序的上下文赋值到寄存器中。
本节的目的在于设计上下文信息。
第 1 步 设计 Context类。在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       //记录触发中断的指令的地址
}

第 2 步 添加依赖
为了使用riscv的寄存器,必须在os/Cargo.toml 中添加依赖,将依赖修改如下:

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

3.2 状态的保存与恢复

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

第 1 步 编写汇编代码实现保存与恢复
建立os/src/interrupt/interrupt.asm文件,编写以下内容:
(本版本中保存运用循环的方式保存和恢复#3-31号通用寄存器,旧版的采用一次列出所有寄存器的方式。)

# 我们将会用一个宏来用循环保存寄存器。这是必要的设置
.altmacro
# 寄存器宽度对应的字节数
.set    REG_SIZE, 8
# Context 的大小
.set    CONTEXT_SIZE, 34

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

.macro SAVE_N n
    SAVE  x\n, \n
.endm


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

.macro LOAD_N n
    LOAD  x\n, \n
.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
    # 保存 x3 至 x31
    .set    n, 3
    .rept   29
        SAVE_N  %n
        .set    n, n + 1
    .endr

    # 取出 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
    # 恢复 x3 至 x31
    .set    n, 3
    .rept   29
        LOAD_N  %n
        .set    n, n + 1
    .endr

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

三,中断处理

第 1 步 开启和处理中断
新建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());
}

第 2 步 模块初始化
基于Rust的语法,新建os/src/interrupt/mod.rs文件添加以下代码初始化interrupt模块:

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

mod handler;
mod context;

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

第 3 步 触发中断
在os/src/main.rs中添加 mod interrupt; 并使用ebreak来触发中断。修改代码如下:

...
mod interrupt;
...

/// Rust 的入口函数
///
/// 在 `_start` 为我们进行了一系列准备之后,这是第一个被调用的 Rust 函数
pub extern "C" fn rust_main() -> ! {
    // 初始化各种模块
    interrupt::init();

    unsafe {
        llvm_asm!("ebreak"::::"volatile");
    };

    unreachable!();
}

四,时钟中断

时钟中断是操作系统能够进行线程调度的基础,操作系统会在每次时钟中断时被唤醒,暂停正在执行的线程,并根据调度算法选择下一个应当运行的线程。本节目标在于实现时钟中断。

第 1 步 开启与设置时钟中断
新建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();
}


/// 时钟中断的间隔,单位是 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);
        }
    }
}

第 2 步 修改sbi
为简化操作系统实现,操作系统可请求(sbi_call调用ecall指令)SBI服务来完成时钟中断的设置。
在os/src/sbi.rs文件添加如下代码。

/// 设置下一次时钟中断的时间
pub fn set_timer(time: usize) {
    sbi_call(SBI_SET_TIMER, time, 0, 0);
}

第 3 步 实现时钟中断的处理流程
修改os/src/interrupt/handler.rs文件中的handle_interrupt()函数。

/// 中断的处理入口
/// 
/// `interrupt.asm` 首先保存寄存器至 Context,其作为参数和 scause 以及 stval 一并传入此函数
/// 具体的中断类型需要根据 scause 来推断,然后分别处理
#[no_mangle]
pub fn handle_interrupt(context: &mut Context, scause: Scause, stval: usize) {
    // 可以通过 Debug 来查看发生了什么中断
    // println!("{:x?}", context.scause.cause());
    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
    );
}

补充内容

1.修改mod.rs
因为加入了timer.rs,使用需要加入相应的初始化操作。修改os/src/interrupt/mod.rs如下。

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

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

2. 修改Context
因为要输出,所以要实现相应的trait。
修改os/src/interrupt/context.rs,内容如下:

use core::fmt;
use core::mem::zeroed;
use riscv::register::{sstatus::Sstatus, scause::Scause};

#[repr(C)]
#[derive(Clone, Copy)]
pub struct Context {
    pub x: [usize; 32],     // 32 个通用寄存器
    pub sstatus: Sstatus,  //状态位,控制全局中断使能
    pub sepc: usize       //记录触发中断的指令的地址
}

impl Default for Context {
    fn default() -> Self {
        unsafe { zeroed() }
    }
}

/// 格式化输出
///
/// # Example
///
/// ```rust
/// println!("{:x?}", Context);   // {:x?} 表示用十六进制打印其中的数值
/// ```
impl fmt::Debug for Context {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        f.debug_struct("Context")
            .field("registers", &self.x)
            .field("sstatus", &self.sstatus)
            .field("sepc", &self.sepc)
            .finish()
    }
}

3.修改 handle
因为调整了包的结构,以及修改了中断处理函数,所以需要修改包含的"头文件"。
os/src/interrupt/handler.rs“头文件”修改如下:

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

4.修改主函数
为了方便测试,需要修改main函数内容,加入无限循环。
os/src/main.rs中main函数修改如下:
#[no_mangle]
pub extern “C” fn rust_main() -> ! {
interrupt::init();

unsafe {
    llvm_asm!("ebreak"::::"volatile");
};
loop{};
unreachable!();

}

至此,lab1实验使用完成。源代码于实验题代码均在github中。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值