Unikernel组件之axtask

众多多进程/多线程功能,如spawn等,都需要底层支持,所以便有了axtask。

通用框架

alt text

一个任务调度框架,要有:

  1. 对上层的接口支持
  2. 当前任务
  3. 任务队列(包括就绪队列和各种等待队列)
  4. 调度器 (有各种调度算法)

为此,在代码实现上,要有如下的类:

  1. 任务控制块类
    表示一个任务,用于保存任务的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
    28
    pub struct TaskInner {
    id: TaskId,
    name: String,
    is_idle: bool,
    is_init: bool,

    entry: Option<*mut dyn FnOnce()>,
    state: AtomicU8,

    in_wait_queue: AtomicBool,
    #[cfg(feature = "irq")]
    in_timer_list: AtomicBool,

    #[cfg(feature = "preempt")]
    need_resched: AtomicBool,
    #[cfg(feature = "preempt")]
    preempt_disable_count: AtomicUsize,

    exit_code: AtomicI32,
    wait_for_exit: WaitQueue,

    kstack: Option<TaskStack>,
    ctx: UnsafeCell<TaskContext>,
    task_ext: AxTaskExt,

    #[cfg(feature = "tls")]
    tls: TlsArea,
    }
  2. 队列类

  3. 调度器类 可灵活配置各种调度策略,如FIFO

核心流程

axtask模块初始化

  1. 创建一个空闲任务IDLE,该任务会不断yield自己,在实在没有任务运行时会用到。
  2. 创建一个任务并任命为当前任务(栈怎么办?)
  3. 创建一个就绪队列
    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());
    }

调度

  1. 把当前任务设置为READY
  2. 将当前任务放入就绪队列
  3. 选出下一个任务
  4. 上下文转换
    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变量的引用。

  1. 刚进入该函数时,实际上执行了call context_switch汇编指令, 这等价于将当前RIP指令寄存器中的值push到当前的栈中,即旧任务的栈中;
  2. 保存当前寄存器到旧任务的栈中;
  3. rsp寄存器的值放到旧任务的rsp变量中
  4. 新任务的rsp变量放到rsp寄存器中,实现了换栈(此时,我们可以想象这个新任务的栈中结构如同现在的旧任务)
  5. 加载新栈中的数据到寄存器中
  6. 执行ret,此时会查找新栈顶的数据,弹出并计算下一个指令地址,作为RIP的新值。而此时这个数据,正是之前新任务中断时保留的那个“当前指令地址”

由此,开始执行新任务,上下文转换。同时,也可以发现,当旧任务再次被调度时,会回到调用context_switch的“下一个语句”,经过层层回溯,离开调度模块,重新执行其应用代码。当然了,如果涉及应用和内核分离,可能又会有更复杂的流程,这个过程可能也没有这么“丝滑”了。

这么看来,在这个框架下,当某个任务执行了context_switch,不过像是执行了一个耗时稍微久了一点但是啥也没干的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// oscamp/arceos/modules/axhal/src/arch/x86_64/context.rs
unsafe extern "C" fn context_switch(_current_stack: &mut u64, _next_stack: &u64) {
asm!(
"
push rbp
push rbx
push r12
push r13
push r14
push r15
mov [rdi], rsp // rdi寄存第一个参数

mov rsp, [rsi]
pop r15
pop r14
pop r13
pop r12
pop rbx
pop rbp
ret",
options(noreturn),
)
}

riscv

在TaskContext里面直接把各个寄存器显式地表示了,大致流程和上面也差不多。

未完待续