第三节 创建任务 - spawn

前面我们已经让内核的主线程成为第一个任务 MainTask;本节将在 MainTask 的基础上,创建一个应用级的任务。流程如下:

maintask-create-apptask

创建新任务 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 退出相对应,下一节专门讨论。

先来验证一下我们目前的成果:

执行成功!