第二节 初始任务 - MainTask

内核从启动开始的主执行流程,将来也会作为一个任务参与多任务的调度,我们称它为 MainTask。这个初始任务有它的特殊性,无论是初始化还是调度方面,都需要特殊处理。后续我们将要创建的那些任务,都是 MainTask 的分支和子任务,需要受到它的管理。这一节我们先来建立起 MainTask 的运行框架,为后面创建更多的任务做一个准备。

完整的 MainTask 的运行过程如下,主要关注三个阶段:封装 MainTask、尝试调度执行 yield 和 exit 退出系统。

初始任务InitTask

封装 MainTask

内核启动之后,以单线程方式执行一系列的初始化步骤,直至初始化多任务这一步。此时需要把当前这个执行流程本身也封装为一个任务 MainTask,以便将来与其它任务一起接受调度。MainTask 的特殊之处在于:首先它当前就正处于运行状态,所以初始 status 就是 Running 而非 Ready,并且它在初始化后直接就是 CurrentTask;此外,它的 entry 无意义置空,kstack 也不必创建,因为已经存在,就是内核栈。封装构建 MainTask 的代码在 axtask/src/run_queue.rs 的 init(...) 中:

pub(crate) fn init() {
	let main_task = Task::new_init("main".into());
	main_task.set_state(TaskState::Running);
	unsafe { CurrentTask::init_current(main_task) }
}

构造 main_task 实例后,直接设置 state 是 Running,然后让 CurrentTask 指向该任务。

看一下 MainTask 的初始化方法:

impl Task {
	pub(crate) fn new_init(name: String) -> AxTaskRef {
        let mut t = Self::new_common(TaskId::new(), name);
        t.is_init = true;
        Arc::new(t)
    }

    fn new_common(id: TaskId, name: String) -> Self {
        Self {
            name,
            entry: None,
            state: AtomicU8::new(TaskState::Ready as u8),
            kstack: None,
            ctx: UnsafeCell::new(TaskContext::new()),
        }
    }
    
    #[inline]
    pub(crate) fn set_state(&self, state: TaskState) {
        self.state.store(state as u8, Ordering::Release)
    }
}

CurrentTask::init_current 用于指明当前任务,实际是调用 axhal 的实现【记录在 tp 中】:

// axtask/src/task.rs
impl CurrentTask {
    pub(crate) unsafe fn init_current(init_task: AxTaskRef) {
        let ptr = Arc::into_raw(init_task);
        axhal::cpu::set_current_task_ptr(ptr);
    }
    
    pub(crate) fn try_get() -> Option<Self> {
        let ptr: *const Task = axhal::cpu::current_task_ptr();
        if !ptr.is_null() {
            Some(Self(unsafe { ManuallyDrop::new(AxTaskRef::from_raw(ptr)) }))
        } else {
            None
        }
    }

    pub(crate) fn get() -> Self {
        Self::try_get().expect("current task is uninitialized")
    }
}

pub fn current() -> CurrentTask {
    CurrentTask::get()
}

// axhal/src/riscv64/cpu.rs
static mut CURRENT_TASK_PTR: usize = 0;

#[inline]
pub fn current_task_ptr<T>() -> *const T {
    unsafe { CURRENT_TASK_PTR as _ }
}

#[inline]
pub unsafe fn set_current_task_ptr<T>(ptr: *const T) {
    CURRENT_TASK_PTR = ptr as usize
}

有两个系列的函数,由 axtask 和 axhal 共同实现。一组是全局函数 current() 及其支撑,用于返回当前正在执行的任务指针;另一组是初始化或设置当前任务的函数。它们在后面的调度中,都会起到重要作用。

下面来考虑一下调度的问题,除了 Task 之外,调度还涉及两个数据结构,当前任务指针 CurrentTask 和任务运行队列 run_queue。

调度原理与概念

如图,我们内核中任务的调度,常称为 resched,主要要解决的问题就是:确定谁是下一个执行任务,这是通过在 CurrentTask 和 run_queue 之间交换任务来实现。

通过前面提到的 current() 函数可以获得 CurrentTask,它指向当前任务。在调度时,必须完整的执行如下两步:

第一步 Put - 把当前任务放回到运行队列 run_queue 中。

第二步 Get - 从运行队列 run_queue 中取出一个任务作为当前任务。

