需要实现什么
本章的目标是实现分时多任务系统,它能并发地执行多个用户程序,并调度这些程序。为此需要实现
一次性加载所有用户程序,减少任务切换开销;
支持任务切换机制,保存切换前后程序上下文;
支持程序主动放弃处理器,实现 yield 系统调用;
以时间片轮转算法调度用户程序,实现资源的时分复用。
拥有什么
实现了用户程序加载、用户态与内核态间的相互转换、trap处理机制、用户程序运行完成后切换至下一个程序的批处理系统。
思路
区别:
- 程序保存在内存中而非作为数据段链接到内核里。
- 同时管理多个应用程序。
定时触发时钟中断:
通过M特权级获取时间 -> 设置mtimecmp触发中断 -> 捕获中断运行下一个app
应用程序管理:
单例管理所有程序 -> 将程序保存在内存中 -> 实现应用加载、切换、退出 -> 实现yield让步
本章代码树
── os
├── build.rs
├── Cargo.toml
├── Makefile
└── src
├── batch.rs(移除:功能分别拆分到 loader 和 task 两个子模块)
├── config.rs(新增:保存内核的一些配置)
├── console.rs
├── logging.rs
├── sync
├── entry.asm
├── lang_items.rs
├── link_app.S
├── linker.ld
├── loader.rs(新增:将应用加载到内存并进行管理)
├── main.rs(修改:主函数进行了修改)
├── sbi.rs(修改:引入新的 sbi call set_timer)
├── syscall(修改:新增若干 syscall)
│ ├── fs.rs
│ ├── mod.rs
│ └── process.rs
├── task(新增:task 子模块,主要负责任务管理)
│ ├── context.rs(引入 Task 上下文 TaskContext)
│ ├── mod.rs(全局任务管理器和提供给其他模块的接口)
│ ├── switch.rs(将任务切换的汇编代码解释为 Rust 接口 __switch)
│ ├── switch.S(任务切换的汇编代码)
│ └── task.rs(任务控制块 TaskControlBlock 和任务状态 TaskStatus 的定义)
├── timer.rs(新增:计时器相关)
└── trap
├── context.rs
├── mod.rs(修改:时钟中断相应处理)
└── trap.S
实现过程
定时触发时钟中断
通过M特权级获取时间
// os/src/timer.rs
use riscv::register::time;
pub fn get_time() -> usize {
time::read()
}
设置mtimecmp触发中断
RISC-V 要求处理器维护时钟计数器 mtime,还有另外一个 CSR mtimecmp 。 一旦计数器 mtime 的值超过了 mtimecmp,就会触发一次时钟中断。
// os\src\sbi.rs
/// use sbi call to set timer
pub fn set_timer(timer: usize) {
sbi_call(SBI_SET_TIMER, timer, 0, 0);
}
捕获中断运行下一个app
重新设置mtimecmp后运行下一个app。
timer 子模块的 set_next_trigger 函数对 set_timer 进行了封装, 它首先读取当前 mtime 的值,然后计算出 10ms 之内计数器的增量,再将 mtimecmp 设置为二者的和。 这样,10ms 之后一个 S 特权级时钟中断就会被触发。
trap_handler详解见 ch2 。
// os\src\trap\mod.rs
/// trap handler
#[no_mangle]
pub fn trap_handler(cx: &mut TrapContext) -> &mut TrapContext {
...
Trap::Interrupt(Interrupt::SupervisorTimer) => {
set_next_trigger();
suspend_current_and_run_next();
}
...
}
// os\src\timer.rs
/// Set the next timer interrupt
pub fn set_next_trigger() {
set_timer(get_time() + CLOCK_FREQ / TICKS_PER_SEC);
}
应用程序管理
单例统一管理所有应用程序
TaskManager保存任务(进程)数、所有任务信息task、当前任务id。
Task包括任务状态和任务上下文TaskContext。
TaskContext包括ra、sp、s0-11寄存器。
lazy_static! {
/// Global variable: TASK_MANAGER
pub static ref TASK_MANAGER: TaskManager = {
let num_app = get_num_app();
let mut tasks = [TaskControlBlock {
task_cx: TaskContext::zero_init(),
task_status: TaskStatus::UnInit,
syscall_times: [0; MAX_SYSCALL_NUM],
first_time: 0,
}; MAX_APP_NUM];
for (i, task) in tasks.iter_mut().enumerate() {
task.task_cx = TaskContext::goto_restore(init_app_cx(i));
task.task_status = TaskStatus::Ready;
}
TaskManager {
num_app,
inner: unsafe {
UPSafeCell::new(TaskManagerInner {
tasks,
current_task: 0,
})
},
}
};
}
将程序保存在内存中
// os\src\loader.rs
/// Load nth user app at
/// [APP_BASE_ADDRESS + n * APP_SIZE_LIMIT, APP_BASE_ADDRESS + (n+1) * APP_SIZE_LIMIT).
pub fn load_apps() {
extern "C" {
fn _num_app();
}
let num_app_ptr = _num_app as usize as *const usize;
let num_app = get_num_app();
let app_start = unsafe { core::slice::from_raw_parts(num_app_ptr.add(1), num_app + 1) };
// clear i-cache first
unsafe {
asm!("fence.i");
}
// load apps
for i in 0..num_app {
let base_i = get_base_i(i);
// clear region
(base_i..base_i + APP_SIZE_LIMIT)
.for_each(|addr| unsafe { (addr as *mut u8).write_volatile(0) });
// load app from data section to memory
let src = unsafe {
core::slice::from_raw_parts(app_start[i] as *const u8, app_start[i + 1] - app_start[i])
};
let dst = unsafe { core::slice::from_raw_parts_mut(base_i as *mut u8, src.len()) };
dst.copy_from_slice(src);
}
}
/// Get base address of app i.
fn get_base_i(app_id: usize) -> usize {
APP_BASE_ADDRESS + app_id * APP_SIZE_LIMIT
}
实现应用加载、切换、退出
在 __switch(os\src\task\switch.S)
中保存 CPU 的某些寄存器,它们就是 任务上下文 (Task Context)。内核先把 current_task_cx_ptr 中包含的寄存器值逐个保存,再把 next_task_cx_ptr 中包含的寄存器值逐个恢复。 __switch 函数的主要作用是保存当前任务的上下文( Task Context,栈指针和寄存器值),并加载下一个任务的上下文,以便可以无缝地切换到下一个任务的执行。
# os\src\task\switch.S
.altmacro
.macro SAVE_SN n
sd s\n, (\n+2)*8(a0)
.endm
.macro LOAD_SN n
ld s\n, (\n+2)*8(a1)
.endm
.section .text
.globl __switch
__switch:
# __switch(
# current_task_cx_ptr: *mut TaskContext,
# next_task_cx_ptr: *const TaskContext
# )
# save kernel stack of current task
sd sp, 8(a0)
# save ra & s0~s11 of current execution
sd ra, 0(a0)
.set n, 0
.rept 12
SAVE_SN %n
.set n, n + 1
.endr
# restore ra & s0~s11 of next execution
ld ra, 0(a1)
.set n, 0
.rept 12
LOAD_SN %n
.set n, n + 1
.endr
# restore kernel stack of next task
ld sp, 8(a1)
ret
这段代码是一个汇编程序,特定于某种RISC-V或其他类似指令集的架构,用于实现任务上下文切换(task context switch)。它保存了当前执行任务的上下文(如寄存器和栈指针),并加载下一个任务的上下文以恢复其执行。以下是对代码的逐行解释:
# 开始的行是注释,它们解释了代码的功能和目的。
2-9. 定义了两个宏 SAVE_SN 和 LOAD_SN,分别用于保存和加载寄存器(s0 到 s11)的值。这些宏使用了一个名为 n 的变量作为参数,该变量在宏内部被替换为具体的寄存器编号。
SAVE_SN 宏使用 sd(store doubleword,即存储双字)指令将寄存器 s\n(例如 s0、s1 等)的值存储到由 a0 指向的内存地址的偏移位置。
LOAD_SN 宏使用 ld(load doubleword,即加载双字)指令从由 a1 指向的内存地址的偏移位置加载值到寄存器 s\n。
10-11. 声明了一个文本段(.text)和一个全局符号 __switch。
12-35. 定义了 __switch 函数,该函数实现了任务上下文切换。
第18行:保存当前任务的栈指针(sp)到由 a0 指向的内存地址的偏移8的位置。
第20行:保存当前执行的返回地址(ra)到由 a0 指向的内存地址的偏移0的位置。
第22-25行:使用 .rept(repeat)伪指令重复12次,以保存寄存器 s0 到 s11 的值。这里使用了前面定义的 SAVE_SN 宏。
第27行:从由 a1 指向的内存地址的偏移0的位置加载下一个任务的返回地址(ra)。
第29-32行:使用 .rept 伪指令重复12次,以加载寄存器 s0 到 s11 的值。这里使用了前面定义的 LOAD_SN 宏。
第34行:从由 a1 指向的内存地址的偏移8的位置加载下一个任务的栈指针(sp)。
第35行:ret 指令返回,此时将开始执行下一个任务的代码。
总之,这个 __switch 函数的主要作用是保存当前任务的上下文(栈指针和寄存器值),并加载下一个任务的上下文,以便可以无缝地切换到下一个任务的执行。这是操作系统中多任务执行的核心机制之一。
// os\src\task\mod.rs
impl TaskManager {
/// Run the first task in task list.
///
/// Generally, the first task in task list is an idle task (we call it zero process later).
/// But in ch3, we load apps statically, so the first task is a real app.
fn run_first_task(&self) -> ! {
let mut inner = self.inner.exclusive_access();
let task0 = &mut inner.tasks[0];
task0.task_status = TaskStatus::Running;
let next_task_cx_ptr = &task0.task_cx as *const TaskContext;
drop(inner);
let mut _unused = TaskContext::zero_init();
// before this, we should drop local variables that must be dropped manually
unsafe {
__switch(&mut _unused as *mut TaskContext, next_task_cx_ptr);
}
panic!("unreachable in run_first_task!");
}
/// Change the status of current `Running` task into `Ready`.
fn mark_current_suspended(&self) {
let mut inner = self.inner.exclusive_access();
let current = inner.current_task;
inner.tasks[current].task_status = TaskStatus::Ready;
}
/// Change the status of current `Running` task into `Exited`.
fn mark_current_exited(&self) {
let mut inner = self.inner.exclusive_access();
let current = inner.current_task;
inner.tasks[current].task_status = TaskStatus::Exited;
}
/// Find next task to run and return task id.
/// In this case, we only return the first `Ready` task in task list.
fn find_next_task(&self) -> Option<usize> {
let inner = self.inner.exclusive_access();
let current = inner.current_task;
(current + 1..current + self.num_app + 1)
.map(|id| id % self.num_app)
.find(|id| inner.tasks[*id].task_status == TaskStatus::Ready)
}
/// Switch current `Running` task to the task we have found,
/// or there is no `Ready` task and we can exit with all applications completed
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;
inner.tasks[next].task_status = TaskStatus::Running;
inner.current_task = next;
let current_task_cx_ptr = &mut inner.tasks[current].task_cx as *mut TaskContext;
let next_task_cx_ptr = &inner.tasks[next].task_cx as *const TaskContext;
drop(inner);
// before this, we should drop local variables that must be dropped manually
unsafe {
__switch(current_task_cx_ptr, next_task_cx_ptr);
}
// go back to user mode
} else {
panic!("All applications completed!");
}
}
}
加载
/// Run the first task in task list.
pub fn run_first_task() {
TASK_MANAGER.run_first_task();
}
切换
/// Suspend the current 'Running' task and run the next task in task list.
pub fn suspend_current_and_run_next() {
mark_current_suspended();
run_next_task();
}
退出
/// Exit the current 'Running' task and run the next task in task list.
pub fn exit_current_and_run_next() {
mark_current_exited();
run_next_task();
}
实现yield让步
实现系统调用,相当于主动切换。
// os\src\syscall\process.rs
/// current task gives up resources for other tasks
pub fn sys_yield() -> isize {
trace!("kernel: sys_yield");
suspend_current_and_run_next();
0
}
chapter3练习
编程作业
获取任务信息
思路:每个task记录系统调用次数、任务第一次被调度时刻。第一次调度任务时设置任务第一次被调度时刻为当前时刻(初始设为-1,run_next_task检测若为-1设置为当前时刻)(这一项直接设置为0好像也没关系),每次触发系统调用时使用TASK_MANAGER更改当前task的syscall_times。
简答作业
- 正确进入 U 态后,程序的特征还应有:使用 S 态特权指令,访问 S 态寄存器后会报错。 请同学们可以自行测试这些内容(运行 三个 bad 测例 (ch2b_bad_*.rs) ), 描述程序出错行为,同时注意注明你使用的 sbi 及其版本。
程序出错行为:
ch2b_bad_address: [kernel] PageFault in application, bad addr = 0x0, bad instruction = 0x804003ac, kernel killed it.
ch2b_bad_instructions: [kernel] IllegalInstruction in application, kernel killed it.
ch2b_bad_register: [kernel] IllegalInstruction in application, kernel killed it.
报错来源:
// os\src\trap\mod.rs
Trap::Exception(Exception::StoreFault) | Trap::Exception(Exception::StorePageFault) => {
println!(“[kernel] PageFault in application, bad addr = {:#x}, bad instruction = {:#x}, kernel killed it.”, stval, cx.sepc);
exit_current_and_run_next();
}
Trap::Exception(Exception::IllegalInstruction) => {
println!(“[kernel] IllegalInstruction in application, kernel killed it.”);
exit_current_and_run_next();
}
os的trap_handler捕获非法物理地址和非法指令错误,终止当前程序运行下一个。
-
深入理解 trap.S 中两个函数 __alltraps 和 __restore 的作用,并回答如下问题:
-
L40:刚进入 __restore 时,a0 代表了什么值。请指出 __restore 的两种使用情景。
当 trap_handler 返回或系统调用结束之后,使用 __restore 从保存在内核栈上的 Trap 上下文恢复寄存器。最后通过一条 sret 指令回到应用程序执行。
-
L43-L48:这几行汇编代码特殊处理了哪些寄存器?这些寄存器的的值对于进入用户态有何意义?请分别解释。
ld t0, 32*8(sp) ld t1, 33*8(sp) ld t2, 2*8(sp) csrw sstatus, t0 csrw sepc, t1 csrw sscratch, t2
从内核栈上加载 sstatus、sepc 和 sscratch 的值。使用 csrw 指令将这些值写回到相应的寄存器中。其中 sstatus 是系统状态寄存器,sepc 是程序计数器寄存器,sscratch 用于保存用户栈地址。
-
L50-L56:为何跳过了 x2 和 x4?
ld x1, 1*8(sp) ld x3, 3*8(sp) .set n, 5 .rept 27 LOAD_GP %n .set n, n+1 .endr
x2、x4分别为sp和tp寄存器,栈指针寄存器sp保存在上下文中,线程指针寄存器tp没有实质性用途。
-
L60:该指令之后,sp 和 sscratch 中的值分别有什么意义?
csrrw sp, sscratch, sp
将sp和sscratch的值交换回来,使sp指向内核栈,sscratch指向用户栈。
-
__restore:中发生状态切换在哪一条指令?为何该指令执行之后会进入用户态?
L61 sret
-
L13:该指令之后,sp 和 sscratch 中的值分别有什么意义?
csrrw sp, sscratch, sp
通过csrrw sp, sscratch, sp指令,将sscratch寄存器(通常包含用户栈指针)的值保存到当前栈指针(sp),并将sp寄存器的值写入sscratch。这样,sp现在指向内核栈,而sscratch包含用户栈的指针。
-
从 U 态进入 S 态是哪一条指令发生的?
ecall
-