众多多进程/多线程功能,如spawn等,都需要底层支持,所以便有了axtask。
通用框架
一个任务调度框架,要有:
- 对上层的接口支持
- 当前任务
- 任务队列(包括就绪队列和各种等待队列)
- 调度器 (有各种调度算法)
为此,在代码实现上,要有如下的类:
任务控制块类
表示一个任务,用于保存任务的id、入口、栈和上下文等。上下文一般专门有一个类,方便调度和上下文切换解耦设计。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28pub struct TaskInner {
id: TaskId,
name: String,
is_idle: bool,
is_init: bool,
entry: Option<*mut dyn FnOnce()>,
state: AtomicU8,
in_wait_queue: AtomicBool,
in_timer_list: AtomicBool,
need_resched: AtomicBool,
preempt_disable_count: AtomicUsize,
exit_code: AtomicI32,
wait_for_exit: WaitQueue,
kstack: Option<TaskStack>,
ctx: UnsafeCell<TaskContext>,
task_ext: AxTaskExt,
tls: TlsArea,
}队列类
调度器类 可灵活配置各种调度策略,如FIFO
核心流程
axtask模块初始化
- 创建一个空闲任务IDLE,该任务会不断yield自己,在实在没有任务运行时会用到。
- 创建一个任务并任命为当前任务(栈怎么办?)
- 创建一个就绪队列
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// oscamp/arceos/modules/axtask/src/run_queue.rs
pub(crate) fn init() {
// Create the `idle` task (not current task).
const IDLE_TASK_STACK_SIZE: usize = 4096;
let idle_task = TaskInner::new(|| crate::run_idle(), "idle".into(), IDLE_TASK_STACK_SIZE);
IDLE_TASK.with_current(|i| {
i.init_once(idle_task.into_arc());
});
// Put the subsequent execution into the `main` task.
let main_task = TaskInner::new_init("main".into()).into_arc();
main_task.set_state(TaskState::Running);
unsafe { CurrentTask::init_current(main_task) };
RUN_QUEUE.init_once(AxRunQueue::new());
}
调度
- 把当前任务设置为READY
- 将当前任务放入就绪队列
- 选出下一个任务
- 上下文转换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// oscamp/arceos/modules/axtask/src/run_queue.rs
fn resched(&mut self, preempt: bool) {
let prev = crate::current();
if prev.is_running() {
prev.set_state(TaskState::Ready);
if !prev.is_idle() {
self.scheduler.put_prev_task(prev.clone(), preempt);
}
}
let next = self.scheduler.pick_next_task().unwrap_or_else(|| unsafe {
// Safety: IRQs must be disabled at this time.
IDLE_TASK.current_ref_raw().get_unchecked().clone()
});
self.switch_to(prev, next);
}
上下文转换
这个是与具体的指令架构相关的
x86
传入的分别是旧任务和新任务的TaskContext中的rsp变量的引用。
- 刚进入该函数时,实际上执行了
call context_switch
汇编指令, 这等价于将当前RIP指令寄存器中的值push到当前的栈中,即旧任务的栈中; - 保存当前寄存器到旧任务的栈中;
- rsp寄存器的值放到旧任务的rsp变量中
- 新任务的rsp变量放到rsp寄存器中,实现了换栈(此时,我们可以想象这个新任务的栈中结构如同现在的旧任务)
- 加载新栈中的数据到寄存器中
- 执行ret,此时会查找新栈顶的数据,弹出并计算下一个指令地址,作为RIP的新值。而此时这个数据,正是之前新任务中断时保留的那个“当前指令地址”
由此,开始执行新任务,上下文转换。同时,也可以发现,当旧任务再次被调度时,会回到调用context_switch的“下一个语句”,经过层层回溯,离开调度模块,重新执行其应用代码。当然了,如果涉及应用和内核分离,可能又会有更复杂的流程,这个过程可能也没有这么“丝滑”了。
这么看来,在这个框架下,当某个任务执行了context_switch,不过像是执行了一个耗时稍微久了一点但是啥也没干的函数。
1 | // oscamp/arceos/modules/axhal/src/arch/x86_64/context.rs |
riscv
在TaskContext里面直接把各个寄存器显式地表示了,大致流程和上面也差不多。