注意:一定是先把当前任务放回运行队列,然后再从队列中选出下一个任务。这意味着,当前任务也会与运行队列中的其它任务一起平等的参与竞争,基于调度策略选出下一个,所以选择结果仍有可能是它。

完成上述两步之后,可能出现 3 种结果:

  1. 当前任务变成了另外一个。这是最常见的情况,说明调度生效了,CPU 开始执行目前更有资格或更紧迫的任务。至于谁更有资格或更紧迫取决于调度的策略。
  2. 当前任务还是调度前的那个任务,相当于调度没有生效。这可能是因为 run_queue 是空的,也有可能在所有待命的任务中,原任务依然是最有资格执行的那个。
  3. 上面第二步 Get 取不到下一个可执行的任务。对于此情况,内核中预设一个名为 IDLE 的特殊的内部任务,充当下一个任务,它名副其实的什么都不做,一旦有其它有效的任务可以运行了,它就会立即让出。本节不涉及它,我们将在本章的最后,说明它的实现。

由于我们目前只有一个任务 MainTask,如果触发调度,将出现上面第 2 种结果,即调度后仍然是 MainTask。

下面来实现 yield api 和背后的 resched 过程,首先是运行队列 run_queue 的实现:

pub(crate) static RUN_QUEUE: SpinNoIrq<AxRunQueue> = SpinNoIrq::new(AxRunQueue::new());

pub(crate) struct AxRunQueue {
    ready_queue: VecDeque<Arc<Task>>,
}

impl AxRunQueue {
    pub fn yield_current(&mut self) {
        self.resched();
    }
    
    fn resched(&mut self) {
        let prev = current();
        if prev.is_running() {
            prev.set_state(TaskState::Ready);
            if !prev.is_idle() {
                self.put_prev_task(prev.clone());
            }
        }
        let next = self.pick_next_task().unwrap(); // FixMe: with IDLE_TASK.get().clone()
        self.switch_to(prev, next);
    }
    
    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;
        }
		todo!("Implement it in future!");
    }

    pub fn pick_next_task(&mut self) -> Option<Arc<Task>> {
        self.ready_queue.pop_front()
    }

    pub fn put_prev_task(&mut self, prev: Arc<Task>, preempt: bool) {
        self.ready_queue.push_back(prev)
    }
}

运行队列采取 VecDeque 的数据结构,可以方便的在两头进行 push/pop。

注意 yield_current,其实就是调用 resched(),实现逻辑已经在上面交代过了,最后是对前后两个任务执行 switch_to 切换。

关于 switch_to(...) 函数,这个是整个调度机制的核心,本节的实现是不完整的,但是那句 todo! 不会被执行到,因为目前我们只有一个任务 MainTask,是在同一个任务上切换,从上面那行 return 就返回了。下一节我们来完成这个 switch_to(...) 函数。

然后,把 yield 的功能通过 axstd 暴露给应用:

// axstd/src/thread.rs
pub fn yield_now() {
    axtask::yield_now();
}

// axtask/src/lib.rs
pub fn yield_now() {
    run_queue::RUN_QUEUE.lock().yield_current();
}

最后,我们为 MainTask 实现 exit:

// axtask/src/lib.rs
pub fn exit(exit_code: i32) -> ! {
    run_queue::RUN_QUEUE.lock().exit_current(exit_code)
}

// axtask/src/run_queue.rs
impl AxRunQueue {
	pub fn exit_current(&mut self, exit_code: i32) -> ! {
		axhal::misc::terminate();
    }
}

对于 MainTask,任务退出就意味着系统退出。但是对于其它任务,则只是自己退出,并等待资源被回收。所以 exit_current(...) 的实现是不完整的,下一节我们再来完成它。

在 axruntime 中,初始化 axtask 组件,以支持多任务:

// axruntime/src/lib.rs
pub extern "C" fn rust_main(hartid: usize, dtb: usize) -> ! {
    ... ...
    axtask::init_scheduler();
    ... ...
    unsafe {
        main();
    }
    axtask::exit(0);
}

pub fn init_scheduler() {
    info!("Initialize scheduling...");
    run_queue::init();
}

现在来运行一下,从运行效果来看,与启用多任务之前没有区别,毕竟目前也只有一个任务;但是在形式上,我们已经完成了多任务框架的支持,为并发计算与按策略调度做好了准备。

下一节,我们将通过 spawn 这个 API 创建一个新的任务,完善调度机制,与初始任务形成真正的并发关系。