type
status
slug
date
summary
tags
category
password
icon
管理多道程序
在初始化任务控制块时,我们是这样做的:
init_app_cx
在 loader
子模块中定义,它向内核栈压入了一个 Trap 上下文,并返回压入 Trap 上下文后 sp
的值。 这个 Trap 上下文的构造方式与第二章相同。CSR 名 | 该 CSR 与 Trap 相关的功能 |
sstatus | SPP 等字段给出 Trap 发生之前 CPU 处在哪个特权级(S/U)等信息 |
sepc | 当 Trap 是一个异常的时候,记录 Trap 发生之前执行的最后一条指令的地址 |
scause | 描述 Trap 的原因 |
stval | 给出 Trap 附加信息 |
stvec | 控制 Trap 处理代码的入口地址 |
goto_restore
保存传入的 sp
,并将 ra
设置为 __restore
的入口地址,构造任务上下文后返回。这样,任务管理器中各个应用的任务上下文就得到了初始化。分时多任务系统
简单起见,我们使用 时间片轮转算法 (RR, Round-Robin) 来对应用进行调度。
关于中断
时间片轮转调度的核心机制就在于计时。操作系统的计时功能是依靠硬件提供的时钟中断来实现的。在介绍时钟中断之前,我们先简单介绍一下中断。
在 RISC-V 架构语境下, 中断 (Interrupt) 和我们第二章中介绍的异常(包括程序错误导致或执行 Trap 类指令如用于系统调用的
ecall
)一样都是一种 Trap ,但是它们被触发的原因却是不同的。对于某个处理器核而言, 异常与当前 CPU 的指令执行是 同步 (Synchronous) 的,异常被触发的原因一定能够追溯到某条指令的执行;而中断则 异步 (Asynchronous) 于当前正在进行的指令,也就是说中断来自于哪个外设以及中断如何触发完全与处理器正在执行的当前指令无关。在不考虑指令集拓展的情况下,RISC-V 架构中定义了如下中断:
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 |
RISC-V 的中断可以分成三类:
- 软件中断 (Software Interrupt):由软件控制发出的中断
- 时钟中断 (Timer Interrupt):由时钟电路发出的中断
- 外部中断 (External Interrupt):由外设发出的中断
另外,相比于异常,中断和特权级之间的联系更为紧密,可以看到这三种中断每一个都有 M/S 特权级两个版本。中断的特权级可以决定该中断是否会被屏蔽,以及需要 Trap 到 CPU 的哪个特权级进行处理。
在判断中断是否会被屏蔽的时候,有以下规则:
- 如果中断的特权级低于 CPU 当前的特权级,则该中断会被屏蔽,不会被处理;
- 如果中断的特权级高于与 CPU 当前的特权级或相同,则需要通过相应的 CSR 判断该中断是否会被屏蔽。
以内核所在的 S 特权级为例,中断屏蔽相应的 CSR 有
sstatus
和 sie
。sstatus
的 sie
为 S 特权级的中断使能,能够同时控制三种中断,如果将其清零则会将它们全部屏蔽。即使 sstatus.sie
置 1 ,还要看 sie
这个 CSR,它的三个字段 ssie/stie/seie
分别控制 S 特权级的软件中断、时钟中断和外部中断的中断使能。比如对于 S 态时钟中断来说,如果 CPU 不高于 S 特权级,需要 sstatus.sie
和 sie.stie
均为 1 该中断才不会被屏蔽;如果 CPU 当前特权级高于 S 特权级,则该中断一定会被屏蔽。
我们会在 附录 C:深入机器模式:RustSBI 中再深入介绍中断/异常代理。在本书中我们只需要了解:
- U 特权级的应用程序发出系统调用或产生错误异常都会跳转到 S 特权级的操作系统内核来处理;
- S 特权级的时钟/软件/外部中断产生后,都会跳转到 S 特权级的操作系统内核来处理。
- 当中断发生时,
sstatus.sie
字段会被保存在sstatus.spie
字段中,同时把sstatus.sie
字段置零,这样软件在进行后续的中断处理过程中,所有 S 特权级的中断都会被屏蔽;
- 当软件执行中断处理完毕后,会执行
sret
指令返回到被中断打断的地方继续执行,硬件会把sstatus.sie
字段恢复为sstatus.spie
字段内的值。
也就是说,如果不去手动设置
sstatus
CSR ,在只考虑 S 特权级中断的情况下,是不会出现 嵌套中断 (Nested Interrupt) 的。嵌套中断是指在处理一个中断的过程中再一次触发了中断。由于默认情况下,在软件开始响应中断前, 硬件会自动禁用所有同特权级中断,自然也就不会再次触发中断导致嵌套中断了。lab解决
第一个问题是怎么找到时间?
我想到的是给每个任务记录它的起始时刻,然后每次系统调用的时候去更新每个任务对应的taskInfo里面的time(当前时刻减去起始时刻)
但是我该怎么知道任务被第一次被调度呢?
首先看看这两个结构体:
字段
num_app
仍然表示任务管理器管理的应用的数目,它在 TaskManager
初始化之后就不会发生变化;而包裹在 TaskManagerInner
内的任务控制块数组 tasks
以及表示 CPU 正在执行的应用编号 current_task
会在执行应用的过程中发生变化:每个应用的运行状态都会发生变化,而 CPU 执行的应用也在不断切换。因此我们需要将 TaskManagerInner
包裹在 UPSafeCell
内以获取其内部可变性以及单核上安全的运行时借用检查能力。
我想到了一个思路,同样的task是有限个,那么可以用桶,初始值为0,对于每个app,在调用__restore的时候设置对应桶内的值为当前时间(只要桶的值为初始值0),否则就不设置桶内的初始时间。上面这个思路不好,我仔细分析了一下,任务的调度发生于run_first_task和run_next_task所以我需要做的是在这两个函数里面更新桶时间:
接下来是对于系统调用的统计,这里就需要每次系统调用的时候去更新全局变量桶的时间,以及系统调用次数
做完了,代码在公开的仓库,现在解决其中的几个问题:
为什么write调用是两次?
gdb里面,通过命令:
可以打印变量的char值,就不用去查ascii表了
因为两个换行符号,淦!!!!
println宏这里有一个:
打印的是:
println!("string from task info test\n");
也有一个,而调用write系统的函数是:
会根据\n或者超过缓冲区大小进行一次flush打印,因为两个\n导致会打印两次调用系统调用。删掉\n你就会发现只有一个了,乐。
- 作者:liamY
- 链接:https://liamy.clovy.top/article/OS_Tutorial/lab3
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。