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,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 上下文地址。