From 976756991adc4a73d009cf48a5f97a0a82422751 Mon Sep 17 00:00:00 2001 From: Akshit Gaur Date: Fri, 17 Apr 2026 12:52:03 +0000 Subject: [PATCH] Deficit Weighted Round Robin Scheduler --- Cargo.lock | 14 +-- Cargo.toml | 2 +- src/context/context.rs | 7 ++ src/context/mod.rs | 62 ++++++++++- src/context/switch.rs | 227 ++++++++++++++++++++++++++++++++--------- src/percpu.rs | 4 + src/scheme/proc.rs | 6 +- 7 files changed, 259 insertions(+), 63 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 71aac01a55..44f84c4740 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -40,9 +40,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "cc" @@ -108,7 +108,7 @@ version = "0.5.12" dependencies = [ "arrayvec", "bitfield", - "bitflags 2.11.0", + "bitflags 2.11.1", "cc", "fdt", "hashbrown 0.14.5", @@ -201,18 +201,18 @@ checksum = "64072665120942deff5fd5425d6c1811b854f4939e7f1c01ce755f64432bbea7" [[package]] name = "redox_syscall" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" +checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", ] [[package]] name = "rmm" version = "0.1.0" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index ca6924893f..0fb581f896 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ fdt = { git = "https://github.com/repnop/fdt.git", rev = "2fb1409edd1877c714a0aa hashbrown = { version = "0.14.3", default-features = false, features = ["ahash", "inline-more"] } linked_list_allocator = "0.9.0" redox-path = "0.2.0" -redox_syscall = { version = "=0.7.3", default-features = false } +redox_syscall = { version = "0.7.4", default-features = false } rmm = { path = "rmm", default-features = false } slab = { version = "0.4", default-features = false } smallvec = { version = "1.15.1", default-features = false } diff --git a/src/context/context.rs b/src/context/context.rs index d3cdc06e86..065c8ad957 100644 --- a/src/context/context.rs +++ b/src/context/context.rs @@ -13,6 +13,7 @@ use crate::{ context::{ self, arch, file::{FileDescriptor, LockedFileDescription}, + run_contexts_mut, }, cpu_set::{LogicalCpuId, LogicalCpuSet}, cpu_stats, @@ -138,6 +139,10 @@ pub struct Context { pub userspace: bool, pub being_sigkilled: bool, pub fmap_ret: Option, + /// Priority + pub prio: usize, + /// Enqueued + pub enqueued: bool, // TODO: id can reappear after wraparound? pub owner_proc_id: Option, @@ -195,6 +200,8 @@ impl Context { files: Arc::new(RwLock::new(FdTbl::new())), userspace: false, fmap_ret: None, + prio: 20, + enqueued: false, being_sigkilled: false, owner_proc_id, diff --git a/src/context/mod.rs b/src/context/mod.rs index 23455cd766..5f2e2d3326 100644 --- a/src/context/mod.rs +++ b/src/context/mod.rs @@ -2,7 +2,10 @@ //! //! For resources on contexts, please consult [wikipedia](https://en.wikipedia.org/wiki/Context_switch) and [osdev](https://wiki.osdev.org/Context_Switching) -use alloc::{collections::BTreeSet, sync::Arc}; +use alloc::{ + collections::{BTreeSet, VecDeque}, + sync::Arc, +}; use core::num::NonZeroUsize; use crate::{ @@ -70,7 +73,6 @@ pub use self::arch::empty_cr3; // Set of weak references to all contexts available for scheduling. The only strong references are // the context file descriptors. static CONTEXTS: Mutex> = Mutex::new(BTreeSet::new()); - /// Try to get the global free contexts pub fn free_contexts_try( token: LockToken<'_, L1>, @@ -83,7 +85,23 @@ pub fn free_contexts(token: LockToken<'_, L1>) -> MutexGuard<'_, L2, BTreeSet = RwLock::new(RunContextData::new()); + +pub struct RunContextData { + set: [VecDeque; 40], +} + +impl RunContextData { + pub const fn new() -> Self { + const EMPTY_VEC: VecDeque = VecDeque::new(); + Self { + set: [EMPTY_VEC; 40], + } + } +} + +/// Get the global schemes list, const pub fn contexts(token: LockToken<'_, L0>) -> RwLockReadGuard<'_, L1, BTreeSet> { let percpu = PercpuBlock::current(); percpu.contexts.read(token) @@ -95,6 +113,14 @@ pub fn contexts_mut(token: LockToken<'_, L0>) -> RwLockWriteGuard<'_, L1, BTreeS percpu.contexts.write(token) } +pub fn run_contexts(token: LockToken<'_, L0>) -> RwLockReadGuard<'_, L1, RunContextData> { + RUN_CONTEXTS.read(token) +} + +pub fn run_contexts_mut(token: LockToken<'_, L0>) -> RwLockWriteGuard<'_, L1, RunContextData> { + RUN_CONTEXTS.write(token) +} + pub fn init(token: &mut CleanLockToken) { let owner = None; // kmain not owned by any fd let mut context = Context::new(owner).expect("failed to create kmain context"); @@ -109,6 +135,7 @@ pub fn init(token: &mut CleanLockToken) { context.status = Status::Runnable; context.running = true; context.cpu_id = Some(crate::cpu_id()); + context.enqueued = false; let context_lock = Arc::new(ContextLock::new(context)); @@ -123,6 +150,30 @@ pub fn init(token: &mut CleanLockToken) { } } +pub fn wakeup_context(context_lock: &Arc>, mut token: LockToken) { + let mut prio = None; + { + let mut context = context_lock.write(token.token()); + + context.wake = None; + context.unblock(); + + if !(context.status.is_runnable() && !context.running && !context.enqueued) { + return; + } + + context.enqueued = true; + + prio = Some(context.prio); + } + + let mut run_queues = run_contexts_mut(token); + + if let Some(priority) = prio { + run_queues.set[priority].push_back(ContextRef(Arc::clone(context_lock))); + } +} + pub fn current() -> Arc { PercpuBlock::current() .switch_internals @@ -185,7 +236,10 @@ pub fn spawn( let context_lock = Arc::new(ContextLock::new(context)); let context_ref = ContextRef(Arc::clone(&context_lock)); - free_contexts(token.token().downgrade()).insert(context_ref); + let run_ref = ContextRef(Arc::clone(&context_lock)); + run_contexts_mut(token.token()).set[20].push_back(run_ref); + contexts_mut(token.token()).insert(context_ref); + context_lock.write(token.token()).enqueued = true; Ok(context_lock) } diff --git a/src/context/switch.rs b/src/context/switch.rs index 4460bcf827..86ccf6a727 100644 --- a/src/context/switch.rs +++ b/src/context/switch.rs @@ -1,26 +1,26 @@ //! This module provides a context-switching mechanism that utilizes a simple round-robin scheduler. //! The scheduler iterates over available contexts, selecting the next context to run, while //! handling process states and synchronization. -use core::{ - cell::{Cell, RefCell}, - hint, mem, - ops::Bound, - sync::atomic::Ordering, -}; - -use alloc::sync::Arc; -use syscall::PtraceFlags; - +use crate::sync::ArcRwLockWriteGuard; +use crate::sync::L4; use crate::{ context::{ - arch, contexts, contexts_mut, free_contexts_try, ArcContextLockWriteGuard, Context, - ContextLock, + self, arch, contexts, contexts_mut, free_contexts_try, run_contexts_mut, + ArcContextLockWriteGuard, Context, ContextLock, }, cpu_set::LogicalCpuId, cpu_stats, percpu::PercpuBlock, sync::CleanLockToken, }; +use alloc::{sync::Arc, vec::Vec}; +use core::{ + cell::{Cell, RefCell}, + hint, mem, + ops::Bound, + sync::atomic::Ordering, +}; +use syscall::PtraceFlags; use super::ContextRef; @@ -29,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). /// @@ -127,11 +134,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! @@ -175,43 +179,44 @@ pub fn switch(token: &mut CleanLockToken) -> SwitchResult { return SwitchResult::Switched; } - 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()); + let current_context = context::current(); - // 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) - // ... but not the current context (note the `Bound::Excluded`), - // which is already locked. - { - { - // 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(next_context_guard); - break; + let mut contexts_guard = contexts(token.token()); + let (context, mut token) = contexts_guard.token_split(); + for context_ref in context.iter().filter_map(|r| r.upgrade()) { + if Arc::ptr_eq(&context_ref, ¤t_context) { + continue; + } + let guard = context_ref.read(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, token.token()); + } + + let cpu_id = crate::cpu_id(); + + let mut switch_context_opt = + match select_next_context(token, percpu, cpu_id, switch_time, &mut prev_context_guard) { + Ok(opt) => opt, + Err(early_ret) => return early_ret, + }; if switch_context_opt.is_none() { let mut this_contexts = contexts_mut(token.token()); @@ -334,6 +339,130 @@ 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, + prev_context_guard: &mut ArcRwLockWriteGuard, +) -> Result, 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(); + + let mut empty_queues = 0; + let mut total_iters = 0; + let mut next_context_guard_opt = None; + + let total_contexts: usize = contexts_list.iter().map(|q| q.len()).sum(); + let mut skipped_contexts = 0; + + '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; + } + + if skipped_contexts > total_contexts && total_contexts > 0 { + break 'priority; + } + + let contexts = contexts_list + .get_mut(i) + .expect("i should be between [0, 39]!"); + + if contexts.is_empty() { + 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 => { + skipped_contexts += 1; + continue; // Ghost Process, just continue + } + }, + None => break, // Empty Queue + }; + + if Arc::ptr_eq(&next_context_lock, &prev_context_lock) { + contexts.push_back(ContextRef(Arc::clone(&next_context_lock))); + continue; + } + + let mut next_context_guard = unsafe { next_context_lock.write_arc() }; + next_context_guard.enqueued = false; + + if !next_context_guard.status.is_runnable() { + skipped_contexts += 1; + 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; + skipped_contexts += 1; + + if skipped_contexts >= total_contexts { + break 'priority; + } + } + } + } + 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() { + 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(next_context_guard)); + } else { + // We found no other process to run. + Ok(None) + } +} + /// Holds per-CPU state necessary for context switching. /// /// This struct contains information such as the idle context, current context, and PIT tick counts, diff --git a/src/percpu.rs b/src/percpu.rs index 7838fd212b..f3c4c3ee6d 100644 --- a/src/percpu.rs +++ b/src/percpu.rs @@ -34,6 +34,8 @@ pub struct PercpuBlock { pub current_addrsp: RefCell>>, pub new_addrsp_tmp: Cell>>, pub wants_tlb_shootdown: AtomicBool, + pub balance: Cell<[usize; 40]>, + pub last_queue: Cell, // TODO: Put mailbox queues here, e.g. for TLB shootdown? Just be sure to 128-byte align it // first to avoid cache invalidation. @@ -204,6 +206,8 @@ impl PercpuBlock { current_addrsp: RefCell::new(None), new_addrsp_tmp: Cell::new(None), wants_tlb_shootdown: AtomicBool::new(false), + balance: Cell::new([0; 40]), + last_queue: Cell::new(39), ptrace_flags: Cell::new(PtraceFlags::empty()), ptrace_session: RefCell::new(None), inside_syscall: Cell::new(false), diff --git a/src/scheme/proc.rs b/src/scheme/proc.rs index d82187065b..4c982d44f4 100644 --- a/src/scheme/proc.rs +++ b/src/scheme/proc.rs @@ -1265,6 +1265,7 @@ impl ContextHandle { guard.pid = info.pid as usize; guard.euid = info.euid; guard.egid = info.egid; + guard.prio = (info.prio as usize).min(39); Ok(size_of::()) } ContextHandle::OpenViaDup => { @@ -1459,14 +1460,15 @@ impl ContextHandle { ContextHandle::Attr => { let mut debug_name = [0; 32]; let c = &context.read(token.token()); - let (euid, egid, pid, name) = (c.euid, c.egid, c.pid as u32, c.name); + let (euid, egid, pid, name, prio) = + (c.euid, c.egid, c.pid as u32, c.name, c.prio as u32); let min = name.len().min(debug_name.len()); debug_name[..min].copy_from_slice(&name.as_bytes()[..min]); buf.copy_common_bytes_from_slice(&ProcSchemeAttrs { pid, euid, egid, - ens: 0, + prio, debug_name, }) }