第三节 创建任务 - spawn
前面我们已经让内核的主线程成为第一个任务 MainTask;本节将在 MainTask 的基础上,创建一个应用级的任务。流程如下:
创建新任务 spawn
首先来实现 spawn 新任务的 API,扩展 axstd/thread,如下:
pub fn spawn<T, F>(f: F) -> JoinHandle<T>
where
F: FnOnce() -> T + 'static,
T: 'static,
{
Builder::new().spawn(f).expect("failed to spawn thread")
}
#[derive(Debug)]
pub struct Builder {
// A name for the thread-to-be, for identification in panic messages
name: Option<String>,
// The size of the stack for the spawned thread in bytes
stack_size: Option<usize>,
}
impl Builder {
pub const fn new() -> Builder {
Builder {
name: None,
stack_size: None,
}
}
pub fn spawn<F, T>(self, f: F) -> io::Result<JoinHandle<T>>
where
F: FnOnce() -> T + 'static,
F: 'static,
T: 'static,
{
unsafe { self.spawn_unchecked(f) }
}
unsafe fn spawn_unchecked<F, T>(self, f: F) -> Result<JoinHandle<T>>
where
F: FnOnce() -> T + 'static,
F: 'static,
T: 'static,
{
let name = self.name.unwrap_or_default();
let stack_size = self
.stack_size
.unwrap_or(axconfig::TASK_STACK_SIZE);
let my_packet = Arc::new(Packet {
result: UnsafeCell::new(None),
});
let their_packet = my_packet.clone();
let main = move || {
let ret = f();
unsafe { *their_packet.result.get() = Some(ret) };
drop(their_packet);
};
let inner = axtask::spawn_raw(main, name, stack_size);
let task = AxTaskHandle {
id: inner.id().as_u64(),
inner,
};
Ok(JoinHandle {
thread: Thread::from_id(task.id),
native: task,
packet: my_packet,
})
}
}
API 这一层负责组织好三个参数,任务的入口、名称和栈大小,然后调用 axtask::spawn_raw(...) 进行实际的创建过程,最后以 JoinHandle 的形式返回任务的句柄和返回值 packet。
关键是 axtask::spawn_raw 的实现:
// axtask/src/lib.rs
pub fn spawn_raw<F>(f: F, name: String, stack_size: usize) -> AxTaskRef
where
F: FnOnce() + 'static,
{
let task = task::Task::new(f, name, stack_size);
run_queue::RUN_QUEUE.lock().add_task(task.clone());
task
}
// axtask/src/task.rs
impl Task {
pub(crate) fn new<F>(entry: F, name: String, stack_size: usize) -> AxTaskRef
where
F: FnOnce() + 'static,
{
let mut t = Self::new_common(TaskId::new(), name);
let kstack = TaskStack::alloc(align_up(stack_size, PAGE_SIZE));
t.entry = Some(Box::into_raw(Box::new(entry)));
t.ctx.get_mut().init(task_entry as usize, kstack.top());
t.kstack = Some(kstack);
Arc::new(t)
}
}
extern "C" fn task_entry() -> ! {
let task = current();
if let Some(entry) = task.entry {
unsafe { Box::from_raw(entry)() };
}
crate::exit(0);
}
// axtask/src/run_queue.rs
impl AxRunQueue {
pub fn add_task(&mut self, task: AxTaskRef) {
self.ready_queue.push_back(task);
}
}
spawn_raw 包括两步,第一步是创建新任务,第二步则是简单的把它加到 run_queue 队列中等待调度。
关键是创建新任务这一步,与上一节 MainTask 的初始化不同,这次任务的 entry 是应用的计算逻辑的入口,同时需要创建一个自己的栈。然后注意上下文 ctx 的初始化,我们把任务的入口 task_entry 和新建栈的栈顶位置填充进去,这是下面调度到该任务时,任务能够从正确的位置执行应用逻辑的关键。
让出执行权 yield - 触发调度
下面就来看一下对新建任务首次调度的过程。
上一节我们已经建立了从 yield 和它背后 resched 的基本过程,但这次情况不同,运行队列 run_queue 中有了一个处于 Ready 状态的可调度任务,因此 switch_to 函数会发生一次真正的任务切换。
impl AxRunQueue {
fn switch_to(&mut self, prev_task: CurrentTask, next_task: AxTaskRef) {
next_task.set_preempt_pending(false);
next_task.set_state(TaskState::Running);
if prev_task.ptr_eq(&next_task) {
return;
}
// Just switch between two tasks.
unsafe {
let prev_ctx_ptr = prev_task.ctx_mut_ptr();
let next_ctx_ptr = next_task.ctx_mut_ptr();
CurrentTask::set_current(prev_task, next_task);
(*prev_ctx_ptr).switch_to(&*next_ctx_ptr);
}
}
}
首先把下一个任务 - 即我们新建的应用任务,设置为 CurrentTask,然后在 axhal 中进行实际的上下文切换:
// axhal/src/riscv64/context.rs
impl TaskContext {
pub const fn new() -> Self {
unsafe { core::mem::MaybeUninit::zeroed().assume_init() }
}
pub fn init(&mut self, entry: usize, kstack_top: usize) {
self.sp = kstack_top;
self.ra = entry;
}
pub fn switch_to(&mut self, next_ctx: &Self) {
unsafe { context_switch(self, next_ctx) }
}
}
#[naked]
unsafe extern "C" fn context_switch(_current_task: &mut TaskContext, _next_task: &TaskContext) {
asm!("
// save old context (callee-saved registers)
sd ra, 0*8(a0)
sd sp, 1*8(a0)
sd s0, 2*8(a0)
sd s1, 3*8(a0)
sd s2, 4*8(a0)
sd s3, 5*8(a0)
sd s4, 6*8(a0)
sd s5, 7*8(a0)
sd s6, 8*8(a0)
sd s7, 9*8(a0)
sd s8, 10*8(a0)
sd s9, 11*8(a0)
sd s10, 12*8(a0)
sd s11, 13*8(a0)
// restore new context
ld s11, 13*8(a1)
ld s10, 12*8(a1)
ld s9, 11*8(a1)
ld s8, 10*8(a1)
ld s7, 9*8(a1)
ld s6, 8*8(a1)
ld s5, 7*8(a1)
ld s4, 6*8(a1)
ld s3, 5*8(a1)
ld s2, 4*8(a1)
ld s1, 3*8(a1)
ld s0, 2*8(a1)
ld sp, 1*8(a1)
ld ra, 0*8(a1)
ret",
options(noreturn),
)
}
所谓上下文,就是能够保持任务状态的一组寄存器,从 context_switch 可以看出,寄存器组包括 ra、sp 和 s0~s11,处理过程就是把当前这组寄存器的值保存到上一个任务的 ctx 上下文中,然后用下一个任务的 ctx 上下文中保存的值来恢复对应的寄存器。
其中,ra 寄存器是函数返回后要执行的下一条指令地址,对它进行切换的效果:context_switch 返回后竟然不是返回到原任务执行流,而是返回到另一个执行流中;sp 寄存器指向栈,它保持了函数压栈的信息,所以在执行流切换的同时,栈也必须切换;s0~s11 是按照 RiscV 规范必须由被调用者负责保存的寄存器,因此一并放到上下文中随任务切换。context_switch 的执行效果如下:

从图中可见,context_switch(...) 是一个非常特殊的函数,当前任务进入函数后被挂起;等函数返回时,继续执行的往往是另一个任务。
现在已经支持了任务调度的完整过程,内核从 MainTask 切换到了新建的 AppTask。AppTask 完成自己的工作之后,将会通过 exit 退出。我们在前面实现 task_entry(...)
时,最后一行就是 exit
,所以应用任务不必显式调用它。下面来完善 exit 的实现。
应用任务退出 - exit
上一节实现了 exit 的框架,不过只是针对 MainTask 的情况。对于应用任务 AppTask,它退出时不会导致系统退出,只是通知系统对其资源进行回收。因此我们需要对 exit 进行分支处理:
// axtask/src/run_queue.rs
impl AxRunQueue {
pub fn exit_current(&mut self, exit_code: i32) -> ! {
let curr = current();
if curr.is_init() {
axhal::misc::terminate();
} else {
curr.set_state(TaskState::Exited);
// Save exit code and notify kernel to reclaim itself.
self.resched(false);
}
unreachable!("task exited!");
}
}
任务 Task 的成员 is_init 标记自己是否就是 MainTask。对于普通的 AppTask,先设置 Exited 状态,然后记录退出码和通知内核回收,最后触发一次 resched 调度,把执行权再切换出去,它的使命已经完成,就等着被回收了。中间那一步“记录退出码和通知内核回收”暂缺,我们到下一节再补充相关的机制。
主任务 MainTask 调用 join 等待 AppTask 退出
这一步与 AppTask 的 exit 退出相对应,下一节专门讨论。
先来验证一下我们目前的成果:
执行成功!