需要实现什么
批处理操作系统,能够自动加载并运行了所有的用户程序(将多个程序打包到一起输入计算机;当一个程序运行结束后,计算机会自动执行下一个程序)。
用户态到内核态的切换。
拥有什么
能够实现sbi调用,实现了能够在终端输出的print宏,在qemu-system-riscv64模拟 RISC-V 64 计算机的运行的程序。
思路
为用户态提供系统调用接口:
用户库使用ecall实现系统调用 -> os内核态捕获trap并执行系统调用
批量加载用户程序并运行:
用户程序打包成可执行文件并链接入口地址 -> os启动时加载所有用户程序 -> 将程序加载到固定地址并执行 -> 执行完成调用exit后运行下一个程序 -> 全部执行完成后关闭os
本章代码树
重要更改:batch.rs、trap、user\src\syscall.rs
── os
│ ├── Cargo.toml
│ ├── Makefile (修改:构建内核之前先构建应用)
│ ├── build.rs (新增:生成 link_app.S 将应用作为一个数据段链接到内核)
│ └── src
│ ├── batch.rs(新增:实现了一个简单的批处理系统)
│ ├── console.rs
│ ├── entry.asm
│ ├── lang_items.rs
│ ├── link_app.S(构建产物,由 os/build.rs 输出)
│ ├── linker.ld
│ ├── logging.rs
│ ├── main.rs(修改:主函数中需要初始化 Trap 处理并加载和执行应用)
│ ├── sbi.rs
│ ├── sync(新增:包装了RefCell,暂时不用关心)
│ │ ├── mod.rs
│ │ └── up.rs
│ ├── syscall(新增:系统调用子模块 syscall)
│ │ ├── fs.rs(包含文件 I/O 相关的 syscall)
│ │ ├── mod.rs(提供 syscall 方法根据 syscall ID 进行分发处理)
│ │ └── process.rs(包含任务处理相关的 syscall)
│ └── trap(新增:Trap 相关子模块 trap)
│ ├── context.rs(包含 Trap 上下文 TrapContext)
│ ├── mod.rs(包含 Trap 处理入口 trap_handler)
│ └── trap.S(包含 Trap 上下文保存与恢复的汇编代码)
└── user(新增:应用测例保存在 user 目录下)
├── Cargo.toml
├── Makefile
└── src
├── bin(基于用户库 user_lib 开发的应用,每个应用放在一个源文件中)
│ ├── ...
├── console.rs
├── lang_items.rs
├── lib.rs(用户库 user_lib)
├── linker.ld(应用的链接脚本)
└── syscall.rs(包含 syscall 方法生成实际用于系统调用的汇编指令,各个具体的 syscall 都是通过 syscall 来实现的)
实现过程
为用户态提供系统调用接口
用户库实现syscall系统调用
与前一章的sbi调用实现基本一样,使用ecall指令从用户态进入内核态。
// user\src\syscall.rs
pub fn syscall(id: usize, args: [usize; 3]) -> isize {
let mut ret: isize;
unsafe {
core::arch::asm!(
"ecall",
inlateout("x10") args[0] => ret,
in("x11") args[1],
in("x12") args[2],
in("x17") id
);
}
ret
}
捕获ecall指令出发的系统调用
trap
stvec 存储处理器在发生异常或中断时跳转到的异常处理基地址(trap handler的入口地址)。
stvec的模式
Direct Mode (直接模式)
在直接模式下,当异常发生时,处理器会跳转到 stvec 寄存器中设置的地址开始执行异常处理程序。所有的异常和中断都会导致处理器跳转到这个单一的入口点,然后由异常处理程序根据异常原因码来处理不同的情况。
Vectored Mode (向量模式)
在向量模式下,stvec 寄存器中的地址是中断向量表的基地址。当异常发生时,处理器会根据异常类型的不同来计算跳转地址。每种异常类型有一个固定的偏移量,处理器会将这个偏移量加到基地址上,计算得到对应的异常处理程序的地址,并跳转到该地址执行。
Trap 处理的总体流程如下:首先通过 __alltraps 将 Trap 上下文保存在内核栈上,然后跳转到使用 Rust 编写的 trap_handler 函数完成 Trap 分发及处理。当 trap_handler 返回之后,使用 __restore 从保存在内核栈上的 Trap 上下文恢复寄存器。最后通过一条 sret 指令回到应用程序执行。
设置trap处理入口,当发生trap终端时,跳转到trap.S中的__alltraps处理,trap.S调用trap_handler。
// os/src/trap/mod.rs
global_asm!(include_str!("trap.S"));
pub fn init() {
extern "C" {
fn __alltraps(); }
unsafe {
stvec::write