The Adventures of OS
使用Rust的RISC-V操作系统
在Patreon上支持我! 操作系统博客 RSS订阅 Github EECS网站
这是用Rust编写RISC-V操作系统系列教程中的第8章。
目录 → 第7章 → (第8章) → 第9章
启动一个进程
2020年3月10日: 仅Patreon
2020年3月16日: 公开
视频和参考资料
我在我的大学里教过操作系统,所以我将在此处链接我在该课程中关于进程的笔记。
https://www.youtube.com/watch?v=eB3dkJ2tBK8
上面的笔记是对进程作为一个概念的一般概述,我们在这里构建的操作系统可能会不太一样,大部分是因为它是用 Rust 编写的——在这里插入笑话。
概述
启动一个进程是我们一直在等待的,操作系统的工作本质上是支持正在运行的进程,在这篇文章中,我们将从操作系统的角度以及 CPU 的角度来看一个进程。
我们在上一章中查看了进程内存,但其中一些已被修改,以便我们拥有一个常驻内存空间(在堆上)。 此外,我将向您展示如何从内核模式进入用户模式,现在我们已经删除了主管 (supervisor) 模式,但是当回顾系统调用以支持进程时,我们会修复它。
进程结构体
进程结构大致相同,但在CPU方面,我们只关心TrapFrame结构。
#[repr(C)]#[derive(Clone, Copy)]pub struct TrapFrame {pub regs: [usize; 32], // 0 - 255pub fregs: [usize; 32], // 256 - 511pub satp: usize, // 512 - 519pub trap_stack: *mut u8, // 520pub hartid: usize, // 528}
我们不会使用所有这些字段,而现在我们只关心寄存器上下文(pub regs)。 当我们捕获一个陷入时,我们会将当前在 CPU 上执行的进程存储到 regs 陷入帧中,因此我们在处理陷入时保留该过程并冻结它。
csrr a0, mepccsrr a1, mtvalcsrr a2, mcausecsrr a3, mhartidcsrr a4, mstatuscsrr a5, mscratchla t0, KERNEL_STACK_ENDld sp, 0(t0)call m_trap
在陷入中,在我们保存了上下文之后,我们开始向 Rust 陷入处理程序 m_trap 提供信息,这些参数必须与 Rust 中的顺序匹配。 最后,请注意我们将 KERNEL_STACK_END 放入堆栈指针。 当我们保存它们时,实际上没有任何寄存器发生变化(除了 a0-a5、50 和现在的 sp),但是当我们跳转到 Rust 时,我们需要一个内核栈。
调度
我添加了一个非常简单的调度程序,它只是轮换进程列表,然后检查最前面的进程。 目前还没有办法改变进程状态,但是每当我们找到一个正在运行的进程时,我们就会抓取它的数据,然后将它放在 CPU 上。
pub fn schedule() -> (usize, usize, usize) {unsafe {if let Some(mut pl) = PROCESS_LIST.take() {pl.rotate_left(1);let mut frame_addr: usize = 0;let mut mepc: usize = 0;let mut satp: usize = 0;let mut pid: usize = 0;if let Some(prc) = pl.front() {match prc.get_state() {ProcessState::Running => {frame_addr =prc.get_frame_address();mepc = prc.get_program_counter();satp = prc.get_table_address() >> 12;pid = prc.get_pid() as usize;},ProcessState::Sleeping => {},_ => {},}}println!("Scheduling {}", pid);PROCESS_LIST.replace(pl);if frame_addr != 0 {// MODE 8 是 39 位虚拟地址 MMU// 我使用 PID 作为地址空间标识符,// 希望在我们切换进程时帮助(不?)刷新 TLB。if satp != 0 {return (frame_addr, mepc, (8 << 60) | (pid << 44) | satp);}else {return (frame_addr, mepc, 0);}}}}(0, 0, 0)}
这不是一个好的调度程序,但它可以满足我们的需要。 在这种情况下,调度程序返回的只是运行进程所需的信息,每当我们执行上下文切换时,我们都会询问调度程序并获得一个新进程,有可能得到完全相同的过程。
您会注意到,如果我们没有找到进程,我们会返回 (0, 0, 0),这实际上是此操作系统的错误状态,我们将需要至少一个进程(init)。 在这里,我们将 yield,但现在,它只是循环通过系统调用将消息打印到屏幕上。
// 我们最终会将这个函数移出这里,// 但它的工作只是在进程列表中占据一个位置。fn init_process() {// 在我们有系统调用之前,我们不能在这里做很多事情,// 因为我们是在用户空间中运行的。let mut i: usize = 0;loop {i += 1;if i > 70_000_000 {unsafe {make_syscall(1);}i = 0;}}}
切换到用户
.global switch_to_userswitch_to_user:# a0 - 帧地址# a1 - 程序计数器# a2 - SATP 寄存器csrw mscratch, a0# 1 << 7 is MPIE# 由于用户模式为 00,# 我们不需要在 MPP 中设置任何内容(bits 12:11)li t0, 1 << 7 | 1 << 5csrw mstatus, t0csrw mepc, a1csrw satp, a2li t1, 0xaaacsrw mie, t1la t2, m_trap_vectorcsrw mtvec, t2# 这个 fence 强制 MMU 刷新 TLB。# 然而,由于我们使用 PID 作为地址空间标识符,# 我们可能只在创建进程时才需要它。# 现在,这确保了正确性,但它不是最有效的。sfence.vma# A0 是上下文帧,所以我们需要重新加载它# 并 mret 以便我们可以开始运行程序。mv t6, a0.set i, 1.rept 31load_gp %i, t6.set i, i+1.endrmret
当我们调用这个函数时,我们不能期望重新获得控制权,那是因为我们加载了我们想要运行的下一个进程(通过它的陷入帧上下文),然后我们在执行 mret 指令时通过 mepc 跳转到该代码。
整合起来
那么,这如何结合在一起呢? 好吧,我们将来某个时候会发出一个上下文切换计时器。 当我们遇到这个陷入时,我们调用调度程序来获取一个新进程,然后我们切换到该进程,从而重新启动 CPU 并退出陷入。
7 => unsafe {// 这是上下文切换计时器。// 我们通常会在这里调用调度程序来选择另一个进程来运行。// 机器定时器 Machine timer// println!("CTX");let (frame, mepc, satp) = schedule();let mtimecmp = 0x0200_4000 as *mut u64;let mtime = 0x0200_bff8 as *const u64;// QEMU 给出的频率是 10_000_000 Hz,// 因此这会将下一个中断设置为从现在开始一秒后触发。// 这对于正常操作来说太慢了,但它能让我们看到屏幕后发生的事情。mtimecmp.write_volatile(mtime.read_volatile() + 10_000_000);unsafe {switch_to_user(frame, mepc, satp);}},
我们再次缩短了 m_trap 函数,但是请看一下陷入处理程序,我们每次都重置内核栈,这对于单个 hart 系统来说很好,但是当我们进行多处理时我们必须更新它。
结论
启动一个进程并不是什么大不了的事,但是它要求我们暂时放下我们曾经想的编程方式。 我们正在调用一个函数(switch_to_user),这将使 Rust 不再起作用,但它仍然可以工作?! 为什么,好吧,我们正在使用 CPU 来改变我们想要去的地方,Rust 是不明智的。
现在,我们的操作系统处理中断和调度进程,当我们运行时,我们应该看到以下内容!
每当我们执行上下文切换计时器时,我们都会看到“调度 1”,现在是每秒 1 个,这对于普通的操作系统来说太慢了,但它给了我们足够的时间来看看发生了什么。 然后,进程本身 init_process 在 70,000,000 次迭代后进行系统调用,然后将“Test syscall”打印到屏幕上。
我们知道我们的进程调度程序正在运行,并且我们知道我们的进程本身正在 CPU 上执行。 因此,我们成功了!
