分时多任务系统与抢占式调度
分时多任务系统
在上一章中我们实现了一个协作式的调度方案,用户程序通过调用sys_yield
的系统调用来主动放弃cpu的使用权,然后内核使用轮转调度的方式切换至下一个任务。何为分时多任务系统呢,任务的切换不是通过用户程序来自行放弃cpu的使用权作为前提的,而是内核自己来决定何时切换任务,这个切换的原则就是每个任务一次只能运行一段时间,时间一到就会被操作系统强制切换到下一个任务执行,这就需要内核有一个定时器的东西,这个定时器是通过硬件提供的时钟中断来实现的。
riscv的时钟中断
RISC-V 的中断可以分成三类:
- 软件中断 (Software Interrupt):由软件控制发出的中断
- 时钟中断 (Timer Interrupt):由时钟电路发出的中断
- 外部中断 (External Interrupt):由外设发出的中断
在介绍trap机制时我们用到了scause
寄存器,scause
最高位为1时代表此次触发的异常为中断类型:
Interrupt | Exception Code | Description |
---|---|---|
1 | 1 | Supervisor software interrupt |
1 | 3 | Machine software interrupt |
1 | 5 | Supervisor timer interrupt |
1 | 7 | Machine timer interrupt |
1 | 9 | Supervisor external interrupt |
1 | 11 | Machine external interrupt |
可以看到这三种中断每一个都有 M/S 特权级两个版本。中断的特权级可以决定该中断是否会被屏蔽,以及需要 Trap 到 CPU 的哪个特权级进行处理。我们的目标是在S态使用时钟中断,这涉及到两个个在S态控制中断的寄存器sstatus
,sie
sstatus
的bit[2]
用来使能S态模式下的所有中断
sie
的bit[5]
用来专门使能S态的时钟中断
当设置STIE
位为1时代表启动S态的时钟中断。
sstatus
的 sie
为 S 特权级的中断使能,能够同时控制三种中断,如果将其清零则会将它们全部屏蔽。即使 sstatus.sie
置 1 ,还要看 sie
这个 CSR,它的三个字段 ssie/stie/seie
分别控制 S 特权级的软件中断、时钟中断和外部中断的中断使能。比如对于 S 态时钟中断来说,如果 CPU 不高于 S 特权级,需要 sstatus.sie
和 sie.stie
均为 1 该中断才不会被屏蔽;如果 CPU 当前特权级高于 S 特权级,则该中断一定会被屏蔽。
由于软件(特别是操作系统)需要一种计时机制,RISC-V 架构要求处理器要有一个内置时钟,其频率一般低于 CPU 主频。此外,还有一个计数器用来统计处理器自上电以来经过了多少个内置时钟的时钟周期。在 RISC-V 64 架构上,该计数器保存在一个 64 位的 CSR mtime
中,我们无需担心它的溢出问题,在内核运行全程可以认为它是一直递增的。这个计数器一般我们叫做RTC。另外一个 64 位的 CSR mtimecmp
的作用是:一旦计数器 mtime
的值超过了 mtimecmp
,就会触发一次时钟中断。
所以我们现在来设置时钟中断,在os目录下新建一个timer.c
|
在timer_init()
函数中,分别将sstatus.sie
置 1 和sie.stie
,操作sie
寄存器的代码放在riscv.h
中:
// Supervisor Interrupt Enable |
为了设置时钟中断的频率我们需要先读到mtime
的值,然后设置mtimecmp
,这两个寄存器都是m模式下的,在S模式下不能直接访问,可惜的是在opensbi
中只提供了设置mtimecmp
的接口,因此需要想办法在S态下获取mtime
的值,经过查找,有两种方式可以去得到mtime
的值:
static inline reg_t r_mtime() |
第一种是使用rdtime
这个伪指令,这里是在哪里找的呢,在opensbi
的源码中,在lib/sbi/sbi_timer.c
有这么一个函数:
u64 sbi_timer_value(void) |
opensbi
用此函数来获取时间,opensbi
在进行时钟初始化时,在sbi_timer_init
函数中,对sbi_timer_value
进行了赋值,所以在opensbi
中实际是通过get_ticks
函数来获取时间的
get_ticks
定义如下:
第二种方式是asm volatime("csrr %0, 0x0C01" : "=r" (x) )
来读取,mtime
这个寄存器通过MMIO
映射到了一个确定的地址,这个地址和平台有关,在opensbi
源码的sbi_emulate_csr.c
中,opensbi
将mtime
的值映射到了0xc01
的地方,这是opensbi
做了二次映射,用于S态的程序来读取,实际mtime
的映射地址应该由qemu
来做的,具体的映射方式我也不太清楚……,看下面代码实际上opensbi
也是通过rdtime
去读取的该值:
所以我也不知道rdtime
如何与rtc
关联上的,疑惑…….
总之得到了mtime
的值。mtimecmp
的值可以通过opensbi
提供的接口来设置:在sbi.c
中定义如下
/** |
调用号为0x54494D45
,定义在sbi.h
中。
在qemu
中rtc
的频率为10mhz
,即10^7,在上面的代码中,我将1s分成了1000个时间片,即每隔1us触发一次时钟中断,因此每次触发时钟中断设置的mtimecmp
值为:r_mtime() + CLOCK_FREQ / TICKS_PER_SEC
。这个频率应该是和设备树中的保持一致的
0x986980
换算成10进制就是10mhz
分时多任务
有了时钟中断后,切换任务就简单许多了,只需要在时钟中断到来时,设置下一次时钟中断的mtimecmp
的值,并切换一次任务。因此对trap.c
修改如下:
TrapContext* trap_handler(TrapContext* cx) |
scause
最高位为1时代表为中断则进入中断的判断分支,否则进入异常的处理分支。
测试
app.c
修改:
void task1() |
在三个任务中注释掉了sys_yield()
,我们让内核自主来进行任务切换,
编译测试:./build.sh , ./run.sh
,测试的时候发现#define TICKS_PER_SEC 1000
这里切换频率太高会触发一个异常,暂时不知道如何引起的,因此降低一下频率为500,测试成功。