Deficit based Weighted Round Robin Scheduler
This commit is contained in:
committed by
Jeremy Soller
parent
74895c4f0f
commit
b7dabfc3c2
+174
-80
@@ -8,13 +8,16 @@ use core::{
|
||||
sync::atomic::Ordering,
|
||||
};
|
||||
|
||||
use alloc::sync::Arc;
|
||||
use alloc::{sync::Arc, vec::Vec};
|
||||
use syscall::PtraceFlags;
|
||||
|
||||
use crate::{
|
||||
context::{arch, contexts, ArcContextLockWriteGuard, Context, ContextLock},
|
||||
context::{
|
||||
self, arch, contexts, run_contexts_mut, ArcContextLockWriteGuard, Context, ContextLock,
|
||||
Status,
|
||||
},
|
||||
cpu_set::LogicalCpuId,
|
||||
cpu_stats,
|
||||
cpu_stats, log,
|
||||
percpu::PercpuBlock,
|
||||
sync::CleanLockToken,
|
||||
};
|
||||
@@ -26,6 +29,13 @@ enum UpdateResult {
|
||||
Skip,
|
||||
}
|
||||
|
||||
// A simple geometric series where value[i] ~= value[i - 1] * 1.25
|
||||
const sched_prio_to_weight: [usize; 40] = [
|
||||
88761, 71755, 56483, 46273, 36291, 29154, 23254, 18705, 14949, 11916, 9548, 7620, 6100, 4904,
|
||||
3906, 3121, 2501, 1991, 1586, 1277, 1024, 820, 655, 526, 423, 335, 272, 215, 172, 137, 110, 87,
|
||||
70, 56, 45, 36, 29, 23, 18, 15,
|
||||
];
|
||||
|
||||
/// Determines if a given context is eligible to be scheduled on a given CPU (in
|
||||
/// principle, the current CPU).
|
||||
///
|
||||
@@ -54,15 +64,6 @@ unsafe fn update_runnable(
|
||||
return UpdateResult::Skip;
|
||||
}
|
||||
|
||||
// If context is soft-blocked and has a wake-up time, check if it should wake up.
|
||||
if context.status.is_soft_blocked()
|
||||
&& let Some(wake) = context.wake
|
||||
&& switch_time >= wake
|
||||
{
|
||||
context.wake = None;
|
||||
context.unblock_no_ipi();
|
||||
}
|
||||
|
||||
// If the context is runnable, indicate it can be switched to.
|
||||
if context.status.is_runnable() {
|
||||
UpdateResult::CanSwitch
|
||||
@@ -124,11 +125,8 @@ pub enum SwitchResult {
|
||||
AllContextsIdle,
|
||||
}
|
||||
|
||||
/// Selects and switches to the next context using a round-robin scheduler.
|
||||
///
|
||||
/// This function performs the context switch, checking each context in a loop for eligibility
|
||||
/// until it finds a context ready to run. If no other context is runnable, it returns to the
|
||||
/// idle context.
|
||||
/// This function performs the context switch, using select_next_context to
|
||||
/// actually select the next context to switch to.
|
||||
///
|
||||
/// # Warning
|
||||
/// This is not memory-unsafe to call. But do NOT call this while holding locks!
|
||||
@@ -157,73 +155,42 @@ pub fn switch(token: &mut CleanLockToken) -> SwitchResult {
|
||||
percpu.maybe_handle_tlb_shootdown();
|
||||
}
|
||||
|
||||
let cpu_id = crate::cpu_id();
|
||||
|
||||
let mut switch_context_opt = None;
|
||||
// Alarm (previously in update_runnable)
|
||||
// TODO: Optimise this somehow
|
||||
let mut wakeups = Vec::new();
|
||||
{
|
||||
let contexts = contexts(token.token());
|
||||
|
||||
// Lock the previous context.
|
||||
let prev_context_lock = crate::context::current();
|
||||
// We are careful not to lock this context twice
|
||||
let prev_context_guard = unsafe { prev_context_lock.write_arc() };
|
||||
|
||||
if !prev_context_guard.is_preemptable() {
|
||||
// Unset global lock
|
||||
arch::CONTEXT_SWITCH_LOCK.store(false, Ordering::SeqCst);
|
||||
|
||||
// Pretend to have finished switching, so CPU is not idled
|
||||
return SwitchResult::Switched;
|
||||
}
|
||||
|
||||
let idle_context = percpu.switch_internals.idle_context();
|
||||
|
||||
// Stateful flag used to skip the idle process the first time it shows up.
|
||||
// After that, this flag is set to `false` so the idle process can be
|
||||
// picked up.
|
||||
let mut skip_idle = true;
|
||||
|
||||
// Attempt to locate the next context to switch to.
|
||||
for next_context_lock in contexts
|
||||
// Include all contexts with IDs greater than the current...
|
||||
.range((
|
||||
Bound::Excluded(ContextRef(Arc::clone(&prev_context_lock))),
|
||||
Bound::Unbounded,
|
||||
))
|
||||
// ... and all contexts with IDs less than the current...
|
||||
.chain(contexts.range((
|
||||
Bound::Unbounded,
|
||||
Bound::Excluded(ContextRef(Arc::clone(&prev_context_lock))),
|
||||
)))
|
||||
.filter_map(ContextRef::upgrade)
|
||||
// ... and the idle context...
|
||||
.chain(Some(Arc::clone(&idle_context)))
|
||||
// ... but not the current context (note the `Bound::Excluded`),
|
||||
// which is already locked.
|
||||
{
|
||||
if Arc::ptr_eq(&next_context_lock, &idle_context) && skip_idle {
|
||||
// Skip idle process the first time it shows up, but allow it
|
||||
// to be picked up again the next time.
|
||||
skip_idle = false;
|
||||
let current_context = context::current();
|
||||
let contexts_guard = contexts(token.token());
|
||||
for context_ref in contexts_guard.iter().filter_map(|r| r.upgrade()) {
|
||||
if Arc::ptr_eq(&context_ref, ¤t_context) {
|
||||
continue;
|
||||
}
|
||||
|
||||
{
|
||||
// Lock next context
|
||||
// We are careful not to lock this context twice
|
||||
let mut next_context_guard = unsafe { next_context_lock.write_arc() };
|
||||
|
||||
// Check if the context is runnable and can be switched to.
|
||||
if let UpdateResult::CanSwitch =
|
||||
unsafe { update_runnable(&mut next_context_guard, cpu_id, switch_time) }
|
||||
{
|
||||
// Store locks for previous and next context and break out from loop
|
||||
// for the switch
|
||||
switch_context_opt = Some((prev_context_guard, next_context_guard));
|
||||
break;
|
||||
let mut local_token = unsafe { CleanLockToken::new() };
|
||||
let guard = context_ref.read(local_token.token());
|
||||
if guard.status.is_soft_blocked() {
|
||||
if let Some(wake) = guard.wake {
|
||||
if switch_time >= wake {
|
||||
wakeups.push(Arc::clone(&context_ref));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if guard.status.is_runnable() && !guard.enqueued && !guard.running {
|
||||
wakeups.push(Arc::clone(&context_ref));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for context_lock in wakeups {
|
||||
context::wakeup_context(&context_lock);
|
||||
}
|
||||
|
||||
let cpu_id = crate::cpu_id();
|
||||
|
||||
let switch_context_opt = match select_next_context(token, percpu, cpu_id, switch_time) {
|
||||
Ok(opt) => opt,
|
||||
Err(early_ret) => return early_ret,
|
||||
};
|
||||
|
||||
// Update per-cpu times
|
||||
@@ -297,8 +264,8 @@ pub fn switch(token: &mut CleanLockToken) -> SwitchResult {
|
||||
prev_context.syscall_debug_info = percpu
|
||||
.syscall_debug_info
|
||||
.replace(next_context.syscall_debug_info);
|
||||
prev_context.syscall_debug_info.on_switch_from(token);
|
||||
next_context.syscall_debug_info.on_switch_to(token);
|
||||
prev_context.syscall_debug_info.on_switch_from();
|
||||
next_context.syscall_debug_info.on_switch_to();
|
||||
}
|
||||
|
||||
percpu
|
||||
@@ -328,6 +295,133 @@ pub fn switch(token: &mut CleanLockToken) -> SwitchResult {
|
||||
}
|
||||
}
|
||||
|
||||
/// This is the scheduler function which currently utilises Deficit Weighted Round Robin Scheduler
|
||||
fn select_next_context(
|
||||
token: &mut CleanLockToken,
|
||||
percpu: &PercpuBlock,
|
||||
cpu_id: LogicalCpuId,
|
||||
switch_time: u128,
|
||||
) -> Result<Option<(ArcContextLockWriteGuard, ArcContextLockWriteGuard)>, SwitchResult> {
|
||||
let mut contexts_data = run_contexts_mut(token.token());
|
||||
let mut contexts_list = &mut contexts_data.set;
|
||||
let mut balance = percpu.balance.get();
|
||||
let mut i = percpu.last_queue.get() % 40;
|
||||
|
||||
// Lock the previous context.
|
||||
let prev_context_lock = crate::context::current();
|
||||
// We are careful not to lock this context twice
|
||||
let mut prev_context_guard = unsafe { prev_context_lock.write_arc() };
|
||||
|
||||
// If we cannot even preempt the prev context, no need to go any further
|
||||
if !prev_context_guard.is_preemptable() {
|
||||
// Unset global lock
|
||||
arch::CONTEXT_SWITCH_LOCK.store(false, Ordering::SeqCst);
|
||||
|
||||
// Pretend to have finished switching, so CPU is not idled
|
||||
return Err(SwitchResult::Switched);
|
||||
}
|
||||
|
||||
let idle_context = percpu.switch_internals.idle_context();
|
||||
|
||||
let mut empty_queues = 0;
|
||||
let mut total_iters = 0;
|
||||
let mut next_context_guard_opt = None;
|
||||
|
||||
'priority: loop {
|
||||
i = (i + 1) % 40;
|
||||
total_iters += 1;
|
||||
|
||||
// The least prioritised queue takes <5000 iters to build up
|
||||
// balance = sched_prio_to_weight[20], if we have already spent
|
||||
// that many iters and not found any context, it is better to just
|
||||
// skip for now
|
||||
if total_iters >= 5000 {
|
||||
break 'priority;
|
||||
}
|
||||
let contexts = contexts_list
|
||||
.get_mut(i)
|
||||
.expect("i should be between [0, 39]!");
|
||||
|
||||
if contexts.is_empty() {
|
||||
balance[i] = 0; // We do not allow a queue to build up its balance when nobody is using it
|
||||
empty_queues += 1;
|
||||
if empty_queues >= 40 {
|
||||
// If all queues are empty, just break out
|
||||
break 'priority;
|
||||
}
|
||||
continue;
|
||||
} else {
|
||||
empty_queues = 0;
|
||||
}
|
||||
|
||||
if balance[i] < sched_prio_to_weight[20] {
|
||||
// This queue does not have enough balance to run,
|
||||
// increment the balance!
|
||||
balance[i] += sched_prio_to_weight[i];
|
||||
continue;
|
||||
}
|
||||
|
||||
let len = contexts.len();
|
||||
for _ in 0..len {
|
||||
let next_context_lock = match contexts.pop_front() {
|
||||
Some(lock) => match lock.upgrade() {
|
||||
Some(new_lock) => new_lock,
|
||||
None => continue, // Ghost Process, just continue
|
||||
},
|
||||
None => break, // Empty Queue
|
||||
};
|
||||
|
||||
let mut next_context_guard = unsafe { next_context_lock.write_arc() };
|
||||
next_context_guard.enqueued = false;
|
||||
|
||||
if !next_context_guard.status.is_runnable() {
|
||||
continue; // Lazy removal of blocked contexts
|
||||
}
|
||||
|
||||
// Is this context runnable on this CPU?
|
||||
if let UpdateResult::CanSwitch =
|
||||
unsafe { update_runnable(&mut next_context_guard, cpu_id, switch_time) }
|
||||
{
|
||||
next_context_guard_opt = Some(next_context_guard);
|
||||
balance[i] -= sched_prio_to_weight[20];
|
||||
break 'priority;
|
||||
} else {
|
||||
contexts.push_back(ContextRef(Arc::clone(&next_context_lock)));
|
||||
next_context_guard.enqueued = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
percpu.balance.set(balance);
|
||||
percpu.last_queue.set(i);
|
||||
|
||||
if let Some(next_context_guard) = next_context_guard_opt {
|
||||
// We found a new process!
|
||||
// Send the old process to the back of the line (if it is still runnable)
|
||||
if prev_context_guard.status.is_runnable()
|
||||
&& !Arc::ptr_eq(&prev_context_lock, &idle_context)
|
||||
{
|
||||
let prio = prev_context_guard.prio;
|
||||
contexts_list[prio].push_back(ContextRef(Arc::clone(&prev_context_lock)));
|
||||
prev_context_guard.enqueued = true;
|
||||
}
|
||||
|
||||
return Ok(Some((prev_context_guard, next_context_guard)));
|
||||
} else {
|
||||
// We found no other process to run.
|
||||
if prev_context_guard.status.is_runnable()
|
||||
&& !Arc::ptr_eq(&prev_context_lock, &idle_context)
|
||||
{
|
||||
arch::CONTEXT_SWITCH_LOCK.store(false, Ordering::SeqCst);
|
||||
return Err(SwitchResult::Switched);
|
||||
} else if Arc::ptr_eq(&prev_context_lock, &idle_context) {
|
||||
return Ok(None);
|
||||
} else {
|
||||
let idle_guard = unsafe { idle_context.write_arc() };
|
||||
return Ok(Some((prev_context_guard, idle_guard)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Holds per-CPU state necessary for context switching.
|
||||
///
|
||||
/// This struct contains information such as the idle context, current context, and PIT tick counts,
|
||||
|
||||
Reference in New Issue
Block a user