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 与中断相关的寄存器和指令
在次只列举部分实验常用的寄存器与指令,更多信息请查阅官方文档。
寄存器
线程相关寄存器
- sscratch : 在用户态,sscratch 保存内核栈的地址;在内核态,sscratch 的值为 0。
发生中断时,硬件自动填写的寄存器
- sepc : 即 Exception Program Counter,用来记录触发中断的指令的地址。
- scause: 记录中断是否是硬件中断,以及具体的中断原因。
- stval :scause 不足以存下中断所有的必须信息。例如缺页异常,就会将 stval 设置成需要访问但是不在内存中的地址,以便于操作系统将这个地址所在的页面加载进来。
指导硬件处理中断的寄存器
- stvec: 设置内核态中断处理流程的入口地址。存储了一个基址 BASE 和模式 MODE:
* MODE 为 0 表示 Direct 模式,即遇到中断便跳转至 BASE 进行执行。
* MODE 为 1 表示 Vectored 模式,此时 BASE 应当指向一个向量,存有不同处理流程的地址,遇到中断会跳转至 BASE + 4 * cause 进行处理流程。 - sstatus: 具有许多状态位,控制全局中断使能等。
- sie :即 Supervisor Interrupt Enable,用来控制具体类型中断的使能,例如其中的 STIE 控制时钟中断使能。
- sip :即 Supervisor Interrupt Pending,和 sie 相对应,记录每种中断是否被触发。仅当 sie 和 sip 的对应位都为 1 时,意味着开中断且已发生中断,这时中断最终触发。
1.3 与中断相关的指令
进入和退出中断
- ecall:触发中断,进入更高一层的中断处理流程之中。用户态进行系统调用进入内核态中断处理流程,内核态进行 SBI 调用进入机器态中断处理流程,使用的都是这条指令。
- sret:从内核态返回用户态,同时将 pc 的值设置为 sepc。(如果需要返回到 sepc 后一条指令,就需要在 sret 之前修改 sepc 的值)
- ebreak:触发一个断点。
- mret 从机器态返回内核态,同时将 pc 的值设置为 mepc。
操作 CSR(读写重置)
- csrrw dst, csr, src(CSR Read Write):同时读写的原子操作,将指定 CSR 的值写入 dst,同时将 src 的值写入 CSR。
- csrr dst, csr(CSR Read):仅读取一个 CSR 寄存器。
- csrw csr, src(CSR Write):仅写入一个 CSR 寄存器。
- csrc(i) csr, rs1(CSR Clear):将 CSR 寄存器中指定的位清零,csrc 使用通用寄存器作为 mask,csrci 则使用立即数。
- 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中。