Co-operative vs preemptive multitasking
In my OS I have already implemented co-operative multitasking however there are a number of reasons why I wanted to implement preemptive multitasking. Firstly since each task could run for an infinite amount of time it would be terrible for one rogue process to bring down the entire system. Secondly in order for tasks such as keyboard and mouse to run I had to add yield_now().await()
all around my code base to share time. This made reading the code more challenging and it was still easy to accidentally add an expensive operation without a yield. Thirdly, in order to add multi processor functionality later on a proper process switcher would be needed and rewriting a code base for a different scheduler type would take forever.
Implementing preemptive multitasking
To implement the TaskManager I was able to copy over many of the tools I wrote for the co-operative scheduler however I actually found it easier to understand how it worked compared to rust async/await. This is likely because fundamentally process switching involves getting a process switch interrupt then saving the current process CPU state and restoring the new processes state.
The initialization of a process was challenging as I had to look up the C calling convention in order to pass arguments. I so far only implemented the basic fact that you can pass 64 bit numbers or pointers in certain registers. I was then able to prefill the CPU state with the parameters instead of 0’s when the task was created. Unfortunately this means only C safe types can be passed through this hacky solution, however I was able to ensure that the parameters passed were of the same type as the function requested via generic parameters as shown below.
impl Task {
/// Safety for set_args_n
/// As long as each type is not longer than 64 bits which would require 2
pub fn set_args_3<A, B, C>(&mut self, func: extern "C" fn(A, B, C), a: A, b: B, c: C) {
// Set the instruction pointer to the function entry point
self.state_isf.instruction_pointer = VirtAddr::from_ptr(func as *const usize);
// Unsafely set the CPU registers to the parameters
unsafe {
self.state_reg.rdi = transmute_copy(&a);
self.state_reg.rsi = transmute_copy(&b);
self.state_reg.rdx = transmute_copy(&c);
}
}
}
The switching code was much simpler boiling down to save current state then get the next process and restore it’s expected CPU state. A simplified code example is shown below.
impl TaskManager {
/// Called by the interrupt handler
pub fn switch_task(&mut self, stack_frame: &mut InterruptStackFrame, regs: &mut Registers) {
let mut task_queue = self.task_queue.lock();
// If we havn't instantiated any tasks return
if self.tasks.is_empty() {
return
}
// Ask the current task handler to save its stack_frame and CPU registers
self.tasks.get_mut(&self.current_task).unwrap().save(stack_frame, regs);
// Push old task to the back of the queue
task_queue.push_back(self.current_task);
// Can we get a new task
if let Some(next_task_id) = task_queue.pop_front() {
// Get the task from the tasks list
let next_task = self.tasks.get(&next_task_id).unwrap();
// Set the current task id to the new task we are executing
self.current_task = next_task_id;
unsafe {
// Get a mutable pointer to the interrupt stack frame
let isf = stack_frame.as_mut().extract_inner();
// Write the new tasks isf to to CPU's isf
write_volatile(
isf as *mut InterruptStackFrameValue,
next_task.stack_isf.clone()
)
// Set the CPU registers to the new processes state
*regs = next_task.state_regs.clone();
}
}
}
}
Testing the system
To test the system I created two functions that print out “a” and “b” respectfully as shown below. This successfully prints out a’s and b’s after each other. As such the system is shwitching between tasks via the timer successfully. The new system also seemlessly intergrates with my existing async code via an async thread.
fn main() {
... Init code
let mut task_manager = TASKMANAGER.lock();
task_manager.spawn(Task::new(a));
task_manager.spawn(Task::new(b));
}
fn a() {
unsafe { asm!("hlt") };
println!("a");
}
fn b() {
unsafe { asm!("hlt") };
println!("a");
}
Challenges
There is an error caused by rust doing certain optimisations on debug mode. I am not quite sure how to fix it yet. However my temperary measure is to use the –release flag, as that works perfectly fine. As shown below I introduced a warning for myself just before the problematic code runs incase it does crash due to this problem.
Timeline
As I have now completed preemtive multiasking a new problem occurs. How does a thread commicate with the process manager? Using syscalls of cause, and as such that is what I will work on for the next week. This unfortunately means delaying networking and filesystems, but it will make a much better OS.