ch3
2025年10月19日 · 4936 字 · 10 分钟
3.1多道程序放置
为了加载运行多个程序,需要每个用户程序被内核加载到内存中的起始地址都不同,本章将不同的应用程序链接到了操作系统的不同的位置,应用程序加载的位置是一个基址加上指定的偏移量
3.2任务切换
即应用在运行中主动或被动地交出 CPU 的使用权,内核可以选择另一个程序继续执行。 内核需要保证用户程序两次运行期间,任务上下文(如寄存器、栈等)保持一致。
上下文切换
操作系统需要“同时”运行远多于CPU核心数量的程序,通过快速地在多个任务之间切换,给用户造成所有任务都在并行执行的假象。
上下文切换便是操作系统为了在单个CPU上实现同时运行多个任务(进程或线程),而保存当前任务状态、并加载下一个任务状态的过程,需要保存包括CPU寄存器的值、程序计数器(PC),堆栈指针以及操作系统为该进程或线程维护的内存分配信息等
上下文切换:
-
中断当前任务:保存当前CPU所有寄存器的值和程序计数器到该任务的进程控制块 中。
-
更新队列:将当前任务的状态从“运行”改为“就绪”或“阻塞”,并放入相应的队列。
-
调度下一个任务:根据调度算法(如轮转、优先级),从就绪队列中选择一个任务来执行。
-
恢复上下文:从下一个任务的PCB中加载其之前保存的寄存器值、程序计数器、内存管理信息(如页表基址寄存器)等。
-
跳转执行:根据恢复的程序计数器,开始执行该任务。
上下文切换实现
//! Implementation of [`TaskContext`]
#[derive(Copy, Clone)]
#[repr(C)]
/// task context structure containing some registers
pub struct TaskContext {
/// Ret position after task switching
ra: usize,
/// Stack pointer
sp: usize,
/// s0-11 register, callee saved
s: [usize; 12],
}
impl TaskContext {
/// Create a new empty task context
pub fn zero_init() -> Self {
Self {
ra: 0,
sp: 0,
s: [0; 12],
}
}
/// Create a new task context with a trap return addr and a kernel stack pointer
pub fn goto_restore(kstack_ptr: usize) -> Self {
extern "C" {
fn __restore();
}
Self {
ra: __restore as usize,
sp: kstack_ptr,
s: [0; 12],
}
}
}
TaskContext结构体就是保存上下文的结构体, 包括s0-s11寄存器, 栈指针sp, 返回地址ra
程序还没运行时,其上下文信息中的ra是ch2介绍的从trap恢复的__restore
任务切换的设计与实现
任务切换是来自两个不同应用在内核中的 Trap 控制流之间的切换
与 Trap 切换不同,它不涉及特权级切换;
与 Trap 切换不同,它的一部分是由编译器帮忙完成的;
与 Trap 切换相同,它对应用是透明的。
阶段[1]:在 Trap 控制流 A 调用 __switch 之前,A 的内核栈上只有 Trap 上下文和 Trap 处理函数的调用栈信息,而 B 是之前被切换出去的;
阶段[2]:A 在 A 任务上下文空间在里面保存 CPU 当前的寄存器快照;
阶段[3]:这一步极为关键,读取 next_task_cx_ptr 指向的 B 任务上下文,根据 B 任务上下文保存的内容来恢复 ra 寄存器、s0~s11 寄存器以及 sp 寄存器。只有这一步做完后, __switch 才能做到一个函数跨两条控制流执行,即 通过换栈也就实现了控制流的切换 。
阶段[4]:上一步寄存器恢复完成后,可以看到通过恢复 sp 寄存器换到了任务 B 的内核栈上,进而实现了控制流的切换。这就是为什么 __switch 能做到一个函数跨两条控制流执行。此后,当 CPU 执行 ret 汇编伪指令完成 __switch 函数返回后,任务 B 可以从调用 __switch 的位置继续向下执行。
__switch的实现:
# 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:
# 阶段 [1]
# __switch(
# current_task_cx_ptr: *mut TaskContext,
# next_task_cx_ptr: *const TaskContext
# )
# 阶段 [2]
# 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
# 阶段 [3]
# 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)
# 阶段 [4]
ret
// os/src/task/context.rs
pub struct TaskContext {
ra: usize,
sp: usize,
s: [usize; 12],
}
a0 和 a1 是函数参数寄存器,在RISC-V调用约定中用来传递第一个和第二个参数。在__switch函数中,它们分别代表current_task_cx_ptr(当前任务上下文指针)和next_task_cx_ptr(下一个任务上下文指针)
-
sd sp, 8(a0):存储当前任务的堆栈指针(sp)到current_task_cx_ptr所指向的结构体的第二个位置(假设结构体的起始位置为0,每个存储单元为8字节,栈指针保存在偏移量为8字节的位置)。
-
sd ra, 0(a0):存储当前任务的返回地址(ra)到current_task_cx_ptr所指向的结构体的起始位置。
-
.set n, 0 和 .rept 12 循环:这是一个宏循环,它重复执行12次,用于保存寄存器s0到s11的值。SAVE_SN是一个宏,用于保存寄存器s0到s11到current_task_cx_ptr所指向的结构体中,对应的偏移量从16字节开始,每次增加8字节。
-
ld ra, 0(a1):加载下一个任务的返回地址到ra寄存器,这个地址来自next_task_cx_ptr所指向的结构体的起始位置。
-
.set n, 0 和 .rept 12 循环:这是另一个宏循环,用于从next_task_cx_ptr所指向的结构体中恢复寄存器s0到s11的值。LOAD_SN宏执行相应的加载操作。
-
ld sp, 8(a1):加载下一个任务的堆栈指针到sp寄存器,这个堆栈指针来自next_task_cx_ptr所指向的结构体的第二个位置。
-
ret:返回指令,它会跳转到ra寄存器中的地址,这里是下一个任务的继续执行点。
// os/src/task/switch.rs将这段汇编代码中的全局符号__switch解释为了一个Rust函数,我们会调用该函数来完成切换功能而不是直接跳转到符号 __switch 的地址。因此在调用前后 Rust 编译器会自动帮助我们插入保存/恢复调用者保存寄存器的汇编代码
3.3管理多道程序
数据结构
// os/src/task/mod.rs
pub struct TaskManager {
num_app: usize,
inner: UPSafeCell<TaskManagerInner>,
}
struct TaskManagerInner {
tasks: [TaskControlBlock; MAX_APP_NUM],
current_task: usize,
}
pub struct TaskControlBlock {
/// The task status in it's lifecycle
pub task_status: TaskStatus,
/// The task context
pub task_cx: TaskContext,
}
/// The status of a task
#[derive(Copy, Clone, PartialEq)]
pub enum TaskStatus {
/// uninitialized
UnInit,
/// ready to run
Ready,
/// running
Running,
/// exited
Exited,
}
将任务的状态和上下文记录在一起, 以便于任务调度
任务调度
找到当前任务的上下文(或者第一个任务运行时的初始化的上下文)和下一个任务的上下文, 将其作为参数传递给__switch函数:
- 第一次运行程序:
// os/src/task/mod.rs
impl TaskManager {
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!");
}
pub fn run_first_task() {
TASK_MANAGER.run_first_task();
}
此时我们没有执行任何应用,__switch前半部分的保存仅仅是在启动栈上保存了一些之后不会用到的数据
显式在启动栈上分配了一个_unused任务上下文并将其作为第一个参数传给__switch,保存一些寄存器之后启动栈栈顶的位置将会保存在此参数中,启动栈在应用开始运行后不会再用到,之后应用都在用户栈和内核栈之间切换
- 程序主动让出CPU或被抢占
// os/src/task/mod.rs
pub fn suspend_current_and_run_next() {
mark_current_suspended();
run_next_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;
}
- 程序运行结束
// os/src/task/mod.rs
pub fn exit_current_and_run_next() {
mark_current_exited();
run_next_task();
}
/// 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;
}
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!");
}
}
3.4分时多任务系统与抢占式调度
一般将 时间片 (Time Slice) 作为应用连续执行时长的度量单位,每个时间片可能在毫秒量级。 简单起见,我们使用 时间片轮转算法 (RR, Round-Robin) 来对应用进行调度
计网入侵了噶人们(x
RISC-V中断机制
软件中断 (Software Interrupt):由软件控制发出的中断
时钟中断 (Timer Interrupt):由时钟电路发出的中断
外部中断 (External Interrupt):由外设发出的中断
时钟中断与计时器
时钟中断是操作系统实现抢占式多任务处理的核心机制。它允许操作系统定期中断当前执行的任务,使调度器能够决定是否继续执行当前任务或切换到其他任务。这种机制确保了:
公平的 CPU 时间分配:所有任务都能获得公平的 CPU 使用机会
系统响应性:即使某个任务执行长时间操作,系统仍能保持响应
真正的多任务:多个任务能够并发执行,提高系统利用率
时钟中断的工作原理
- 硬件时钟:硬件时钟(通常是一个定时器设备)被配置为在固定的时间间隔发出信号。这个时间间隔可以是毫秒级别,具体取决于操作系统的设计和配置。
- 中断信号:当硬件时钟达到预设的时间间隔时,它会向处理器发送一个中断信号。这个信号提示处理器当前正在执行的指令流应该被暂时中断
// os/src/timer.rs
use riscv::register::time;
pub fn get_time() -> usize {
time::read()
}
// os/src/sbi.rs
const SBI_SET_TIMER: usize = 0;
pub fn set_timer(timer: usize) {
sbi_call(SBI_SET_TIMER, timer, 0, 0);
}
// os/src/timer.rs
use crate::config::CLOCK_FREQ;
const TICKS_PER_SEC: usize = 100;
pub fn set_next_trigger() {
set_timer(get_time() + CLOCK_FREQ / TICKS_PER_SEC);
}
sbi子模块有一个set_timer调用,用来设置mtimecmp的值,timer子模块set_next_trigger函数对set_timer进行了封装,它首先读取当前mtime的值,然后计算出10ms之内计数器的增量,再将mtimecmp设置为二者的和
-
中断服务例程(ISR):当处理器接收到中断信号后,会跳转执行特定的中断服务例程。这些 ISR 由操作系统提供,负责处理不同类型的中断。
RISC-V 架构的特殊性:在 RISC-V 架构中,中断和异常通过统一的陷阱(Trap)机制进行处理。所有陷阱(包括中断和异常)都共享同一个入口点,这个入口地址存储在 stvec(Supervisor Trap Vector Base Address Register)寄存器中。
///os/src/trap/mod.rs
pub fn init() {
extern "C" {
fn __alltraps();
}
unsafe {
stvec::write(__alltraps as usize, TrapMode::Direct);
}
}
3.5抢占式调度
// os/src/trap/mod.rs
match scause.cause() {
Trap::Interrupt(Interrupt::SupervisorTimer) => {
set_next_trigger();
suspend_current_and_run_next();
}
}
当时钟中断(SupervisorTimer)发生时,陷阱处理程序会:
-
重置定时器以触发下一次中断
-
调用 suspend_current_and_run_next() 进行任务切换
-
保存当前任务上下文,切换到下一个就绪任务
3.6 ch3练习
代码见github
- 实现sys_trace
- 新增syscall_count用于计数每个syscall_id对应syscall调用次数
- 通过inc_current_syscall_count和get_current_syscall_count分别用于统计系统调用计数和获取系统调用计数,即可支持读、写、统计三种操作
- 通过检查系统调用ID范围,避免越界
简答作业
RustSBI-QEMU Version 0.2.0-alpha.2
[kernel] PageFault in application, bad addr = 0x0, bad instruction = 0x804003c4, kernel killed it.
[kernel] IllegalInstruction in application, kernel killed it.
[kernel] IllegalInstruction in application, kernel killed it.
ch2b_bad_address.rs 用户程序尝试访问地址 0x0,该地址不在有效的用户地址空间内
ch2b_bad_instructions.rs 在用户态非法使用指令sret
ch2b_bad_register.rs 在U态通过csrr指令非法读取S态寄存器sstatus
RISC-V 特权级保护
-
指令特权级检查:
-
U态程序无法执行sret, wfi, sfence.vma等S态指令
-
尝试执行会触发非法指令异常
-
-
寄存器访问保护:
-
U态无法访问S态CSR寄存器(sstatus, sie, stvec等)
-
CSR读写指令在错误特权级会触发异常
-
-
内存访问保护:
-
通过页表机制隔离用户空间和内核空间
-
用户程序只能访问映射的用户地址空间
-
2-1.
刚进入 __restore 时,sp代表当前任务的内核栈顶
__restore 的两种使用情景:
任务切换后的恢复:当从调度器切换到新任务时,调用 __restore 来恢复该任务的用户态上下文
中断/异常处理返回:处理完陷阱后,恢复被中断的用户程序继续执行
2-2.
ld t0, 32*8(sp) # 内核栈 32*8(sp) 处存储了原 sstatus 寄存器的值, 将其读取到 t0
ld t1, 33*8(sp) # 内核栈 32*8(sp) 处存储了原 sepc 寄存器的值, 将其读取到 t1
ld t2, 2*8(sp) # 内核栈 32*8(sp) 处存储了原 sscratch 寄存器的值, 将其读取到 t2
csrw sstatus, t0 # 将 t0中原 sstatus 寄存器的值读取到 sstatus
csrw sepc, t1 # 将 t0中原 sepc 寄存器的值读取到 sepc
csrw sscratch, t2 # 将 t0中原 sscratch 寄存器的值读取到 sscratch
2-3.
x2 (sp):栈指针需要特殊处理,不能在这里直接恢复,因为当前还在使用内核栈
x4 (tp):线程指针通常在内核初始化后保持不变,不需要在每次陷阱返回时恢复
2-4.
L60
sp:指向用户栈指针
sscratch:指向内核栈指针(保存原来的 sp 值)
2-5.
sret检查sstatus.SPP位,如果为0表示返回用户态,将程序计数器设置为sepc的值,将特权级从S态降为U态,从sstatus.SPIE恢复sstatus.SIE
2-6.
L13
sp:保存了原sscratch中的内核栈指针
sscratch:保存了原sp中的用户栈栈指针
2-7.
ecall
荣誉准则
-
在完成本次实验的过程(含此前学习的过程)中,我曾分别与以下各位 就(与本次实验相关的)以下方面做过交流,还在代码中对应的位置以注释形式记录了具体的交流对象及内容:
无 -
此外,我也参考了 以下资料 ,还在代码中对应的位置以注释形式记录了具体的参考来源及内容:
无 -
我独立完成了本次实验除以上方面之外的所有工作,包括代码与文档。 我清楚地知道,从以上方面获得的信息在一定程度上降低了实验难度,可能会影响起评分。
-
我从未使用过他人的代码,不管是原封不动地复制,还是经过了某些等价转换。 我未曾也不会向他人(含此后各届同学)复制或公开我的实验代码,我有义务妥善保管好它们。 我提交至本实验的评测系统的代码,均无意于破坏或妨碍任何计算机系统的正常运转。 我清楚地知道,以上情况均为本课程纪律所禁止,若违反,对应的实验成绩将按“-100”分计。

