任务调度
NUDT-OS是分时多任务系统,目前的实现采用简单的时间片轮转调度算法,用户线程有两种时机将会被调度:(1)时间片用完,由内核中断处理程序调度 (2)线程主动放弃CPU,这种情况发生在线程运行结束或者阻塞资源时。
线程是CPU调度的最小单位,NUDT-OS中存在用户线程和内核线程两种线程,内核线程为用户线程提供服务,理论上应该具有更高的优先级。内核必须统一调度内核线程和用户线程两种线程。目前采用的调度算法是当任意内核服务线程需要提供服务时,都优先执行内核线程,所有内核线程空闲时,执行用户线程。
这一节首先介绍线程上下文切换的代码片段,再给出几个典型线程调度时机。
一、调度代码片段
/// 执行用户态程序
pub fn start_user_loop() {
loop {
if has_ready_kthread() {
// 有内核线程,调度其它内核线程
yield_current_kthread();
continue;
}
// 调度用户线程
if set_current_thread() {
// 运行当前线程
run_current_thread();
// 清理当前线程
clear_current_thread();
continue;
}
}
}
主内核线程统一调度内核线程和用户线程,当任意内核服务线程需要提供服务时,都优先执行内核线程,所有内核线程空闲时,执行用户线程。
.text
.global context_switch
context_switch: # (cur: &mut Context, nxt: &Context)
# Save cur's registers
mov rax, [rsp] # return address
mov [rdi + 56], rax # 56 = offsetof(Context, rip)
mov [rdi + 0], rsp
mov [rdi + 8], rbx
mov [rdi + 16], rbp
mov [rdi + 24], r12
mov [rdi + 32], r13
mov [rdi + 40], r14
mov [rdi + 48], r15
# Restore nxt's registers
mov rsp, [rsi + 0]
mov rbx, [rsi + 8]
mov rbp, [rsi + 16]
mov r12, [rsi + 24]
mov r13, [rsi + 32]
mov r14, [rsi + 40]
mov r15, [rsi + 48]
mov rax, [rsi + 56] # restore return address
mov [rsp], rax # for stack balance, must use mov instead of push
ret
切换内核线程或用户线程时,都将线程的现场保存在自己的Context结构中,取出要切换线程的Context恢复寄存器跳转到响应指令地址去执行。
二、几个调度时机
2.1 时钟中断时调度
#[no_mangle]
pub extern "C" fn trap_handler(f: &'static mut TrapFrame) {
match f.id {
... => {
...
}
// 时钟中断时调度
TIMER => {
pic::ack();
*pic::TICKS.get() += 1;
task::current_yield();
}
...
}
}
采用时间片轮转调度算法,每个时钟中断时调度切换线程。
2.2 阻塞系统资源时主动调度
impl File for Stdin {
...
/// 从串口读取一个字符到buf中
fn read(&self, buf: &mut [u8]) -> usize {
loop {
if let Some(c) = console::receive() {
buf[0] = c as _;
return 1;
} else {
// 当前不能立即读取,则当前线程主动放弃CPU
task::current_yield();
}
}
}
当线程阻塞系统资源时(不能立刻得到资源),主动调度放弃CPU。
2.3 线程退出时主动调度
/// 线程退出
pub fn exit(&mut self, exit_code: i32) -> ! {
...
// 释放所有线程资源
self.exit_code = exit_code;
self.state = ThreadState::Zombie;
THREAD_MANAGER.get().resched();
unreachable!("task exited");
}