As the growth of OS, dispatch resource could be divided to smaller piece for more efficient operations. Now, process can’t satisfied our demand, we want some programs could be implemented in parallel. Then, we introduce Thread.
Therefore, process will be the container of threads, each threads contain its id, state, current instruction ptr, registers, stack. However, it will share data(which means same memory and addr) in the same process. So, we will develop a accompany exclusion mechanism for parallel operations by each threads.
Design Data
Now, clarify our resource dispatch for one thread:
Immutable:
kernel stack
Mutable:
thread id
user stack
trap context
trap status
exit code
Every tasks is a thread unit and contained in one process, so now, process is really a process rather a task, it can owns many tasks.
Notice, we should separate user stack and kernel stack, we shouldn’t allocate user stack and kernel stack by same logic. Kernel stack is immutable, we only need its top place for trap context.
Because every thread use the same memory set, so each user stack and its trampoline would be allocated by its thread id. We encapsulate these to TaskUserRes data.
We can see many structure need a id allocation, we could design a general id allocator.
pubfnfork(self: &Arc<Self>) -> Arc<Self> { let child = ...; parent.children.push(Arc::clone(&child)); let task = Arc::new(TaskControlBlock::new( Arc::clone(&child), parent .get_task(0) .inner_exclusive_access() .res .as_ref() .unwrap() .ustack_base(), // here we do not allocate trap_cx or ustack again // but mention that we allocate a new kstack here false, )); letmut child_inner = child.inner_exclusive_access(); child_inner.tasks.push(Some(Arc::clone(&task))); drop(child_inner); ... }
Design System Operation
If we want to create a thread, as a naive designer, we only need the function entry addr, and arguments, yes! That’s it!
pubfnsys_thread_create(entry: usize, arg: usize) -> isize { let task = current_task().unwrap(); let process = task.process.upgrade().unwrap(); // create a new thread let new_task = Arc::new(TaskControlBlock::new( Arc::clone(&process), task.inner_exclusive_access().res.as_ref().unwrap().ustack_base, true, )); // add new task to scheduler add_task(Arc::clone(&new_task)); let new_task_inner = new_task.inner_exclusive_access(); let new_task_res = new_task_inner.res.as_ref().unwrap(); let new_task_tid = new_task_res.tid; letmut process_inner = process.inner_exclusive_access(); // add new thread to current process let tasks = &mut process_inner.tasks; while tasks.len() < new_task_tid + 1 { tasks.push(None); } tasks[new_task_tid] = Some(Arc::clone(&new_task)); let new_task_trap_cx = new_task_inner.get_trap_cx(); *new_task_trap_cx = TrapContext::app_init_context( entry, new_task_res.ustack_top(), kernel_token(), new_task.kstack.get_top(), trap_handler asusize, ); (*new_task_trap_cx).x[10] = arg; new_task_tid asisize }
Now, sys_exit will receive a exit_code and recycle its resource. Notice, if tid == 0, the thread of process itself will make other sub threads moved to init process.
letmut recycle_res = Vec::<TaskUserRes>::new(); for task in process_inner.tasks.iter().filter(|t| t.is_some()) { let task = task.as_ref().unwrap(); remove_inactive_task(Arc::clone(&task)); letmut task_inner = task.inner_exclusive_access(); ifletSome(res) = task_inner.res.take() { recycle_res.push(res); } }
sys_waittid will check thread state and recycle if could, return -2 if it has not exited. Why need it? Because sys_exit can’t recycle itself unless the thread of process, other thread can call waittid to remove it from tasks queue, then it will be cleared by rust!