kernel/scheduler.rs
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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192
use crate::{
object::task::{Task, TaskState},
tasklets::TaskletScheduler,
Platform,
};
use alloc::{collections::VecDeque, sync::Arc, vec::Vec};
use spinning_top::{guard::SpinlockGuard, Spinlock};
use tracing::{info, trace};
/// The global `Scheduler` coordinates the main 'run loop' of the kernel, allocating CPU time to
/// userspace tasks. There is one global `Scheduler` instance, which then holds a `CpuScheduler`
/// for each running processor to coordinate tasks running on that processor.
///
/// It is also responsible for managing spawned kernel asynchronous tasklets (which are somewhat
/// confusingly also often called `Task`s) - this involves tracking tasks that have been 'woken'
/// (are ready to make progress) and making sure they are polled regularly. The forward progress of
/// both userspace tasks and kernel tasklets are intertwined, and so are managed together.
pub struct Scheduler<P>
where
P: Platform,
{
// TODO: in the future, this will be a vec with a CpuScheduler for each CPU
task_scheduler: Spinlock<CpuScheduler<P>>,
// TODO: have a maitake scheduler for each processor (ACTUALLY I can't work out if we need one
// - LocalScheduler could be the core-local one, but both say single-core... Maybe we can just
// have one and tick it from whatever processor is available?)
pub tasklet_scheduler: TaskletScheduler,
}
pub struct CpuScheduler<P>
where
P: Platform,
{
pub running_task: Option<Arc<Task<P>>>,
/// List of Tasks ready to be scheduled. Backed by a `VecDeque` so we can rotate objects in the queue efficiently.
ready_queue: VecDeque<Arc<Task<P>>>,
blocked_queue: Vec<Arc<Task<P>>>,
}
impl<P> CpuScheduler<P>
where
P: Platform,
{
pub fn new() -> CpuScheduler<P> {
CpuScheduler { running_task: None, ready_queue: VecDeque::new(), blocked_queue: Vec::new() }
}
/// Choose the next task to be run. Returns `None` if no suitable task could be found to be run.
fn choose_next(&mut self) -> Option<Arc<Task<P>>> {
// TODO: in the future, this should consider task priorities etc.
self.ready_queue.pop_front()
}
}
impl<P> Scheduler<P>
where
P: Platform,
{
pub fn new() -> Scheduler<P> {
Scheduler {
task_scheduler: Spinlock::new(CpuScheduler::new()),
tasklet_scheduler: TaskletScheduler::new(),
}
}
pub fn add_task(&self, task: Arc<Task<P>>) {
let mut scheduler = self.for_this_cpu();
let current_state = task.state.lock().clone();
match current_state {
TaskState::Ready => scheduler.ready_queue.push_back(task),
TaskState::Blocked(_) => scheduler.blocked_queue.push(task),
TaskState::Running => panic!("Tried to schedule task that's already running!"),
}
}
pub fn for_this_cpu(&self) -> SpinlockGuard<CpuScheduler<P>> {
// XXX: this will need to take into account which CPU we're running on in the future
self.task_scheduler.lock()
}
/// Start scheduling! This should be called after a platform has finished initializing, and is
/// diverging. It gives kernel tasklets an initial poll while we're here in the kernel, and
/// then drops down into userspace.
pub fn start_scheduling(&self) -> ! {
info!("Kernel initialization done. Dropping to userspace.");
self.tasklet_scheduler.tick();
let mut scheduler = self.for_this_cpu();
assert!(scheduler.running_task.is_none());
let task = scheduler.choose_next().expect("Tried to drop into userspace with no ready tasks!");
assert!(task.state.lock().is_ready());
Self::drop_to_userspace(scheduler, task);
}
/// Called when a userspace task yields or is pre-empted. This is responsible for the
/// 'scheduling' part of the scheduler - it polls kernel tasklets as they need attention, and
/// shares CPU time between userspace tasks.
///
/// On each call to `schedule`, the kernel can choose to:
/// - Give CPU time to the kernel-space tasklet scheduler
/// - Switch to another userspace task
/// - Steal work from another CPU's scheduler
/// - Idle the CPU, if there is nothing to be done
/// - Nothing
///
/// If the current task is switched away from, it will be placed in the state `new_state`. This
/// allows the caller to block the current task on a dependency. If a task has been pre-empted
/// or yields, it should be placed into `TaskState::Ready`.
pub fn schedule(&self, new_state: TaskState) {
self.tasklet_scheduler.tick();
let mut scheduler = self.for_this_cpu();
assert!(scheduler.running_task.is_some());
if let Some(next_task) = scheduler.choose_next() {
Self::switch_to(scheduler, new_state, next_task);
} else {
/*
* There aren't any schedulable tasks. For now, we just return to the current one (by
* doing nothing here).
*
* TODO: this should idle the CPU to minimise power use, waking to interrupts + every
* so often to run tasklets, and see if any tasks are unblocked.
*/
trace!("No more schedulable tasks. Returning to current one!");
}
}
/// Perform the first transistion from the kernel into userspace. On some platforms, this has
/// to be done differently to just a regular context-switch, so we handle it here separately.
fn drop_to_userspace(mut scheduler: SpinlockGuard<CpuScheduler<P>>, task: Arc<Task<P>>) -> ! {
trace!("Dropping into usermode into task: '{}'", task.name);
*task.state.lock() = TaskState::Running;
scheduler.running_task = Some(task.clone());
task.address_space.switch_to();
drop(scheduler);
unsafe {
let context = task.context.get() as *const P::TaskContext;
P::drop_into_userspace(context)
}
}
/// This actually performs a context switch between two tasks. It takes ownership of the locked
/// `CpuScheduler` because we need to carefully release the lock before changing kernel stacks,
/// else the next task will not be able to use the scheduler.
///
/// This function returns when the userspace task that originally called `schedule` is
/// scheduled again, as if nothing happened.
fn switch_to(mut scheduler: SpinlockGuard<CpuScheduler<P>>, new_state: TaskState, next_task: Arc<Task<P>>) {
/*
* We're switching task! We sort out the internal scheduler state, and then ask the
* platform to perform the context switch for us!
* NOTE: This temporarily allows `running_task` to be `None`.
*/
let current_task = scheduler.running_task.take().unwrap();
assert!(current_task.state.lock().is_running());
assert!(next_task.state.lock().is_ready());
trace!("Switching from task '{}' to task '{}'", current_task.name, next_task.name);
scheduler.running_task = Some(next_task.clone());
*scheduler.running_task.as_ref().unwrap().state.lock() = TaskState::Running;
match new_state {
TaskState::Running => panic!("Tried to switch away from a task to state of Running!"),
TaskState::Ready => {
*current_task.state.lock() = TaskState::Ready;
scheduler.ready_queue.push_back(current_task.clone());
}
TaskState::Blocked(block) => {
trace!("Blocking task: {}", current_task.name);
*current_task.state.lock() = TaskState::Blocked(block);
scheduler.blocked_queue.push(current_task.clone());
}
}
current_task.address_space.switch_from();
next_task.address_space.switch_to();
let from_context = current_task.context.get();
let to_context = scheduler.running_task.as_ref().unwrap().context.get() as *const P::TaskContext;
drop(scheduler);
unsafe {
P::context_switch(from_context, to_context);
}
}
}