分时多任务系统¶
现代的任务调度算法基本都是抢占式的,它要求每个应用只能连续执行一段时间,然后内核就会将它强制性切换出去。 一般将 时间片 (Time Slice) 作为应用连续执行时长的度量单位,每个时间片可能在毫秒量级。 简单起见,我们使用 时间片轮转算法 (RR, Round-Robin) 来对应用进行调度。
时钟中断与计时器¶
实现调度算法需要计时。RISC-V 要求处理器维护时钟计数器 mtime
,还有另外一个 CSR mtimecmp
。
一旦计数器 mtime
的值超过了 mtimecmp
,就会触发一次时钟中断。
运行在 M 特权级的 SEE 已经预留了相应的接口,基于此编写的 get_time
函数可以取得当前 mtime
计数器的值;
// os/src/timer.rs
use riscv::register::time;
pub fn get_time() -> usize {
time::read()
}
在 10 ms 后设置时钟中断的代码如下:
1// os/src/sbi.rs
2
3const SBI_SET_TIMER: usize = 0;
4
5pub fn set_timer(timer: usize) {
6 sbi_call(SBI_SET_TIMER, timer, 0, 0);
7}
8
9// os/src/timer.rs
10
11use crate::config::CLOCK_FREQ;
12const TICKS_PER_SEC: usize = 100;
13
14pub fn set_next_trigger() {
15 set_timer(get_time() + CLOCK_FREQ / TICKS_PER_SEC);
16}
第 5 行,
sbi
子模块有一个set_timer
调用,用来设置mtimecmp
的值。第 14 行,
timer
子模块的set_next_trigger
函数对set_timer
进行了封装, 它首先读取当前mtime
的值,然后计算出 10ms 之内计数器的增量,再将mtimecmp
设置为二者的和。 这样,10ms 之后一个 S 特权级时钟中断就会被触发。至于增量的计算方式,
CLOCK_FREQ
是一个预先获取到的各平台不同的时钟频率,单位为赫兹,也就是一秒钟之内计数器的增量。 它可以在config
子模块中找到。10ms 的话只需除以常数TICKS_PER_SEC
也就是 100 即可。
后面可能还有一些计时的需求,我们再设计一个函数:
// os/src/timer.rs
const MICRO_PER_SEC: usize = 1_000_000;
pub fn get_time_us() -> usize {
time::read() / (CLOCK_FREQ / MICRO_PER_SEC)
}
timer
子模块的 get_time_us
可以以微秒为单位返回当前计数器的值。
新增一个系统调用,使应用能获取当前的时间:
/// 功能:获取当前的时间,保存在 TimeVal 结构体 ts 中,_tz 在我们的实现中忽略
/// 返回值:返回是否执行成功,成功则返回 0
/// syscall ID:169
fn sys_get_time(ts: *mut TimeVal, _tz: usize) -> isize;
结构体 TimeVal
的定义如下,内核只需调用 get_time_us
即可实现该系统调用。
// os/src/syscall/process.rs
#[repr(C)]
pub struct TimeVal {
pub sec: usize,
pub usec: usize,
}
RISC-V 架构中的嵌套中断问题¶
默认情况下,当 Trap 进入某个特权级之后,在 Trap 处理的过程中同特权级的中断都会被屏蔽。
当 Trap 发生时,
sstatus.sie
会被保存在sstatus.spie
字段中,同时sstatus.sie
置零, 这也就在 Trap 处理的过程中屏蔽了所有 S 特权级的中断;当 Trap 处理完毕
sret
的时候,sstatus.sie
会恢复到sstatus.spie
内的值。
也就是说,如果不去手动设置 sstatus
CSR ,在只考虑 S 特权级中断的情况下,是不会出现 嵌套中断 (Nested Interrupt) 的。
注解
嵌套中断与嵌套 Trap
嵌套中断可以分为两部分:在处理一个中断的过程中又被同特权级/高特权级中断所打断。默认情况下硬件会避免前一部分, 也可以通过手动设置来允许前一部分的存在;而从上面介绍的规则可以知道,后一部分则是无论如何设置都不可避免的。
嵌套 Trap 则是指处理一个 Trap 过程中又再次发生 Trap ,嵌套中断算是嵌套 Trap 的一种。
抢占式调度¶
有了时钟中断和计时器,抢占式调度就很容易实现了:
// os/src/trap/mod.rs
match scause.cause() {
Trap::Interrupt(Interrupt::SupervisorTimer) => {
set_next_trigger();
suspend_current_and_run_next();
}
}
我们只需在 trap_handler
函数下新增一个分支,触发了 S 特权级时钟中断时,重新设置计时器,
调用 suspend_current_and_run_next
函数暂停当前应用并切换到下一个。
为了避免 S 特权级时钟中断被屏蔽,我们需要在执行第一个应用前调用 enable_timer_interrupt()
设置 sie.stie
,
使得 S 特权级时钟中断不会被屏蔽;再设置第一个 10ms 的计时器。
1// os/src/main.rs
2
3#[no_mangle]
4pub fn rust_main() -> ! {
5 // ...
6 trap::enable_timer_interrupt();
7 timer::set_next_trigger();
8 // ...
9}
10
11// os/src/trap/mod.rs
12
13use riscv::register::sie;
14
15pub fn enable_timer_interrupt() {
16 unsafe { sie::set_stimer(); }
17}
就这样,我们实现了时间片轮转任务调度算法。 power
系列用户程序可以验证我们取得的成果:这些应用并没有主动 yield,
内核仍能公平地把时间片分配给它们。