需要实现什么
批处理操作系统,能够自动加载并运行了所有的用户程序(将多个程序打包到一起输入计算机;当一个程序运行结束后,计算机会自动执行下一个程序)。
用户态到内核态的切换。
拥有什么
能够实现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(__alltraps as usize, TrapMode::Direct);
}
}
// trap.S
__alltraps:
...
call trap_handler
...
trap_handler
当用户态发出ecall指令时,触发UserEnvCall trap,os捕获此trap后调用对应的系统调用。
sbi_rt 是如何调用 SBI 服务的
SBI spec 的 Chapter 3 介绍了服务的调用方法:
只需将要调用功能的拓展 ID 和功能 ID 分别放在 a7 和 a6 寄存器中,并按照 RISC-V 调用规范将参数放置在其他寄存器中,随后执行 ecall 指令即可。
这会将控制权转交给 RustSBI 并由 RustSBI 来处理请求,处理完成后会将控制权交还给内核。
返回值会被保存在 a0 和 a1 寄存器中。在本书的第二章中,我们会手动编写汇编代码来实现类似的过程。
更多内容可以参阅 SBI spec 的 Chapter 10。
有个疑问
为什么同样是ecall,sbicall能跳到sbi态,syscall却会触发UserEnvCall跳到系统态。
个人猜想两个ecall触发的异常类型是不一样的,那么ecall触发的异常类型在哪指定。
恳请知道的大佬在评论区告诉我。
// os\src\trap\mod.rs
#[no_mangle]
/// handle an interrupt, exception, or system call from user space
pub fn trap_handler(cx: &mut TrapContext) -> &mut TrapContext {
let scause = scause::read(); // get trap cause
let stval = stval::read(); // get extra value
match scause.cause() {
Trap::Exception(Exception::UserEnvCall) => {
cx.sepc += 4;
cx.x[10] = syscall(cx.x[17], [cx.x[10], cx.x[11], cx.x[12]]) as usize;
}
...
}
cx
}
这里我们首先修改保存在内核栈上的 Trap 上下文里面 sepc,让其增加 4。这是因为我们知道这是一个由 ecall 指令触发的系统调用,在进入 Trap 的时候,硬件会将 sepc 设置为这条 ecall 指令所在的地址(因为它是进入 Trap 之前最后一条执行的指令)。而在 Trap 返回之后,我们希望应用程序控制流从 ecall 的下一条指令 开始执行。因此我们只需修改 Trap 上下文里面的 sepc,让它增加 ecall 指令的码长,也即 4 字节。这样在 __restore 的时候 sepc 在恢复之后就会指向 ecall 的下一条指令,并在 sret 之后从那里开始执行。
用来保存系统调用返回值的 a0 寄存器也会同样发生变化。我们从 Trap 上下文取出作为 syscall ID 的 a7 和系统调用的三个参数 a0~a2 传给 syscall 函数并获取返回值。 syscall 函数是在 syscall 子模块中实现的。 这段代码是处理正常系统调用的控制逻辑。
提供 syscall 方法根据 syscall ID 进行分发处理
处理sys_write和sys_exit调用
// os\src\syscall\mod.rs
/// handle syscall exception with `syscall_id` and other arguments
pub fn syscall(syscall_id: usize, args: [usize; 3]) -> isize {
match syscall_id {
SYSCALL_WRITE => sys_write(args[0], args[1] as *const u8, args[2]),
SYSCALL_EXIT => sys_exit(args[0] as i32),
_ => panic!("Unsupported syscall_id: {}", syscall_id),
}
}
批量加载用户程序并运行
内存布局
我们使用链接脚本 user/src/linker.ld 规定用户程序的内存布局:
将程序的起始物理地址调整为 0x80400000 ,三个应用程序都会被加载到这个物理地址上运行;
将 _start 所在的 .text.entry 放在整个程序的开头 0x80400000; 批处理系统在加载应用后,跳转到 0x80400000,就进入了用户库的 _start 函数;// 为什么会跳到0x80400000?
提供了最终生成可执行文件的 .bss 段的起始和终止地址,方便 clear_bss 函数使用。
分析可执行文件,可以看到Entry已经被设置为0x80400000。
root@0f542aa653cc:/home/2024s-rcore-xizhihina# rust-readobj -h ./user/target/riscv64gc-unknown-none-elf/release/ch2b_hello_world
File: ./user/target/riscv64gc-unknown-none-elf/release/ch2b_hello_world
Format: elf64-littleriscv
Arch: riscv64
AddressSize: 64bit
LoadName: <Not found>
ElfHeader {
...
Entry: 0x80400000
...
}
加载app列表
make之后运行python程序自动生成link_app.S文件。从该文件中获取所有程序加载到APP_MANAGER单例中。
// os\src\batch.rs
lazy_static! {
static ref APP_MANAGER: UPSafeCell<AppManager> = unsafe {
UPSafeCell::new({
extern "C" {
fn _num_app();
}
let num_app_ptr = _num_app as usize as *const usize;
let num_app = num_app_ptr.read_volatile();
let mut app_start: [usize; MAX_APP_NUM + 1] = [0; MAX_APP_NUM + 1];
let app_start_raw: &[usize] =
core::slice::from_raw_parts(num_app_ptr.add(1), num_app + 1);
app_start[..=num_app].copy_from_slice(app_start_raw);
AppManager {
num_app,
current_app: 0,
app_start,
}
})
};
}
# os\src\link_app.S
.align 3
.section .data
.global _num_app
_num_app:
.quad 7
.quad app_0_start
.quad app_1_start
.quad app_2_start
.quad app_3_start
.quad app_4_start
.quad app_5_start
.quad app_6_start
.quad app_6_end
.section .data
.global app_0_start
.global app_0_end
app_0_start:
.incbin "../user/build/bin/ch2b_bad_address.bin"
app_0_end:
...
打印app信息
[kernel] num_app = 7
[kernel] app_0 [0x80209048, 0x8020d0f0)
[kernel] app_1 [0x8020d0f0, 0x80211198)
[kernel] app_2 [0x80211198, 0x80215240)
[kernel] app_3 [0x80215240, 0x802192e8)
[kernel] app_4 [0x802192e8, 0x8021d390)
[kernel] app_5 [0x8021d390, 0x80221438)
[kernel] app_6 [0x80221438, 0x802254e0)
这里的地址应该是应用程序的二进制镜像在物理内存中暂时存放的地址,之后运行的时候才将他们依次加载到0x80400000处。
这些地址是怎么生成的呢?
运行app
// os\src\batch.rs
/// run next app
pub fn run_next_app() -> ! {
let mut app_manager = APP_MANAGER.exclusive_access();//UPSafeCell::exclusive_access()保证线程安全的强引用
let current_app = app_manager.get_current_app();
unsafe {
app_manager.load_app(current_app);
}
app_manager.move_to_next_app();
drop(app_manager);
// before this we have to drop local variables related to resources manually
// and release the resources
extern "C" {
fn __restore(cx_addr: usize);
}
unsafe {
__restore(KERNEL_STACK.push_context(TrapContext::app_init_context(
APP_BASE_ADDRESS,
USER_STACK.get_sp(),
)) as *const _ as usize);
}
panic!("Unreachable in batch::run_current_app!");
}
- 获取当前app
- 加载当前app
将从APP_BASE_ADDRESS开始的一块内存清空,然后找到待加载应用二进制镜像的位置(app_start),保存在app_src中,并将它复制到正确的位置(APP_BASE_ADDRESS)。
// os\src\batch.rs
unsafe fn load_app(&self, app_id: usize) {
if app_id >= self.num_app {
println!("All applications completed!");
use crate::board::QEMUExit;
crate::board::QEMU_EXIT_HANDLE.exit_success();
}
println!("[kernel] Loading app_{}", app_id);
// clear app area
core::slice::from_raw_parts_mut(APP_BASE_ADDRESS as *mut u8, APP_SIZE_LIMIT).fill(0);
let app_src = core::slice::from_raw_parts(
self.app_start[app_id] as *const u8,
self.app_start[app_id + 1] - self.app_start[app_id],
);
let app_dst = core::slice::from_raw_parts_mut(APP_BASE_ADDRESS as *mut u8, app_src.len());
app_dst.copy_from_slice(app_src);
asm!("fence.i");
}
- 运行当前app
调用__restore将特权级转为用户态,保存与恢复Trap 上下文,之后寄存器中的指令为用户程序,CPU将运行当前用户程序。
用户栈内核栈相关、汇编代码详解见rCore-Tutorial-Guide-2024S文档。 - 用户程序调用exit(),运行下一个app
// os\src\syscall\process.rs
/// task exits and submit an exit code
pub fn sys_exit(exit_code: i32) -> ! {
trace!("[kernel] Application exited with code {}", exit_code);
run_next_app()
}
全部执行完成后关闭os
// os\src\batch.rs
if app_id >= self.num_app {
println!("All applications completed!");
use crate::board::QEMUExit;
crate::board::QEMU_EXIT_HANDLE.exit_success();
}
2555





