add os[1-8]-ref for os refereces, add guide, add README

This commit is contained in:
Yu Chen
2022-06-27 22:22:44 +08:00
parent 7c1679774c
commit d752a67137
360 changed files with 32863 additions and 1 deletions

View File

@@ -0,0 +1,209 @@
引言
========================================
本章导读
--------------------------
本章的目标是实现分时多任务系统,它能并发地执行多个用户程序,并调度这些程序。为此需要实现
- 一次性加载所有用户程序,减少任务切换开销;
- 支持任务切换机制,保存切换前后程序上下文;
- 支持程序主动放弃处理器,实现 yield 系统调用;
- 以时间片轮转算法调度用户程序,实现资源的时分复用。
实践体验
-------------------------------------
.. code-block:: console
$ git clone https://github.com/LearningOS/rCore-Tutorial-Code-2022S.git
$ cd rCore-Tutorial-Code-2022S
$ git checkout ch3
$ git clone https://github.com/LearningOS/rCore-Tutorial-Test-2022S.git user
在 qemu 模拟器上运行本章代码:
.. code-block:: console
$ cd os
$ make run
运行代码,看到用户程序交替输出信息:
.. code-block::
[rustsbi] RustSBI version 0.2.0-alpha.4
.______ __ __ _______.___________. _______..______ __
| _ \ | | | | / | | / || _ \ | |
| |_) | | | | | | (----`---| |----`| (----`| |_) || |
| / | | | | \ \ | | \ \ | _ < | |
| |\ \----.| `--' |.----) | | | .----) | | |_) || |
| _| `._____| \______/ |_______/ |__| |_______/ |______/ |__|
[rustsbi] Implementation: RustSBI-QEMU Version 0.0.1
[rustsbi-dtb] Hart count: cluster0 with 1 cores
[rustsbi] misa: RV64ACDFIMSU
[rustsbi] mideleg: ssoft, stimer, sext (0x222)
[rustsbi] medeleg: ima, ia, bkpt, la, sa, uecall, ipage, lpage, spage (0xb1ab)
[rustsbi] pmp0: 0x80000000 ..= 0x800fffff (rwx)
[rustsbi] pmp1: 0x80000000 ..= 0x807fffff (rwx)
[rustsbi] pmp2: 0x0 ..= 0xffffffffffffff (---)
[rustsbi] enter supervisor 0x80200000
[kernel] Hello, world!
power_3 [10000/200000]
power_3 [20000/200000]
power_3 [30000/200000]
power_3 [40000/200000]
power_3 [50000/200000]
power_3 [60000/200000]
power_3 [70000/200000]
power_3 [80000/200000]
power_3 [90000/200000]
power_3 [100000/200000]
power_3 [110000/200000]
power_3 [120000/200000]
power_3 [130000/200000]
power_3 [140000/200000]
power_3 [150000/200000]
power_3 [160000/200000]
power_3 [170000/200000]
power_3 [180000/200000]
power_3 [190000/200000]
power_3 [200000/200000]
3^200000 = 871008973(MOD 998244353)
Test power_3 OK!
power_5 [10000/140000]
power_5 [20000/140000]
power_5 [30000/140000]
power_5 [40000/140000]
power_5 [50000/140000]
power_5 [60000/140000]
power_7 [10000/160000]
power_7 [20000/160000]
power_7 [30000/160000]
power_7 [40000/160000]
power_7 [50000/160000]
power_7 [60000/160000]
power_7 [70000/160000]
power_7 [80000/160000]
power_7 [90000/160000]
power_7 [100000/160000]
power_7 [110000/160000]
power_7 [120000/160000]
power_7 [130000/160000]
power_7 [140000/160000]
power_7 [150000/160000]
power_7 [160000/160000]
7^160000 = 667897727(MOD 998244353)
Test power_7 OK!
get_time OK! 42
current time_msec = 42
AAAAAAAAAA [1/5]
BBBBBBBBBB [1/5]
CCCCCCCCCC [1/5]
power_5 [70000/140000]
AAAAAAAAAA [2/5]
BBBBBBBBBB [2/5]
CCCCCCCCCC [2/5]
power_5 [80000/140000]
power_5 [90000/140000]
power_5 [100000/140000]
power_5 [110000/140000]
power_5 [120000/140000]
power_5 [130000/140000]
power_5 [140000/140000]
5^140000 = 386471875(MOD 998244353)
Test power_5 OK!
AAAAAAAAAA [3/5]
BBBBBBBBBB [3/5]
CCCCCCCCCC [3/5]
AAAAAAAAAA [4/5]
BBBBBBBBBB [4/5]
CCCCCCCCCC [4/5]
AAAAAAAAAA [5/5]
BBBBBBBBBB [5/5]
CCCCCCCCCC [5/5]
Test write A OK!
Test write B OK!
Test write C OK!
time_msec = 143 after sleeping 100 ticks, delta = 101ms!
Test sleep1 passed!
Test sleep OK!
Panicked at src/task/mod.rs:98 All applications completed!
本章代码树
---------------------------------------------
.. code-block::
── os
   ├── build.rs
   ├── Cargo.toml
   ├── Makefile
   └── src
   ├── batch.rs(移除:功能分别拆分到 loader 和 task 两个子模块)
  ├── config.rs(新增:保存内核的一些配置)
   ├── console.rs
├── logging.rs
├── sync
   ├── entry.asm
   ├── lang_items.rs
   ├── link_app.S
   ├── linker.ld
   ├── loader.rs(新增:将应用加载到内存并进行管理)
   ├── main.rs(修改:主函数进行了修改)
   ├── sbi.rs(修改:引入新的 sbi call set_timer)
   ├── syscall(修改:新增若干 syscall)
   │   ├── fs.rs
   │   ├── mod.rs
   │   └── process.rs
   ├── task(新增task 子模块,主要负责任务管理)
   │   ├── context.rs(引入 Task 上下文 TaskContext)
   │   ├── mod.rs(全局任务管理器和提供给其他模块的接口)
   │   ├── switch.rs(将任务切换的汇编代码解释为 Rust 接口 __switch)
   │   ├── switch.S(任务切换的汇编代码)
   │   └── task.rs(任务控制块 TaskControlBlock 和任务状态 TaskStatus 的定义)
   ├── timer.rs(新增:计时器相关)
   └── trap
   ├── context.rs
   ├── mod.rs(修改:时钟中断相应处理)
   └── trap.S
cloc os
-------------------------------------------------------------------------------
Language files blank comment code
-------------------------------------------------------------------------------
Rust 21 87 20 627
Assembly 4 12 22 144
make 1 11 4 36
TOML 1 2 1 10
-------------------------------------------------------------------------------
SUM: 27 112 47 817
-------------------------------------------------------------------------------
.. 本章代码导读
.. -----------------------------------------------------
.. 本章的重点是实现对应用之间的协作式和抢占式任务切换的操作系统支持。与上一章的操作系统实现相比,有如下一些不同的情况导致实现上也有差异:
.. - 多个应用同时放在内存中,所以他们的起始地址是不同的,且地址范围不能重叠
.. - 应用在整个执行过程中会暂停或被抢占,即会有主动或被动的任务切换
.. 这些实现上差异主要集中在对应用程序执行过程的管理、支持应用程序暂停的系统调用和主动切换应用程序所需的时钟中断机制的管理。
.. 对于第一个不同情况,需要对应用程序的地址空间布局进行调整,每个应用的地址空间都不相同,且不能重叠。这并不要修改应用程序本身,而是通过一个脚本 ``build.py`` 来针对每个应用程序修改链接脚本 ``linker.ld`` 中的 ``BASE_ADDRESS`` ,让编译器在编译不同应用时用到的 ``BASE_ADDRESS`` 都不同,且有足够大的地址间隔。这样就可以让每个应用所在的内存空间是不同的。
.. 对于第二个不同情况,需要实现任务切换,这就需要在上一章的 ``trap`` 上下文切换的基础上,再加上一个 ``task`` 上下文切换,才能完成完整的任务切换。这里面的关键数据结构是表示应用执行上下文的 ``TaskContext`` 数据结构和具体完成上下文切换的汇编语言编写的 ``__switch`` 函数。一个应用的执行需要被操作系统管理起来,这是通过 ``TaskControlBlock`` 数据结构来表示应用执行上下文的动态过程和动态状态(运行态、就绪态等)。而为了做好应用程序第一次执行的前期初始化准备, ``TaskManager`` 数据结构的全局变量实例 ``TASK_MANAGER`` 描述了应用程序初始化所需的数据, 而 ``TASK_MANAGER`` 的初始化赋值过程是实现这个准备的关键步骤。
.. 应用程序可以在用户态执行后,还需要有新的系统调用 ``sys_yield`` 的实现来支持应用自己的主动暂停;还要添加对时钟中断的处理,来支持抢占应用执行的抢占式切换。有了时钟中断,就可以在一定时间内打断应用的执行,并主动切换到另外一个应用,这部分主要是通过对 ``trap_handler`` 函数中进行扩展,来完成在时钟中断产生时可能进行的任务切换。 ``TaskManager`` 数据结构的成员函数 ``run_next_task`` 来实现基于任务控制块的切换,并会具体调用 ``__switch`` 函数完成硬件相关部分的任务上下文切换。
.. 如果理解了上面的数据结构和相关函数的关系和相互调用的情况,那么就比较容易理解本章改进后的操作系统了。
.. .. [#prionosuchus] 锯齿螈身长可达9米是迄今出现过的最大的两栖动物是二叠纪时期江河湖泊和沼泽中的顶级掠食者。
.. .. [#eoraptor] 始初龙(也称始盗龙)是后三叠纪时期的两足食肉动物,也是目前所知最早的恐龙,它们只有一米长,却代表着恐龙的黎明。
.. .. [#coelophysis] 腔骨龙(也称虚形龙)最早出现于三叠纪晚期,它体形纤细,善于奔跑,以小型动物为食。

View File

@@ -0,0 +1,71 @@
多道程序放置与加载
=====================================
多道程序放置
----------------------------
在第二章中,内核让所有应用都共享同一个固定的起始地址。
正因如此,内存中同时最多只能驻留一个应用,
要一次加载运行多个程序,就要求每个用户程序被内核加载到内存中的起始地址都不同。
为此,我们编写脚本 ``user/build.py`` 为每个应用定制各自的起始地址。
它的思路很简单,对于每一个应用程序,使用 ``cargo rustc`` 单独编译,
``-Clink-args=-Ttext=xxxx`` 选项指定链接时 .text 段的地址为 ``0x80400000 + app_id * 0x20000``
.. note::
qemu 预留的内存空间是有限的,如果加载的程序过多,程序地址超出内存空间,可能出现 ``core dumped``.
多道程序加载
----------------------------
在第二章中负责应用加载和执行的子模块 ``batch`` 被拆分为 ``loader````task``
前者负责启动时加载应用程序,后者负责切换和调度。
其中, ``loader`` 模块的 ``load_apps`` 函数负责将所有用户程序在内核初始化的时一并加载进内存。
.. code-block:: rust
:linenos:
// os/src/loader.rs
pub fn load_apps() {
extern "C" {
fn _num_app();
}
let num_app_ptr = _num_app as usize as *const usize;
let num_app = get_num_app();
let app_start = unsafe { core::slice::from_raw_parts(num_app_ptr.add(1), num_app + 1) };
// clear i-cache first
unsafe {
core::arch::asm!("fence.i");
}
// load apps
for i in 0..num_app {
let base_i = get_base_i(i);
// clear region
(base_i..base_i + APP_SIZE_LIMIT)
.for_each(|addr| unsafe { (addr as *mut u8).write_volatile(0) });
// load app from data section to memory
let src = unsafe {
core::slice::from_raw_parts(app_start[i] as *const u8, app_start[i + 1] - app_start[i])
};
let dst = unsafe { core::slice::from_raw_parts_mut(base_i as *mut u8, src.len()) };
dst.copy_from_slice(src);
}
}
:math:`i` 个应用被加载到以物理地址 ``base_i`` 开头的一段物理内存上,而 ``base_i`` 的计算方式如下:
.. code-block:: rust
:linenos:
// os/src/loader.rs
fn get_base_i(app_id: usize) -> usize {
APP_BASE_ADDRESS + app_id * APP_SIZE_LIMIT
}
我们可以在 ``config`` 子模块中找到这两个常数, ``APP_BASE_ADDRESS`` 被设置为 ``0x80400000``
``APP_SIZE_LIMIT`` 和上一章一样被设置为 ``0x20000`` 。这种放置方式与 ``user/build.py`` 的实现一致。

View File

@@ -0,0 +1,104 @@
任务切换
================================
本节我们将见识操作系统的核心机制—— **任务切换**
即应用在运行中主动或被动地交出 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`` 的实现:
.. code-block:: riscv
:linenos:
# 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:
# __switch(
# current_task_cx_ptr: *mut TaskContext,
# next_task_cx_ptr: *const TaskContext
# )
# 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
# 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)
ret
它的两个参数分别是当前和即将被切换到的 Trap 控制流的 ``task_cx_ptr`` ,从 RISC-V 调用规范可知,它们分别通过寄存器 ``a0/a1`` 传入。
内核先把 ``current_task_cx_ptr`` 中包含的寄存器值逐个保存,再把 ``next_task_cx_ptr`` 中包含的寄存器值逐个恢复。
``TaskContext`` 里包含的寄存器有:
.. code-block:: rust
:linenos:
// os/src/task/context.rs
#[repr(C)]
pub struct TaskContext {
ra: usize,
sp: usize,
s: [usize; 12],
}
``s0~s11`` 是被调用者保存寄存器, ``__switch`` 是用汇编编写的,编译器不会帮我们处理这些寄存器。
保存 ``ra`` 很重要,它记录了 ``__switch`` 函数返回之后应该跳转到哪里继续执行。
我们将这段汇编代码 ``__switch`` 解释为一个 Rust 函数:
.. code-block:: rust
:linenos:
// os/src/task/switch.rs
core::arch::global_asm!(include_str!("switch.S"));
extern "C" {
pub fn __switch(
current_task_cx_ptr: *mut TaskContext,
next_task_cx_ptr: *const TaskContext);
}
我们会调用该函数来完成切换功能,而不是直接跳转到符号 ``__switch`` 的地址。
因此在调用前后,编译器会帮我们保存和恢复调用者保存寄存器。

View File

@@ -0,0 +1,317 @@
管理多道程序
=========================================
而内核为了管理任务,需要维护任务信息,相关内容包括:
- 任务运行状态:未初始化、准备执行、正在执行、已退出
- 任务控制块:维护任务状态和任务上下文
- 任务相关系统调用:程序主动暂停 ``sys_yield`` 和主动退出 ``sys_exit``
yield 系统调用
-------------------------------------------------------------------------
.. image:: multiprogramming.png
上图描述了一种多道程序执行的典型情况。其中横轴为时间线,纵轴为正在执行的实体。
开始时,蓝色应用向外设提交了一个请求,外设随即开始工作,
但是它要一段时间后才能返回结果。蓝色应用于是调用 ``sys_yield`` 交出 CPU 使用权,
内核让绿色应用继续执行。一段时间后 CPU 切换回蓝色应用,发现外设仍未返回结果,
于是再次 ``sys_yield`` 。直到第二次切换回蓝色应用,外设才处理完请求,于是蓝色应用终于可以向下执行了。
我们还会遇到很多其他需要等待其完成才能继续向下执行的事件,调用 ``sys_yield`` 可以避免等待过程造成的资源浪费。
.. code-block:: rust
:caption: 第三章新增系统调用(一)
/// 功能:应用主动交出 CPU 所有权并切换到其他应用。
/// 返回值:总是返回 0。
/// syscall ID124
fn sys_yield() -> isize;
用户库对应的实现和封装:
.. code-block:: rust
// user/src/syscall.rs
pub fn sys_yield() -> isize {
syscall(SYSCALL_YIELD, [0, 0, 0])
}
// user/src/lib.rs
// yield 是 Rust 的关键字
pub fn yield_() -> isize { sys_yield() }
下文介绍内核应如何实现该系统调用。
任务控制块与任务运行状态
---------------------------------------------------------
任务运行状态暂包括如下几种:
.. code-block:: rust
:linenos:
// os/src/task/task.rs
#[derive(Copy, Clone, PartialEq)]
pub enum TaskStatus {
UnInit, // 未初始化
Ready, // 准备运行
Running, // 正在运行
Exited, // 已退出
}
任务状态外和任务上下文一并保存在名为 **任务控制块** (Task Control Block) 的数据结构中:
.. code-block:: rust
:linenos:
// os/src/task/task.rs
#[derive(Copy, Clone)]
pub struct TaskControlBlock {
pub task_status: TaskStatus,
pub task_cx: TaskContext,
}
任务控制块非常重要。在内核中,它就是应用的管理单位。后面的章节我们还会不断向里面添加更多内容。
任务管理器
--------------------------------------
内核需要一个全局的任务管理器来管理这些任务控制块:
.. code-block:: rust
// os/src/task/mod.rs
pub struct TaskManager {
num_app: usize,
inner: UPSafeCell<TaskManagerInner>,
}
struct TaskManagerInner {
tasks: [TaskControlBlock; MAX_APP_NUM],
current_task: usize,
}
这里用到了变量与常量分离的编程风格:字段 ``num_app`` 表示应用数目,它在 ``TaskManager`` 初始化后将保持不变;
而包裹在 ``TaskManagerInner`` 内的任务控制块数组 ``tasks``,以及正在执行的应用编号 ``current_task`` 会在执行过程中变化。
初始化 ``TaskManager`` 的全局实例 ``TASK_MANAGER``
.. code-block:: rust
:linenos:
// os/src/task/mod.rs
lazy_static! {
pub static ref TASK_MANAGER: TaskManager = {
let num_app = get_num_app();
let mut tasks = [TaskControlBlock {
task_cx: TaskContext::zero_init(),
task_status: TaskStatus::UnInit,
}; MAX_APP_NUM];
for (i, t) in tasks.iter_mut().enumerate().take(num_app) {
t.task_cx = TaskContext::goto_restore(init_app_cx(i));
t.task_status = TaskStatus::Ready;
}
TaskManager {
num_app,
inner: unsafe {
UPSafeCell::new(TaskManagerInner {
tasks,
current_task: 0,
})
},
}
};
}
- 第 5 行:调用 ``loader`` 子模块提供的 ``get_num_app`` 接口获取链接到内核的应用总数;
- 第 10~12 行:依次对每个任务控制块进行初始化,将其运行状态设置为 ``Ready`` ,并在它的内核栈栈顶压入一些初始化
上下文,然后更新它的 ``task_cx`` 。一些细节我们会稍后介绍。
- 从第 14 行开始:创建 ``TaskManager`` 实例并返回。
.. note::
关于 Rust 迭代器语法如 ``iter_mut/(a..b)`` ,及其方法如 ``enumerate/map/find/take``,请参考 Rust 官方文档。
实现 sys_yield 和 sys_exit
----------------------------------------------------------------------------
``sys_yield`` 的实现用到了 ``task`` 子模块提供的 ``suspend_current_and_run_next`` 接口,这个接口如字面含义,就是暂停当前的应用并切换到下个应用。
.. code-block:: rust
// os/src/syscall/process.rs
use crate::task::suspend_current_and_run_next;
pub fn sys_yield() -> isize {
suspend_current_and_run_next();
0
}
``sys_exit`` 基于 ``task`` 子模块提供的 ``exit_current_and_run_next`` 接口,它的含义是退出当前的应用并切换到下个应用:
.. code-block:: rust
// os/src/syscall/process.rs
use crate::task::exit_current_and_run_next;
pub fn sys_exit(exit_code: i32) -> ! {
println!("[kernel] Application exited with code {}", exit_code);
exit_current_and_run_next();
panic!("Unreachable in sys_exit!");
}
那么 ``suspend_current_and_run_next````exit_current_and_run_next`` 各是如何实现的呢?
.. code-block:: rust
// os/src/task/mod.rs
pub fn suspend_current_and_run_next() {
TASK_MANAGER.mark_current_suspended();
TASK_MANAGER.run_next_task();
}
pub fn exit_current_and_run_next() {
TASK_MANAGER.mark_current_exited();
TASK_MANAGER.run_next_task();
}
它们都是先修改当前应用的运行状态,然后尝试切换到下一个应用。修改运行状态比较简单,实现如下:
.. code-block:: rust
:linenos:
// os/src/task/mod.rs
impl TaskManager {
fn mark_current_suspended(&self) {
let mut inner = self.inner.exclusive_access();
let current = inner.current_task;
inner.tasks[current].task_status = TaskStatus::Ready;
}
}
``mark_current_suspended`` 为例。首先获得里层 ``TaskManagerInner`` 的可变引用,然后修改任务控制块数组 ``tasks`` 中当前任务的状态。
再看 ``run_next_task`` 的实现:
.. code-block:: rust
:linenos:
// os/src/task/mod.rs
impl TaskManager {
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!");
}
}
fn find_next_task(&self) -> Option<usize> {
let inner = self.inner.exclusive_access();
let current = inner.current_task;
(current + 1..current + self.num_app + 1)
.map(|id| id % self.num_app)
.find(|id| inner.tasks[*id].task_status == TaskStatus::Ready)
}
}
``run_next_task`` 会调用 ``find_next_task`` 方法尝试寻找一个运行状态为 ``Ready`` 的应用并获得其 ID 。
如果找不到, 说明所有应用都执行完了, ``find_next_task`` 将返回 ``None`` ,内核 panic 退出。
如果能够找到下一个可运行应用,我们就调用 ``__switch`` 切换任务。
切换任务之前,我们要手动 drop 掉我们获取到的 ``TaskManagerInner`` 可变引用。
因为函数还没有返回, ``inner`` 不会自动销毁。我们只有令 ``TASK_MANAGER````inner`` 字段回到未被借用的状态,下次任务切换时才能再借用。
我们可以总结一下应用的运行状态变化图:
.. image:: fsm-coop.png
第一次进入用户态
------------------------------------------
我们在第二章中介绍过 CPU 第一次从内核态进入用户态的方法,只需在内核栈上压入构造好的 Trap 上下文,
然后 ``__restore`` 即可。本章要在此基础上做一些扩展。
在初始化任务控制块时,我们是这样做的:
.. code-block:: rust
// os/src/task/mod.rs
for (i, t) in tasks.iter_mut().enumerate().take(num_app) {
t.task_cx = TaskContext::goto_restore(init_app_cx(i));
t.task_status = TaskStatus::Ready;
}
``init_app_cx````loader`` 子模块中定义,它向内核栈压入了一个 Trap 上下文,并返回压入 Trap 上下文后 ``sp`` 的值。
这个 Trap 上下文的构造方式与第二章相同。
``goto_restore`` 保存传入的 ``sp``,并将 ``ra`` 设置为 ``__restore`` 的入口地址,构造任务上下文后返回。这样,任务管理器中各个应用的任务上下文就得到了初始化。
.. code-block:: rust
// os/src/task/context.rs
impl TaskContext {
pub fn goto_restore(kstack_ptr: usize) -> Self {
extern "C" { fn __restore(); }
Self {
ra: __restore as usize,
sp: kstack_ptr,
s: [0; 12],
}
}
}
``rust_main`` 中我们调用 ``task::run_first_task`` 来执行第一个应用:
.. code-block:: rust
:linenos:
// os/src/task/mod.rs
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!");
}
我们显式声明了一个 ``_unused`` 变量,并将它的地址作为第一个参数传给 ``__switch``
声明此变量的意义仅仅是为了避免其他数据被覆盖。
``__switch`` 中恢复 ``sp`` 后, ``sp`` 将指向 ``init_app_cx`` 构造的 Trap 上下文,后面就回到第二章的情况了。
此外, ``__restore`` 的实现需要做出变化:它 **不再需要** 在开头 ``mv sp, a0`` 了。因为在 ``__switch`` 之后,``sp`` 就已经正确指向了我们需要的 Trap 上下文地址。

View File

@@ -0,0 +1,161 @@
分时多任务系统
===========================================================
现代的任务调度算法基本都是抢占式的,它要求每个应用只能连续执行一段时间,然后内核就会将它强制性切换出去。
一般将 **时间片** (Time Slice) 作为应用连续执行时长的度量单位,每个时间片可能在毫秒量级。
简单起见,我们使用 **时间片轮转算法** (RR, Round-Robin) 来对应用进行调度。
时钟中断与计时器
------------------------------------------------------------------
实现调度算法需要计时。RISC-V 要求处理器维护时钟计数器 ``mtime``,还有另外一个 CSR ``mtimecmp``
一旦计数器 ``mtime`` 的值超过了 ``mtimecmp``,就会触发一次时钟中断。
运行在 M 特权级的 SEE 已经预留了相应的接口,基于此编写的 ``get_time`` 函数可以取得当前 ``mtime`` 计数器的值;
.. code-block:: rust
// os/src/timer.rs
use riscv::register::time;
pub fn get_time() -> usize {
time::read()
}
在 10 ms 后设置时钟中断的代码如下:
.. code-block:: rust
:linenos:
// 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);
}
- 第 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 即可。
后面可能还有一些计时的需求,我们再设计一个函数:
.. code-block:: rust
// 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`` 可以以微秒为单位返回当前计数器的值。
新增一个系统调用,使应用能获取当前的时间:
.. code-block:: rust
:caption: 第三章新增系统调用(二)
/// 功能:获取当前的时间,保存在 TimeVal 结构体 ts 中_tz 在我们的实现中忽略
/// 返回值:返回是否执行成功,成功则返回 0
/// syscall ID169
fn sys_get_time(ts: *mut TimeVal, _tz: usize) -> isize;
结构体 ``TimeVal`` 的定义如下,内核只需调用 ``get_time_us`` 即可实现该系统调用。
.. code-block:: rust
// 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) 的。
.. note::
**嵌套中断与嵌套 Trap**
嵌套中断可以分为两部分:在处理一个中断的过程中又被同特权级/高特权级中断所打断。默认情况下硬件会避免前一部分,
也可以通过手动设置来允许前一部分的存在;而从上面介绍的规则可以知道,后一部分则是无论如何设置都不可避免的。
嵌套 Trap 则是指处理一个 Trap 过程中又再次发生 Trap ,嵌套中断算是嵌套 Trap 的一种。
抢占式调度
-----------------------------------
有了时钟中断和计时器,抢占式调度就很容易实现了:
.. code-block:: rust
// 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 的计时器。
.. code-block:: rust
:linenos:
// os/src/main.rs
#[no_mangle]
pub fn rust_main() -> ! {
// ...
trap::enable_timer_interrupt();
timer::set_next_trigger();
// ...
}
// os/src/trap/mod.rs
use riscv::register::sie;
pub fn enable_timer_interrupt() {
unsafe { sie::set_stimer(); }
}
就这样,我们实现了时间片轮转任务调度算法。 ``power`` 系列用户程序可以验证我们取得的成果:这些应用并没有主动 yield
内核仍能公平地把时间片分配给它们。

View File

@@ -0,0 +1,133 @@
chapter3练习
=======================================
编程作业
--------------------------------------
获取任务信息
++++++++++++++++++++++++++
ch3 中,我们的系统已经能够支持多个任务分时轮流运行,我们希望引入一个新的系统调用 ``sys_task_info`` 以获取当前任务的信息,定义如下:
.. code-block:: rust
fn sys_task_info(ti: *mut TaskInfo) -> isize
- syscall ID: 410
- 查询当前正在执行的任务信息任务信息包括任务控制块相关信息任务状态、任务使用的系统调用及调用次数、任务总运行时长单位ms
.. code-block:: rust
struct TaskInfo {
status: TaskStatus,
syscall_times: [u32; MAX_SYSCALL_NUM],
time: usize
}
- 参数:
- ti: 待查询任务信息
- 返回值执行成功返回0错误返回-1
- 说明:
- 相关结构已在框架中给出,只需添加逻辑实现功能需求即可。
- 在我们的实验中,系统调用号一定小于 500所以直接使用一个长为 ``MAX_SYSCALL_NUM=500`` 的数组做桶计数。
- 运行时间 time 返回系统调用时刻距离任务第一次被调度时刻的时长,也就是说这个时长可能包含该任务被其他任务抢占后的等待重新调度的时间。
- 由于查询的是当前任务的状态,因此 TaskStatus 一定是 Running。助教起初想设计根据任务 id 查询,但是既不好定义任务 id 也不好写测例,遂放弃 QAQ
- 调用 ``sys_task_info`` 也会对本次调用计数。
- 提示:
- 大胆修改已有框架!除了配置文件,你几乎可以随意修改已有框架的内容。
- 程序运行时间可以通过调用 ``get_time()`` 获取,注意任务运行总时长的单位是 ms。
- 系统调用次数可以考虑在进入内核态系统调用异常处理函数之后,进入具体系统调用函数之前维护。
- 阅读 TaskManager 的实现,思考如何维护内核控制块信息(可以在控制块可变部分加入需要的信息)。
实验要求
+++++++++++++++++++++++++++++++++++++++++
- 完成分支: ch3。
- 实验目录要求
.. code-block::
├── os(内核实现)
│   ├── Cargo.toml(配置文件)
│   └── src(所有内核的源代码放在 os/src 目录下)
│   ├── main.rs(内核主函数)
│   └── ...
├── reports (不是 report)
│   ├── lab1.md/pdf
│   └── ...
├── ...
- 通过所有测例:
CI 使用的测例与本地相同测试中user 文件夹及其它与构建相关的文件将被替换,请不要试图依靠硬编码通过测试。
默认情况下makefile 仅编译基础测例 (``BASE=1``),即无需修改框架即可正常运行的测例。
你需要在编译时指定 ``BASE=0`` 控制框架仅编译实验测例(在 os 目录执行 ``make run BASE=0``
或指定 ``BASE=2`` 控制框架同时编译基础测例和实验测例。
.. note::
你的实现只需且必须通过测例,建议读者感到困惑时先检查测例。
简答作业
--------------------------------------------
1. 正确进入 U 态后,程序的特征还应有:使用 S 态特权指令,访问 S 态寄存器后会报错。
请同学们可以自行测试这些内容 (运行 `Rust 三个 bad 测例 (ch2b_bad_*.rs) <https://github.com/LearningOS/rCore-Tutorial-Test-2022S/tree/master/src/bin>`_
注意在编译时至少需要指定 ``LOG=ERROR`` 才能观察到内核的报错信息)
描述程序出错行为,同时注意注明你使用的 sbi 及其版本。
2. 深入理解 `trap.S <https://github.com/LearningOS/rCore-Tutorial-Code-2022S/blob/ch3/os/src/trap/trap.S>`_
中两个函数 ``__alltraps````__restore`` 的作用,并回答如下问题:
1. L40刚进入 ``__restore`` 时,``a0`` 代表了什么值。请指出 ``__restore`` 的两种使用情景。
2. L46-L51这几行汇编代码特殊处理了哪些寄存器这些寄存器的的值对于进入用户态有何意义请分别解释。
.. code-block:: riscv
ld t0, 32*8(sp)
ld t1, 33*8(sp)
ld t2, 2*8(sp)
csrw sstatus, t0
csrw sepc, t1
csrw sscratch, t2
3. L53-L59为何跳过了 ``x2````x4``
.. code-block:: riscv
ld x1, 1*8(sp)
ld x3, 3*8(sp)
.set n, 5
.rept 27
LOAD_GP %n
.set n, n+1
.endr
4. L63该指令之后``sp````sscratch`` 中的值分别有什么意义?
.. code-block:: riscv
csrrw sp, sscratch, sp
5. ``__restore``:中发生状态切换在哪一条指令?为何该指令执行之后会进入用户态?
6. L13该指令之后``sp````sscratch`` 中的值分别有什么意义?
.. code-block:: riscv
csrrw sp, sscratch, sp
7. 从 U 态进入 S 态是哪一条指令发生的?
报告要求
-------------------------------
- 简单总结你实现的功能200字以内不要贴代码
- 完成问答题。
- (optional) 你对本次实验设计及难度/工作量的看法,以及有哪些需要改进的地方,欢迎畅所欲言。

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -0,0 +1,14 @@
.. _link-chapter3:
第三章:多道程序与分时多任务
==============================================
.. toctree::
:maxdepth: 4
0intro
1multi-loader
2task-switching
3multiprogramming
4time-sharing-system
5exercise

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB