任务切换¶
本节我们将见识操作系统的核心机制—— 任务切换 , 即应用在运行中主动或被动地交出 CPU 的使用权,内核可以选择另一个程序继续执行。 内核需要保证用户程序两次运行期间,任务上下文(如寄存器、栈等)保持一致。
任务切换的设计与实现¶
任务切换与上一章提及的 Trap 控制流切换相比,有如下异同:
与 Trap 切换不同,它不涉及特权级切换,部分由编译器完成;
与 Trap 切换相同,它对应用是透明的。
事实上,任务切换是来自两个不同应用在内核中的 Trap 控制流之间的切换。
当一个应用 Trap 到 S 态 OS 内核中进行进一步处理时,
其 Trap 控制流可以调用一个特殊的 __switch
函数。
在 __switch
返回之后,Trap 控制流将继续从调用该函数的位置继续向下执行。
而在调用 __switch
之后到返回前的这段时间里,
原 Trap 控制流 A
会先被暂停并被切换出去, CPU 转而运行另一个应用的 Trap 控制流 B
。
__switch
返回之后,原 Trap 控制流 A
才会从某一条 Trap 控制流 C
切换回来继续执行。
我们需要在 __switch
中保存 CPU 的某些寄存器,它们就是 任务上下文 (Task Context)。
下面我们给出 __switch
的实现:
1# os/src/task/switch.S
2
3.altmacro
4.macro SAVE_SN n
5 sd s\n, (\n+2)*8(a0)
6.endm
7.macro LOAD_SN n
8 ld s\n, (\n+2)*8(a1)
9.endm
10 .section .text
11 .globl __switch
12__switch:
13 # __switch(
14 # current_task_cx_ptr: *mut TaskContext,
15 # next_task_cx_ptr: *const TaskContext
16 # )
17 # save kernel stack of current task
18 sd sp, 8(a0)
19 # save ra & s0~s11 of current execution
20 sd ra, 0(a0)
21 .set n, 0
22 .rept 12
23 SAVE_SN %n
24 .set n, n + 1
25 .endr
26 # restore ra & s0~s11 of next execution
27 ld ra, 0(a1)
28 .set n, 0
29 .rept 12
30 LOAD_SN %n
31 .set n, n + 1
32 .endr
33 # restore kernel stack of next task
34 ld sp, 8(a1)
35 ret
它的两个参数分别是当前和即将被切换到的 Trap 控制流的 task_cx_ptr
,从 RISC-V 调用规范可知,它们分别通过寄存器 a0/a1
传入。
内核先把 current_task_cx_ptr
中包含的寄存器值逐个保存,再把 next_task_cx_ptr
中包含的寄存器值逐个恢复。
TaskContext
里包含的寄存器有:
1// os/src/task/context.rs
2#[repr(C)]
3pub struct TaskContext {
4 ra: usize,
5 sp: usize,
6 s: [usize; 12],
7}
s0~s11
是被调用者保存寄存器, __switch
是用汇编编写的,编译器不会帮我们处理这些寄存器。
保存 ra
很重要,它记录了 __switch
函数返回之后应该跳转到哪里继续执行。
我们将这段汇编代码 __switch
解释为一个 Rust 函数:
1// os/src/task/switch.rs
2
3core::arch::global_asm!(include_str!("switch.S"));
4
5extern "C" {
6 pub fn __switch(
7 current_task_cx_ptr: *mut TaskContext,
8 next_task_cx_ptr: *const TaskContext);
9}
我们会调用该函数来完成切换功能,而不是直接跳转到符号 __switch
的地址。
因此在调用前后,编译器会帮我们保存和恢复调用者保存寄存器。