第一节 基本概念 - 任务与调度

在现代操作系统中,多任务支持是基本功能。对我们的内核来说,所支撑的应用可以开启多任务,同时执行多条相对独立的逻辑流程;内核内部同样可以开启多任务,并发完成一些维护性的工作。无论在哪一个层面,合理的多任务调度都可以带来整体效率上的提升。

在多任务方面,任务与调度是两个核心的概念。

任务是被调度的对象,它具有独立的工作逻辑。

调度是资源不足时,协调每个请求对资源使用的方法。通常,每个任务都在尽力争取获得更多的计算资源 - 其实就是 CPU 时间,但是 CPU 无论从数量还是算力常常是处于相对不足的状态的,这就需要协调各个任务之间对有限 CPU 资源的分配。协调的好,系统整体效率有保证;协调的不好,系统效率下降甚至卡死。

在 ArceOS 的语境下,任务等价于线程。也就是说,每个任务拥有独立的执行流独立的栈,但是它们没有独立的地址空间,而是共享唯一的内核地址空间。相应的对于调度,我们也需要更多的去参考线程调度方面的历史经验。

任务的核心构成要素包括:执行流、栈、环境上下文和状态。先来说状态,有四个基本的调度状态:

任务状态变迁

调度过程中,需要随时跟踪和更新任务的状态,作为对任务有效管理的基础。

下面以一个典型任务的生命周期为例,说明任务各个构成要素分别的职责作用:

任务生命周期

上图是一个典型任务在整个生命周期过程中,将会(可能会)遇到的几种情况。

  1. 任务首次被创建时,status 默认是 Ready 状态,entry 为任务执行的入口,创建单独的由 kstack 指向的栈,还需要预留一块空间作为任务上下文 context,用于在任务被调度出去时,保存该任务的运行环境,以便在调度回来时恢复现场。
  2. 任务被调度运行,status 改为 Running 状态,有一个特殊的指针 CurrentTask 指向它,表示它就是当前正在被执行的任务。任务一旦启动之后,entry 就不再有用,寄存器 pc 始终指向 CPU 当前正在执行的指令,寄存器 sp 则指向 kstack 栈的栈顶位置。
  3. 任务在运行过程中可能随时被调度出去,使得 CPU 可以执行其它任务。调度出去的原因有两种:一是它必须等待某种资源可用,此时任务被设置为 Blocked 状态,让出执行权;二是它主动(调用 yield)或被动(时间片耗尽)让出执行权。无论何种原因,当前任务的上下文必须被首先被保存,即保护现场,等到该任务再次被调度时,把上下文再恢复出来,让任务从上次的断点处继续运行。后面我们将会看到,所谓任务的上下文,就是一组维持任务运行状态的必要的寄存器。
  4. 任务在运行中,还会遇到另外一种涉及运行环境保存与恢复的情况,即触发异常或遇到外部中断时。此时系统应该挂起当前任务,转而去执行异常/中断的例程,处理完成后再回到任务断点处恢复执行。这期间内核处于一个特殊的运行上下文中,我们称为异常中断上下文,它会叠加到当前任务的任务上下文之上。具体来说,当前任务的一组寄存器状态会作为 TrapFrame 被保存在任务的栈上。
  5. 任务运行完成后,它的某些资源不能被自己回收,所以内核安排了一个内部任务 GcTask,专门负责进行此项工作。

XXX

根据上面的说明,我们先把任务相关的结构定义出来,首先是 Task 和 CurrentTask 定义在 axtask/src/task.rs:

#[repr(u8)]
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub(crate) enum TaskState {
    Running = 1,
    Ready = 2,
    Blocked = 3,
    Exited = 4,
}

struct TaskStack {
    ptr: NonNull<u8>,
    layout: Layout,
}

pub struct Task {
    entry: Option<*mut dyn FnOnce()>,
    state: AtomicU8,
    kstack: Option<TaskStack>,
    ctx: UnsafeCell<TaskContext>,
}

unsafe impl Send for Task {}
unsafe impl Sync for Task {}

pub struct CurrentTask(ManuallyDrop<AxTaskRef>);

pub fn current() -> CurrentTask {
    CurrentTask::get()
}

而 TaskContext 因为与体系结构相关,定义在 axhal/src/riscv64/context.rs 中:

#[repr(C)]
#[derive(Debug, Default)]
pub struct TaskContext {
    pub ra: usize, // return address (x1)
    pub sp: usize, // stack pointer (x2)
    pub s0: usize, // x8-x9
    pub s1: usize,
    pub s2: usize, // x18-x27
    pub s3: usize,
    pub s4: usize,
    pub s5: usize,
    pub s6: usize,
    pub s7: usize,
    pub s8: usize,
    pub s9: usize,
    pub s10: usize,
    pub s11: usize,
}

XXX