第一节 任务调度中的抢占

抢占是操作系统调度方面的一个基本概念,通常是指,高优先级的任务可以抢占正在运行的低优先级任务的执行权。但是在各种操作系统设计的具体实践上,它们的具体策略、具体设计与实现方式存在差异。这一节,先来澄清 ArceOS 中,任务抢占采取的具体策略与方式。这个抢占机制有以下几个特点:

  1. 抢占是有条件的,并且包括内部条件和外部条件,二者同时具备时,才能触发抢占。内部条件指的是,在任务内部维护的某种状态达到条件,例如本次运行的时间片配额耗尽;外部条件指的是,内核可以在某些阶段,暂时关闭抢占,比如,下步我们的自旋锁就需要在加锁期间关闭抢占,以保证锁范围的原子性。由此可见,这个抢占是兼顾了任务自身状况的,一个正在运行的任务即使是低优先级,在达到内部条件之前,也不会被其它任务抢占。这与典型的硬实时操作系统的抢占就有着明显的区别。
  2. 抢占是边沿触发。在内部条件符合的前提下,外部状态从禁止抢占到启用抢占的那个变迁点,会触发一次抢占式重调度 resched。
抢占的内外部条件

内部条件涉及任务结构的升级和具体策略,这里我们采取一个最简单的调度策略 - Round-Robin:为每个任务分配相同数量的时间片配额,当前任务耗尽本次配额后可以被抢占,它被追加到运行队列的末尾,以此类推,形成一个环形的调度序列,每个任务都能获得近似相等的计算资源。先来看一下任务的数据结构:

// axtask/src/task.rs
pub struct Task {
	... ...
    need_resched: AtomicBool,
    time_slice: AtomicIsize,
}

impl Task {
    const MAX_TIME_SLICE: isize = 5;

    fn new_common(id: TaskId, name: String) -> Self {
        Self {
            ... ...
            need_resched: AtomicBool::new(false),
            time_slice: AtomicIsize::new(Self::MAX_TIME_SLICE),
        }
    }

    pub fn reset_time_slice(&self) {
        self.time_slice.store(Self::MAX_TIME_SLICE, Ordering::Release);
    }
    pub fn task_tick(&self) -> bool {
        let old_slice = self.time_slice.fetch_sub(1, Ordering::Release);
        old_slice <= 1
    }
    
    pub(crate) fn set_preempt_pending(&self, pending: bool) {
        self.need_resched.store(pending, Ordering::Release)
    }
}

在任务 Task 中增加一个 time_slice 成员用于记录时间片的消耗情况,初始化为 MAX_TIME_SLICE。实现一个关键的方法 task_tick 用于维护任务的内部条件:每次时钟中断时,将自动调用此方法对时间片 time_slice 减 1,当减到 0 时,方法返回 true 表示任务内部状态满足了抢占条件。