0. 资料汇总
-
RISC-V
- RISC-V ELF psABI: Processor-specific application binary interface document.
- RISC-V Supervisor Binary Interface: Spec for SBI.
- RISC-V C API: RISC-V-specific predefined macros, function attributes and language extensions.
- RISC-V Assembly Programmer’s Manual: Document for pseudoinstructions and assembly directives.
- RISC-V Specifications:
- RISC-V ACLINT specification: ACLINT (Advanced Core Local Interruptor) specification defines a set of memory mapped devices which provide inter-processor interrupt and timer functionality for each HART of a multi-HART (or multi-processor) RISC-V platform.
- RISC-V Assembly Programmer’s Manual: Provide guidance to assembly programmers targeting the standard RISC-V assembly language.
-
rCore
- rCore 第三章相关内容的实现记录在 Github Tag: [ch3]
- rCore source code of labs for spring 2023: rCore-Tutorial-Guide-2023S Source Code
- rCore Concise Manual: rCore-Tutorial-Guide-2023S
- rCore Detail Book: rCore-Tutorial-Book-v3
隔了这么就才整到第三章, 毕竟最近杂活儿有点多, 另外第二章的编程题也是安排很不合理, 把后面的内容提前扔给读者结果第三章实验部分要推倒重来太浪费时间了。 更新这章的 OS 框架的时候变动的地方有些多, 尤其是
Makefile
部分以及build.rs
部分和 rCore-Tutorial-Guide-2023S 中进行了同步以及微调。
1. Multi-program OS
Multi-program OS Details, rCore
要满足 任务切换 的要求, 所以程序需要预先被加载到一个固定的内存地址, 而不是所有程序共用一个入口地址。 rCore 添加了 user/build.py
读取 os/src/link_app.S
并对每个程序的地址空间进行更改以适应前述要求。
与 Trap 不同, Mult-program OS 中的任务切换发生在内核态 (S Mode) 且不涉及特权级切换, 通过 __switch
函数实现两个不同的 Task Context
的切换。
为什么
__switch
需要通过汇编的完成主要功能?
由于任务切换不涉及特权级的改变, 因而需要保存的上下文就比较少, 仅需要保存ra
入口地址,sp
栈指针, 以及 Callee 负责寄存器s0~s11
即可。 高级程序语言会在函数中自己使用一些通用的寄存器, 可能会发生意料之外的控制流, 另外__switch
函数仅作为 label 存在不会被 Rust/C 编译器处理, 这些都是使用汇编完成__switch
主体功能的原因。
2. Cooperative OS
Cooperative OS Details, rCore
Multi-program OS 仅仅介绍了任务切换中的 换栈 需求, 任务切换还需要指示任务的状态, 维护这一信息。 这里我仅记录初始化流程中的几个自己困惑的细节。
// os/src/task/mod.rs
for (i, task) in tasks.iter_mut().enumerate() {
task.task_cx = TaskContext::goto_restore(init_app_cx(i));
task.task_status = TaskStatus::Ready;
}
// os/src/task/context.rs
pub fn goto_restore(kstack_ptr: usize) -> Self {
extern "C" {
fn __restore();
}
Self {
ra: __restore as usize,
sp: kstack_ptr,
s: [0; 12],
}
}
TASK_MANAGER
通过 init_app_cx
函数初始化了每个 task 的内核栈, 而 goto_restore
的设计就非常巧妙, 这里并没有进入 trap.S
的 __restore
标签处, 而是将任务栈的 ra
初始化为 __restore
地址, 这样当 __switch
完成后 ret
就能直接进入trap.S
的 __restore
恢复到用户态继续执行任务。 相应的其中的 sp
指针被初始化为内核栈指针。
丢失的
mv sp, a0
可以顺着控制流梳理一遍, 第一次用到
__switch
是初始化时运行run_first_task
函数。__switch
将unused
中的垃圾数据保存在了程序栈后切换到了第一个 task (此后就是内核栈和用户栈之间的切换了), 通过ra
指向的__restore
进行上下文恢复, 对于 task 而并不需要再次更新sp
, 该值已经在__switch
中被恢复成该任务对应的内核指针了。另一个就是进入 trap 之后,
__alltrap
最后进入trap_handler
函数, 该函数最后会在返回参数的a0
中保存之前存入的TrapContext
, 而在此之前有这么两句指令说明了sp
就是这个TrapContext
。 因而无需再进行重复保存。# set input argument of trap_handler(cx: &mut TrapContext) mv a0, sp call trap_handler
3. Timesharing OS
Timesharing OS Details, rCore
4. 课后练习
这章的编程题还是有必要提前做一下的, 汲取了上一章的教训这次先看了 lab 部分, 发现 lab 有个要求是记录当前的正在运行的 task 的运行总时长, 这部分时间既包括内核态与用户态两部分的运行时间, 正好编程题包括了。 这部分可以选择性的完成几个基础练习, 难的部分再好好研究一下。
4.1 编程题
-
扩展内核, 能够显示操作系统切换任务的过程。
只用在
os/src/task/mod.rs
的run_next_task
以及mark_current_exited
,mark_current_suspend
加入打印就行, 另外在这里我对任务切换的流程进行了优化, 仅在当前current != next
时才进行Task Context
交换以及打印相关的信息, 这样能减少内核开销。// os/src/task/mod.rs fn run_next_task(&self) { if let Some(next) = self.find_next_task() { let mut inner = self.inner.exclusive_access(); let current = inner.current_task; ... // ch3-pro1 if current != next { println!("[kernel] task switch from {} to {}", current, next); unsafe { __switch(current_task_cx_ptr, next_task_cx_ptr); } } } ... }
除此之外
struct TaskManagerInner
中我添加了alive_task_num
用以记录仍在Ready
以及Running
的 task 的数量, 仅在alive_task_num > 1
的时候才在mark_current_suspend
中添加打印以保持输出面板整洁以及系统开销最小。具体实现可以参考 commit#4f1dd08 以及 commit#208d863
-
扩展内核, 能够统计每个应用执行后的完成时间: 用户态完成时间和内核态完成时间。
lab 相关的重点,
sys_task_info
大部分信息其实都需要 task module 的支持。 另外, 每个 task 都是独立的, 因而需要在struct TaskControlBlock
中增加记录 task 运行时间的变量。#[derive(Copy, Clone)] pub struct TaskControlBlock { pub task_status: TaskStatus, pub task_cx: TaskContext, pub user_time: usize, pub kernel_time: usize, }
之后比较关键的是如何记录这个时间, 这个我在之前 chapter2 实现了类似的函数, 基本思路就是利用 riscv 的 mtime 寄存器, 需要有个特定的变量存储 mtime 寄存器的值并在每次进入该 task 时计算与现在的 mtime 的插值获取时间信息的变更。
lab 中要求时间单位为 ms, 可以使用
timer.rs
的get_time_ms
函数。pub struct TaskManagerInner { /// task list tasks: [TaskControlBlock; MAX_APP_NUM], /// id of current `Running` task current_task: usize, /// the number of tasks that have not exit alive_task_num: usize, /// record time point checkpoint: usize, }
- 内核态时间
- 在
run_first_task
以及mark_current_exited
,mark_current_suspend
中更新信息, 另外需要在 task 退出时打印耗时。 - 用户态时间
- 用户态和内核态的分界处就是 trap, 因而在
trap_handler
的起始位置和末尾位置可分别作为 user time 的开始以及 user time 的结束。
具体实现可以参考课后参考答案部分 以及 commit#5904c2f, 我把
user_time_start
和user_time_end
的位置搞反了, 后续所有的版本都要更改。 -
编写浮点应用程序A, 并扩展内核, 支持面向浮点应用的正常切换与抢占。
这块没怎么搞懂, 看起来要加很多东西。
-
编写应用程序或扩展内核, 能够统计任务切换的大致开销。
这里的参考答案是有问题的,
__switch
之后会跳转到__restore
恢复到用户态,后面那句只有下一次用户态 trap 后才会执行。 虽然不对我还是测试了一下, 发现 context switch 要花费几百毫秒, 这肯定是不可能的。 可行的办法是更改goto_restore
函数, 将每个__switch
调用的返回地址更改为__pre_restore
并在这里插入一个更新TaskControlBlock
中的switch_time
的值, 这样才能统计不同的 task 的 switch context 开销。-
首先需要在
os/src/task/task.rs
的struct TaskControlBlock
中插入统计每个 task 任务切换上下文开销的变量。// os/src/task/task/rs #[derive(Copy, Clone)] pub struct TaskControlBlock { pub task_status: TaskStatus, pub task_cx: TaskContext, pub syscall_times: [u32; MAX_SYSCALL_NUM], pub switch_time: usize, pub user_time: usize, pub kernel_time: usize, }
-
更新
trap.S
并在尾部插入如下代码, 相应的需要更改goto_restore
中ra
地址为__pre_restore
。__pre_restore: mv a0, sp call switch_cost mv sp, a0 j __restore
-
context switch 的开销统计的相关函数如下。
// os/src/task/mod.rs pub static mut SWITCH_TASK_START: usize = 0; pub unsafe fn __switch(current_task_cx_ptr: *mut TaskContext, next_task_cx_ptr: *const TaskContext) { SWITCH_TASK_START = get_time_us(); switch::__switch(current_task_cx_ptr, next_task_cx_ptr); // 记录除了第一次运行外的 switch cost crate::task::update_switch_cost(get_time_us() - SWITCH_TASK_START); } pub fn update_switch_cost(cost: usize) { let mut inner = TASK_MANAGER.inner.exclusive_access(); let current = inner.current_task; inner.tasks[current].switch_time += cost; } // os/src/trap/mod.rs #[no_mangle] pub unsafe extern "C" fn switch_cost (cx: &mut TrapContext) -> &mut TrapContext { crate::task::update_switch_cost(get_time_us() - SWITCH_TASK_START); cx }
具体的实现和细节可以参考 commit#4e9425, 但当时做的时候遗漏了
__switch
计时函数中的crate::task::update_switch_cost(get_time_us() - SWITCH_TASK_START);
导致内核只能记载程序 switch to not running 即程序开始第一次运行前的开销。 具体可以参考 Mars 在 rCore 相关章节的留言。 -
-
扩展内核,支持在内核态响应中断。
-
扩展内核,支持在内核运行的任务(简称内核任务),并支持内核任务的抢占式切换。
4.2 实验练习
获取任务信息
做完编程题的第二个就跑来做 lab 了, 事情比我想得要复杂一些。 这里需要声明一下 rCore-Tutorial-Guide 2023 实验指导书对应的 test repo 和当前的 rCore-Tutorial-Book-v3 3.6.0-alpha.1 文档 是有差异的, 如果你和我一样是按照 tutorial 一步步搭起来的 OS 请注意以下这些细节。
Guide 2023S 中提供的接口函数是如下形式, 在具体实现的时候由于 TaskInfo
中存在一个数组结构, 需要添加 #[derive(Clone, Copy)]
, 如果添加了 #[repr(C)]
修饰, 后续在 user 目录下的 TaskInfo
需要保持声明的一致性, 否则 os 部分的 sys_task_info
将无法正确赋值。
// os/src/syscall/process.rs
fn sys_task_info(ti: *mut TaskInfo) -> isize
struct TaskInfo {
status: TaskStatus,
syscall_times: [u32; MAX_SYSCALL_NUM],
time: usize
}
我最开始设想在 TaskInfo
外在包裹一个 TaskInfoWrapper
, 并将其声明为如下 case1 形式, 但问题也随之而来, 程序运行会出现 PageFault 错误。 而 case2 这样试图将 TaskInfo
作为可变量用 UPSafeCell
保护, 之后再通过 lazy_static!
宏声明一个 [TaskInfoWrapper;MAX_APP_NUM]
全局数组变量则会遇到 Copy Trait 未实现的问题。
// case1
struct TaskInfoWrapper {
inner: UPSafeCell<[TaskInfo; MAX_APP_NUM]>,
}
// case2
struct TaskInfoWrapper {
inner: UPSafeCell<TaskInfo>,
}
因而我想现阶段我没办法找到一种合适的办法构造一个全局数组变量去存储每个 task info, 那不如利用现有的资源, 每个 task 都维护了一个 TaskControlBlock
变量, 此前的编程题作业在这里面添加了 kernel_time
以及 user_time
, 不如再增加一个 syscall_times
数组变量(与所给测例一致)。
// os/src/task/task.rs
#[derive(Copy, Clone)]
pub struct TaskControlBlock {
pub task_status: TaskStatus,
pub task_cx: TaskContext,
pub syscall_times: [u32; MAX_SYSCALL_NUM],
pub user_time: usize,
pub kernel_time: usize,
}
剩下的事情就比较简单了, 给 sys_task_info
提供一个可以获取当前 task 的 TaskCOntrolBlock
的函数, 以及一个可以在 trap_handler
的 syscall 之前调用的增加 syscall_times
值的函数就能基本满足题干的要求, 我是这么实现的。
// os/src/task/mod.rs
pub fn get_current_task_block() -> TaskControlBlock {
let inner = TASK_MANAGER.inner.exclusive_access();
let current = inner.current_task;
inner.tasks[current].clone()
}
pub fn update_task_syscall_times(syscall_id: usize) {
let mut inner = TASK_MANAGER.inner.exclusive_access();
let current = inner.current_task;
inner.tasks[current].syscall_times[syscall_id] += 1;
}
// os/src/syscall/process.rs
pub fn sys_task_info(ti: *mut TaskInfo) -> isize {
let task_block = get_current_task_block();
unsafe {
*ti = TaskInfo {
status: task_block.task_status,
syscall_times: task_block.syscall_times,
time: task_block.kernel_time + task_block.user_time,
};
}
0
}
具体实现可以参考 commit#d5f5555, 但该 commit 中我还对之前的
sys_write
的 checker 函数进行了更新。
需要注意 ch3_taskinfo.rs
需要单独运行测试, 否则 get_time
函数获取的几个时间点的差值会受到其他程序影响而产生很大偏差。 另外测例中的 println!
是调用了 flush 函数的, 因而在 rCore 2023S 提供的测试环境中确实会有两次 write 系统调用, 但是按照 tutorial 搭的 OS 现阶段仅有一次 write 系统调用, 若这个测试没通过可以自行修改一下。