第一节 任务调度中的抢占
抢占是操作系统调度方面的一个基本概念,通常是指,高优先级的任务可以抢占正在运行的低优先级任务的执行权。但是在各种操作系统设计的具体实践上,它们的具体策略、具体设计与实现方式存在差异。这一节,先来澄清 ArceOS 中,任务抢占采取的具体策略与方式。这个抢占机制有以下几个特点:
- 抢占是有条件的,并且包括内部条件和外部条件,二者同时具备时,才能触发抢占。内部条件指的是,在任务内部维护的某种状态达到条件,例如本次运行的时间片配额耗尽;外部条件指的是,内核可以在某些阶段,暂时关闭抢占,比如,下步我们的自旋锁就需要在加锁期间关闭抢占,以保证锁范围的原子性。由此可见,这个抢占是兼顾了任务自身状况的,一个正在运行的任务即使是低优先级,在达到内部条件之前,也不会被其它任务抢占。这与典型的硬实时操作系统的抢占就有着明显的区别。
- 抢占是边沿触发。在内部条件符合的前提下,外部状态从禁止抢占到启用抢占的那个变迁点,会触发一次抢占式重调度 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 表示任务内部状态满足了抢占条件。