用户线程管理
线程是OS进行调度的最小单位,每个线程都有自己的指令执行流。NUDT-OS使用Thread结构体表示用户线程(其实内核中存在也存在一个thread实例用于回收僵死进程)。每个用户线程都有自己的内核栈,发生中断异常或系统调用时,线程将上下文保存在自己的内核栈上,然后调用中断/系统调用处理程序,返回内核态前从内核栈中恢复寄存器和rip。
一、数据结构
// kernel/src/task/thread.rs
pub enum ThreadState {
/// 可运行
Runnable,
/// 阻塞,等待被唤醒
Blocking,
/// 异步等待
Waiting,
/// 已退出,但尚不能回收
Zombie,
/// 已退出, 可以被回收
Waited,
}
线程具有五种状态,状态转移图如下所示:
异步等待状态发生在异步系统调用时,用户线程使用系统调用向内核服务线程请求服务时,内核主线程将会创建一个协程将其添加到协程执行线程中,用户线程转为异步等待状态,当内核服务线程完成服务时,唤醒用户线程,转为可运行态。
/// 线程抽象
#[repr(C)]
pub struct Thread {
/// 对齐
_align: ThreadAlign,
/// 线程id
pub tid: usize,
/// 线程所属进程的指针
pub proc: ProcPtr,
/// 线程状态
pub state: ThreadState,
/// 结束后的退出码
pub exit_code: i32,
/// 线程执行上下文
pub ctx: Context,
/// 线程独立内核栈
kstack: [u8; THREAD_SIZE - size_of::<usize>() * 3 - size_of::<Context>()],
}
每个用户线程都有自己的内核栈,发生中断异常或系统调用时,线程将上下文保存在自己的内核栈上,然后调用中断/系统调用处理程序,返回内核态前从内核栈中恢复寄存器和rip。ctx用于在内核态进行线程切换时保存线程切换前一时刻的内核栈顶位置rsp、指令寄存器rip和被调用者保存的寄存器现场。发生线程调度时,取出下一个就绪线程,从其ctx恢复寄存器现场并执行。
二、关键方法
线程的关键方法包括创建一个新线程new(),线程退出exit()以及从线程自己的内核栈获取用户态上下文syscall_frame(),下面给出介绍
2.1 new()
// kernel/src/task/thread.rs
// impl Thread
/// 创建一个新的线程
///
/// 返回(线程指针,是否需要栈)
pub fn new(proc: &mut Process, entry: fn(usize) -> usize, arg: usize) -> (ThreadPtr, bool) {
// 线程入口函数
fn kernel_thread_entry() -> ! {
let cur = current();
let entry: fn(usize) -> usize = unsafe { transmute(cur.ctx.regs.rbx) };
let arg = cur.ctx.regs.rbp;
let ret = entry(arg);
// 若是用户态线程,则不会执行下面的exit,需要手动在线程函数中exit
cur.exit(ret as _);
}
let (t, need_stack);
unsafe {
let mut it = proc.threads.iter_mut();
// 寻找当前进程是否有已退出可回收的线程
loop {
// 有可回收线程,不需要栈
if let Some(t1) = it.next() {
if t1.state == ThreadState::Waited {
t = transmute(t1);
need_stack = false;
break;
}
// 遍历结束,没有Waited状态的线程,需要栈
} else {
let mut t1 = Box::<Thread>::new_uninit();
t = &mut *t1.as_mut_ptr();
t.tid = proc.threads.len();
proc.threads.push(transmute(t1));
need_stack = true;
break;
}
}
// 将新线程加入就绪线程队列
THREAD_MANAGER.get().enqueue(&mut *(t as *mut _));
t.proc = &mut *(proc as *mut _);
}
t.state = ThreadState::Runnable;
t.ctx.rip = kernel_thread_entry as _;
t.ctx.regs.rsp =
t.kstack.as_ptr_range().end as usize - size_of::<usize>() - size_of::<SyscallFrame>();
t.ctx.regs.rbx = entry as _;
t.ctx.regs.rbp = arg;
(t, need_stack)
}
创建线程可以分为三个主要部分:
-
总是先寻找进程是否存在waited状态(已被回收但还未释放资源)的线程,若存在,则不需要为其分配用户栈空间(使用之前分配的空间),否则需要为其分配独立的用户栈空间。
-
总是将线程(内核主线程和所有用户线程)的入口函数设置为kernel_thread_entry(),这是一个封装,它从线程上下文的几个寄存器中读出线程真正的执行函数和参数并执行,然后调用exit()退出线程。
-
设置线程内核栈栈顶,下图为线程内核栈布局图:
利用new()方法,即可实现创建线程sys_thread_create系统调用。可见若需要栈则为其分配栈空间,并初始线程进入用户态时的上下文(初始化syscall_frame),最终线程通过系统调用处理函数的返回代码片段(参加系统调用)syscall_return返回用户态,从syscall_frame中取出上下文并开始执行。
// src/task/thread.rs
// impl Thread
/// 创建一个线程,若需要则为其分配栈空间
///
///返回线程tid
pub fn sys_thread_create(entry: usize, arg: usize) -> isize {
let t = current();
let (t1, need_stack) = Thread::new(t.proc, user_task_entry, 0);
let stack = USTACK_TOP - t1.tid * USTACK_SIZE;
// 为线程分配栈空间
if need_stack {
t.proc.vm.as_mut().unwrap().insert(MapArea::new(
VirtAddr(stack - USTACK_SIZE),
USTACK_SIZE,
PageTableFlags::PRESENT | PageTableFlags::WRITABLE | PageTableFlags::USER_ACCESSIBLE,
));
}
let f = t1.syscall_frame();
f.caller.rcx = entry;
f.caller.r11 = my_x86_64::RFLAGS_IF;
f.callee.rsp = stack;
f.caller.rdi = arg;
t1.tid as _
}
2.2 exit()
// src/task/thread.rs
// impl Thread
/// 线程退出
///
/// 在每个内核线程的入口函数中,执行完线程函数后调用
pub fn exit(&mut self, exit_code: i32) -> ! {
println!(
"[kernel] Process {} Thread {} exited with code {}",
self.proc.pid, self.tid, exit_code
);
// 为根线程,这时释放进程所有线程资源
if self.tid == 0 {
let p = &mut self.proc;
PID2PROC.get().remove(&p.pid).unwrap();
p.vm = None;
p.zombie = true;
p.exit_code = exit_code;
for ch in &mut p.children {
root_proc().add_child(ch);
}
p.children.clear();
for t in &mut p.threads {
t.state = ThreadState::Zombie;
}
THREAD_MANAGER.get().clear_zombie();
// 清理除了0号线程的所有线程. 内核代码在0号进程中,故不能清理
p.threads.drain(1..);
p.files.clear();
}
self.exit_code = exit_code;
self.state = ThreadState::Zombie;
THREAD_MANAGER.get().resched();
unreachable!("task exited");
}
-
exit函数接受线程的tid,只有当tid为0(主线程)时,其回收进程中所有线程(除了0号线程)的资源,并将进程的zombie标志置为true,但当前进程可能还有未终止的子进程,这时将其所有子进程的父进程都设置为内核中的root进程,root进程通过无限循环waitpid(-1)回收所有终止的子进程。
-
若tid不为0,则只是将这个线程状态设置为Zombie(已终止但未被回收),当主线程使用waittid系统调用时,其状态变为waited(已回收可复用),则可以在new中被重复利用了。
// syscall/task.rs
/// 等待一个线程结束
///
/// 若回收自己,返回-1
///
/// 若未能回收符合的线程,返回-2
pub fn sys_waittid(tid: usize) -> isize {
let t = current();
// 线程不能回收自己
if t.tid == tid {
return -1;
}
let t1 = try_!(t.proc.threads.get_mut(tid), -1);
if t1.state == ThreadState::Zombie {
t1.state = ThreadState::Waited;
t1.exit_code as _
} else {
-2
}
}
2.3 syscall_frame()
// src/task/thread.rs
// impl Thread
/// 从当前线程栈最高地址获取SyscallFrame
pub fn syscall_frame(&mut self) -> &mut SyscallFrame {
unsafe { &mut *(self.kstack.as_ptr_range().end as *mut SyscallFrame).sub(1) }
}
syscallframe函数简单地从内核栈最高地址往下取出数据并解析为SyscallFrame结构体,其即为线程用户态上下文。
三、用户程序多线程示例
// user/src/bin/thread.rs
pub fn thread_a() -> ! {
for _ in 0..1000 {
print!("a");
}
exit(1)
}
pub fn thread_b() -> ! {
for _ in 0..1000 {
print!("b");
}
exit(2)
}
pub fn thread_c() -> ! {
for _ in 0..1000 {
print!("c");
}
exit(3)
}
#[no_mangle]
pub fn main() -> i32 {
let v = vec![
thread_create(thread_a as usize, 0),
thread_create(thread_b as usize, 0),
thread_create(thread_c as usize, 0),
];
for tid in v.iter() {
let exit_code = waittid(*tid as usize);
println!("thread#{} exited with code {}", tid, exit_code);
}
println!("main thread exited");
0
}
用户态程序编写线程函数,使用thread_create创建线程,使用waittid回收线程即可