fix: sched round-robin, cap slot OOB panic, clippy, make ci
- RunList -> intrusive FIFO, no starvation, no silent drop - cap table sized to CapSlot range (kani caught OOB panic) - snake food color test uses const - kani cfg lint, RUST_MIN_STACK for big kernel struct in tests
This commit is contained in:
@@ -5,3 +5,6 @@ edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
types = { workspace = true }
|
||||
|
||||
[lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ["cfg(kani)"] }
|
||||
|
||||
@@ -8,7 +8,8 @@ use crate::{error::KernelError, resource::Resource};
|
||||
#[path = "tests.rs"]
|
||||
mod tests;
|
||||
|
||||
const MAX_CAPS_PER_PROCESS: usize = 16;
|
||||
// Sized so that every representable `CapSlot` indexes a real slot.
|
||||
pub const MAX_CAPS_PER_PROCESS: usize = CapSlot::MAX as usize + 1;
|
||||
|
||||
pub struct Capability {
|
||||
resource: Resource,
|
||||
@@ -76,3 +77,9 @@ impl CapabilityTable {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for CapabilityTable {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,15 +113,19 @@ impl EndpointTable {
|
||||
}
|
||||
|
||||
pub fn remove_thread_from_all(&mut self, thread: ThreadId) {
|
||||
for slot in &mut self.endpoints {
|
||||
if let Some(ep) = slot {
|
||||
ep.remove_sender(thread);
|
||||
ep.remove_receiver(thread);
|
||||
}
|
||||
for ep in self.endpoints.iter_mut().flatten() {
|
||||
ep.remove_sender(thread);
|
||||
ep.remove_receiver(thread);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for EndpointTable {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait MessagePassing {
|
||||
fn send(
|
||||
&mut self,
|
||||
|
||||
@@ -44,3 +44,9 @@ impl WaitQueue {
|
||||
self.count == 0
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for WaitQueue {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,3 +36,9 @@ impl IrqRoutingTable {
|
||||
self.owners[irq.raw() as usize]
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for IrqRoutingTable {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,10 +104,11 @@ impl RegionTable {
|
||||
base: PhysicalAddress,
|
||||
) -> Option<(RegionId, &MemoryRegion)> {
|
||||
for (i, slot) in self.regions.iter().enumerate() {
|
||||
if let Some(region) = slot {
|
||||
if region.owner() == owner && region.base() == base {
|
||||
return Some((RegionId::from_raw(i as u16), region));
|
||||
}
|
||||
if let Some(region) = slot
|
||||
&& region.owner() == owner
|
||||
&& region.base() == base
|
||||
{
|
||||
return Some((RegionId::from_raw(i as u16), region));
|
||||
}
|
||||
}
|
||||
None
|
||||
@@ -120,3 +121,9 @@ impl RegionTable {
|
||||
.filter(move |r| r.owner() == process)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RegionTable {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,9 +12,7 @@ pub struct PagePool {
|
||||
impl PagePool {
|
||||
pub fn new(base: PhysicalAddress, total_pages: usize) -> Self {
|
||||
let mut pages = [false; MAX_PAGES];
|
||||
for i in 0..total_pages {
|
||||
pages[i] = true;
|
||||
}
|
||||
pages[..total_pages].fill(true);
|
||||
Self {
|
||||
pages,
|
||||
base,
|
||||
|
||||
@@ -124,3 +124,9 @@ impl ProcessTable {
|
||||
.ok_or(KernelError::InvalidProcess(id))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ProcessTable {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,13 +47,13 @@ impl MultiCoreScheduler {
|
||||
|
||||
fn least_loaded_core(&self) -> Option<CoreId> {
|
||||
(0..self.num_cores)
|
||||
.map(|i| CoreId::from_raw(i))
|
||||
.map(CoreId::from_raw)
|
||||
.min_by_key(|&c| self.thread_count_on(c))
|
||||
}
|
||||
|
||||
fn most_loaded_core(&self) -> Option<CoreId> {
|
||||
(0..self.num_cores)
|
||||
.map(|i| CoreId::from_raw(i))
|
||||
.map(CoreId::from_raw)
|
||||
.max_by_key(|&c| self.thread_count_on(c))
|
||||
}
|
||||
}
|
||||
@@ -69,7 +69,7 @@ impl Scheduler for MultiCoreScheduler {
|
||||
let idx = thread.raw() as usize;
|
||||
if let Some(placement) = self.placements[idx].take() {
|
||||
let core_idx = placement.core.raw() as usize;
|
||||
self.cores[core_idx].remove(thread, placement.priority);
|
||||
self.cores[core_idx].remove(thread);
|
||||
if self.cores[core_idx].current() == Some(thread) {
|
||||
self.cores[core_idx].clear_current();
|
||||
}
|
||||
@@ -85,13 +85,12 @@ impl Scheduler for MultiCoreScheduler {
|
||||
if let Some(placement) = &self.placements[idx] {
|
||||
let current_priority = placement.priority;
|
||||
let core_idx = core.raw() as usize;
|
||||
if let Some(highest) = self.cores[core_idx].highest_waiting_priority() {
|
||||
if highest > current_priority {
|
||||
if let Some(next) = self.cores[core_idx].pick_next() {
|
||||
self.cores[core_idx].add(current, current_priority);
|
||||
return SchedulerDecision::Preempt(next);
|
||||
}
|
||||
}
|
||||
if let Some(highest) = self.cores[core_idx].highest_waiting_priority()
|
||||
&& highest > current_priority
|
||||
&& let Some(next) = self.cores[core_idx].pick_next()
|
||||
{
|
||||
self.cores[core_idx].add(current, current_priority);
|
||||
return SchedulerDecision::Preempt(next);
|
||||
}
|
||||
}
|
||||
SchedulerDecision::Continue
|
||||
|
||||
@@ -29,12 +29,12 @@ impl PerCoreScheduler {
|
||||
self.queue.add(thread, priority);
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, thread: ThreadId, priority: Priority) {
|
||||
self.queue.remove(thread, priority);
|
||||
pub fn remove(&mut self, thread: ThreadId) {
|
||||
self.queue.remove(thread);
|
||||
}
|
||||
|
||||
pub fn pick_next(&mut self) -> Option<ThreadId> {
|
||||
let (thread, _priority) = self.queue.next()?;
|
||||
let (thread, _priority) = self.queue.pop_highest()?;
|
||||
self.current = Some(thread);
|
||||
Some(thread)
|
||||
}
|
||||
@@ -56,6 +56,10 @@ impl PerCoreScheduler {
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.queue.bitmap().count_ones() as usize
|
||||
self.queue.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.queue.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,71 +2,42 @@ use types::bounded::Priority;
|
||||
use types::id::ThreadId;
|
||||
|
||||
const NUM_PRIORITIES: usize = 32;
|
||||
const MAX_THREADS_PER_PRIORITY: usize = 16;
|
||||
const MAX_THREADS: usize = crate::thread::MAX_THREADS;
|
||||
|
||||
struct RunList {
|
||||
threads: [Option<ThreadId>; MAX_THREADS_PER_PRIORITY],
|
||||
head: usize,
|
||||
tail: usize,
|
||||
count: u8,
|
||||
#[derive(Clone, Copy)]
|
||||
struct Links {
|
||||
prev: Option<ThreadId>,
|
||||
next: Option<ThreadId>,
|
||||
queued_at: Option<u8>,
|
||||
}
|
||||
|
||||
impl RunList {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
threads: [None; MAX_THREADS_PER_PRIORITY],
|
||||
head: 0,
|
||||
tail: 0,
|
||||
count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn push(&mut self, thread: ThreadId) -> bool {
|
||||
match self.threads.iter_mut().find(|s| s.is_none()) {
|
||||
Some(slot) => {
|
||||
*slot = Some(thread);
|
||||
self.count += 1;
|
||||
true
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn pop(&mut self) -> Option<ThreadId> {
|
||||
if self.count == 0 {
|
||||
return None;
|
||||
}
|
||||
let slot = self.threads.iter_mut().find(|s| s.is_some())?;
|
||||
let thread = slot.take();
|
||||
self.count -= 1;
|
||||
thread
|
||||
}
|
||||
|
||||
fn remove(&mut self, thread: ThreadId) -> bool {
|
||||
for slot in &mut self.threads {
|
||||
if *slot == Some(thread) {
|
||||
*slot = None;
|
||||
self.count -= 1;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
self.count == 0
|
||||
}
|
||||
#[derive(Clone, Copy)]
|
||||
struct ListEnds {
|
||||
head: Option<ThreadId>,
|
||||
tail: Option<ThreadId>,
|
||||
}
|
||||
|
||||
/// Ready queue: one FIFO per priority level, threaded through an
|
||||
/// intrusive doubly linked list indexed by thread id. A thread is in at
|
||||
/// most one list, so capacity never runs out and every op is O(1).
|
||||
pub struct PriorityQueue {
|
||||
queues: [RunList; NUM_PRIORITIES],
|
||||
lists: [ListEnds; NUM_PRIORITIES],
|
||||
links: [Links; MAX_THREADS],
|
||||
bitmap: u32,
|
||||
}
|
||||
|
||||
impl PriorityQueue {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
queues: core::array::from_fn(|_| RunList::new()),
|
||||
lists: [ListEnds {
|
||||
head: None,
|
||||
tail: None,
|
||||
}; NUM_PRIORITIES],
|
||||
links: [Links {
|
||||
prev: None,
|
||||
next: None,
|
||||
queued_at: None,
|
||||
}; MAX_THREADS],
|
||||
bitmap: 0,
|
||||
}
|
||||
}
|
||||
@@ -75,34 +46,84 @@ impl PriorityQueue {
|
||||
self.bitmap
|
||||
}
|
||||
|
||||
pub fn add(&mut self, thread: ThreadId, priority: Priority) {
|
||||
let idx = priority.value() as usize;
|
||||
let added = self.queues[idx].push(thread);
|
||||
debug_assert!(added, "scheduler run list full for priority {}", idx);
|
||||
if added {
|
||||
self.bitmap |= 1 << idx;
|
||||
}
|
||||
pub fn len(&self) -> usize {
|
||||
self.links.iter().filter(|l| l.queued_at.is_some()).count()
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, thread: ThreadId, priority: Priority) {
|
||||
pub fn contains(&self, thread: ThreadId) -> bool {
|
||||
self.link(thread).is_some_and(|l| l.queued_at.is_some())
|
||||
}
|
||||
|
||||
fn link(&self, thread: ThreadId) -> Option<&Links> {
|
||||
self.links.get(thread.raw() as usize)
|
||||
}
|
||||
|
||||
/// Appends `thread` at `priority`. A thread that is already queued is
|
||||
/// left where it is; returns whether it was added.
|
||||
pub fn add(&mut self, thread: ThreadId, priority: Priority) -> bool {
|
||||
let idx = priority.value() as usize;
|
||||
self.queues[idx].remove(thread);
|
||||
if self.queues[idx].is_empty() {
|
||||
let t = thread.raw() as usize;
|
||||
if t >= MAX_THREADS || idx >= NUM_PRIORITIES || self.links[t].queued_at.is_some() {
|
||||
return false;
|
||||
}
|
||||
let old_tail = self.lists[idx].tail;
|
||||
self.links[t] = Links {
|
||||
prev: old_tail,
|
||||
next: None,
|
||||
queued_at: Some(idx as u8),
|
||||
};
|
||||
match old_tail {
|
||||
Some(tail) => self.links[tail.raw() as usize].next = Some(thread),
|
||||
None => self.lists[idx].head = Some(thread),
|
||||
}
|
||||
self.lists[idx].tail = Some(thread);
|
||||
self.bitmap |= 1 << idx;
|
||||
true
|
||||
}
|
||||
|
||||
/// Unlinks `thread` from whichever level it is queued at.
|
||||
pub fn remove(&mut self, thread: ThreadId) -> bool {
|
||||
let t = thread.raw() as usize;
|
||||
let Some(Links {
|
||||
prev,
|
||||
next,
|
||||
queued_at: Some(level),
|
||||
}) = self.links.get(t).copied()
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let idx = level as usize;
|
||||
match prev {
|
||||
Some(p) => self.links[p.raw() as usize].next = next,
|
||||
None => self.lists[idx].head = next,
|
||||
}
|
||||
match next {
|
||||
Some(n) => self.links[n.raw() as usize].prev = prev,
|
||||
None => self.lists[idx].tail = prev,
|
||||
}
|
||||
self.links[t] = Links {
|
||||
prev: None,
|
||||
next: None,
|
||||
queued_at: None,
|
||||
};
|
||||
if self.lists[idx].head.is_none() {
|
||||
self.bitmap &= !(1 << idx);
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub fn next(&mut self) -> Option<(ThreadId, Priority)> {
|
||||
fn pop_level(&mut self, idx: usize) -> Option<(ThreadId, Priority)> {
|
||||
let thread = self.lists[idx].head?;
|
||||
self.remove(thread);
|
||||
Some((thread, Priority::new(idx as u8)?))
|
||||
}
|
||||
|
||||
pub fn pop_highest(&mut self) -> Option<(ThreadId, Priority)> {
|
||||
if self.bitmap == 0 {
|
||||
return None;
|
||||
}
|
||||
// highest set bit = highest priority
|
||||
let idx = 31 - self.bitmap.leading_zeros() as usize;
|
||||
let thread = self.queues[idx].pop()?;
|
||||
if self.queues[idx].is_empty() {
|
||||
self.bitmap &= !(1 << idx);
|
||||
}
|
||||
Some((thread, Priority::new(idx as u8)?))
|
||||
self.pop_level(31 - self.bitmap.leading_zeros() as usize)
|
||||
}
|
||||
|
||||
pub fn highest_priority(&self) -> Option<Priority> {
|
||||
@@ -117,15 +138,16 @@ impl PriorityQueue {
|
||||
if self.bitmap == 0 {
|
||||
return None;
|
||||
}
|
||||
let idx = self.bitmap.trailing_zeros() as usize;
|
||||
let thread = self.queues[idx].pop()?;
|
||||
if self.queues[idx].is_empty() {
|
||||
self.bitmap &= !(1 << idx);
|
||||
}
|
||||
Some((thread, Priority::new(idx as u8)?))
|
||||
self.pop_level(self.bitmap.trailing_zeros() as usize)
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.bitmap == 0
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PriorityQueue {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ fn prio(value: u8) -> Priority {
|
||||
#[test]
|
||||
fn empty_queue_returns_none() {
|
||||
let mut q = PriorityQueue::new();
|
||||
assert!(q.next().is_none());
|
||||
assert!(q.pop_highest().is_none());
|
||||
assert!(q.is_empty());
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ fn single_thread() {
|
||||
let mut q = PriorityQueue::new();
|
||||
q.add(thread(1), prio(10));
|
||||
|
||||
let (t, p) = q.next().unwrap();
|
||||
let (t, p) = q.pop_highest().unwrap();
|
||||
assert_eq!(t, thread(1));
|
||||
assert_eq!(p, prio(10));
|
||||
}
|
||||
@@ -43,7 +43,7 @@ fn highest_priority_runs_first() {
|
||||
q.add(thread(2), prio(20));
|
||||
q.add(thread(3), prio(10));
|
||||
|
||||
let (t, _) = q.next().unwrap();
|
||||
let (t, _) = q.pop_highest().unwrap();
|
||||
assert_eq!(t, thread(2));
|
||||
}
|
||||
|
||||
@@ -54,10 +54,10 @@ fn same_priority_is_fifo() {
|
||||
q.add(thread(2), prio(10));
|
||||
q.add(thread(3), prio(10));
|
||||
|
||||
assert_eq!(q.next().unwrap().0, thread(1));
|
||||
assert_eq!(q.next().unwrap().0, thread(2));
|
||||
assert_eq!(q.next().unwrap().0, thread(3));
|
||||
assert!(q.next().is_none());
|
||||
assert_eq!(q.pop_highest().unwrap().0, thread(1));
|
||||
assert_eq!(q.pop_highest().unwrap().0, thread(2));
|
||||
assert_eq!(q.pop_highest().unwrap().0, thread(3));
|
||||
assert!(q.pop_highest().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -66,18 +66,18 @@ fn remove_thread() {
|
||||
q.add(thread(1), prio(10));
|
||||
q.add(thread(2), prio(10));
|
||||
|
||||
q.remove(thread(1), prio(10));
|
||||
q.remove(thread(1));
|
||||
|
||||
let (t, _) = q.next().unwrap();
|
||||
let (t, _) = q.pop_highest().unwrap();
|
||||
assert_eq!(t, thread(2));
|
||||
assert!(q.next().is_none());
|
||||
assert!(q.pop_highest().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_clears_bitmap_when_empty() {
|
||||
let mut q = PriorityQueue::new();
|
||||
q.add(thread(1), prio(10));
|
||||
q.remove(thread(1), prio(10));
|
||||
q.remove(thread(1));
|
||||
|
||||
assert!(q.is_empty());
|
||||
}
|
||||
@@ -89,9 +89,9 @@ fn drain_all_priorities() {
|
||||
q.add(thread(2), prio(15));
|
||||
q.add(thread(3), prio(31));
|
||||
|
||||
assert_eq!(q.next().unwrap().0, thread(3)); // highest first
|
||||
assert_eq!(q.next().unwrap().0, thread(2));
|
||||
assert_eq!(q.next().unwrap().0, thread(1)); // lowest last
|
||||
assert_eq!(q.pop_highest().unwrap().0, thread(3)); // highest first
|
||||
assert_eq!(q.pop_highest().unwrap().0, thread(2));
|
||||
assert_eq!(q.pop_highest().unwrap().0, thread(1)); // lowest last
|
||||
assert!(q.is_empty());
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ fn highest_priority_peeks_without_popping() {
|
||||
q.add(thread(1), prio(5));
|
||||
q.add(thread(2), prio(20));
|
||||
assert_eq!(q.highest_priority(), Some(prio(20)));
|
||||
assert_eq!(q.next().unwrap().0, thread(2));
|
||||
assert_eq!(q.pop_highest().unwrap().0, thread(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -154,7 +154,7 @@ fn steal_lowest_takes_lowest_priority_thread() {
|
||||
let (t, p) = q.steal_lowest().unwrap();
|
||||
assert_eq!(t, thread(1));
|
||||
assert_eq!(p, prio(5));
|
||||
assert_eq!(q.next().unwrap().0, thread(2));
|
||||
assert_eq!(q.pop_highest().unwrap().0, thread(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -240,3 +240,47 @@ fn multi_threads_on_different_cores() {
|
||||
assert_eq!(s.next(core(0)), Some(thread(1)));
|
||||
assert_eq!(s.next(core(1)), Some(thread(2)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn requeued_thread_goes_behind_same_priority_peers() {
|
||||
let mut q = PriorityQueue::new();
|
||||
let p = Priority::new(5).unwrap();
|
||||
q.add(thread(1), p);
|
||||
q.add(thread(2), p);
|
||||
|
||||
assert_eq!(q.pop_highest().unwrap().0, thread(1));
|
||||
q.add(thread(1), p); // preempted, back in the run list
|
||||
|
||||
assert_eq!(q.pop_highest().unwrap().0, thread(2));
|
||||
assert_eq!(q.pop_highest().unwrap().0, thread(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_robin_survives_wraparound() {
|
||||
let mut q = PriorityQueue::new();
|
||||
let p = Priority::new(5).unwrap();
|
||||
q.add(thread(1), p);
|
||||
q.add(thread(2), p);
|
||||
q.add(thread(3), p);
|
||||
for _ in 0..40 {
|
||||
let (a, _) = q.pop_highest().unwrap();
|
||||
q.add(a, p);
|
||||
}
|
||||
// 40 rotations of 3 threads: 40 % 3 == 1, so thread 2 is at the head
|
||||
assert_eq!(q.pop_highest().unwrap().0, thread(2));
|
||||
assert_eq!(q.pop_highest().unwrap().0, thread(3));
|
||||
assert_eq!(q.pop_highest().unwrap().0, thread(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn removed_thread_is_skipped_and_order_kept() {
|
||||
let mut q = PriorityQueue::new();
|
||||
let p = Priority::new(5).unwrap();
|
||||
q.add(thread(1), p);
|
||||
q.add(thread(2), p);
|
||||
q.add(thread(3), p);
|
||||
q.remove(thread(2));
|
||||
assert_eq!(q.pop_highest().unwrap().0, thread(1));
|
||||
assert_eq!(q.pop_highest().unwrap().0, thread(3));
|
||||
assert!(q.pop_highest().is_none());
|
||||
}
|
||||
|
||||
@@ -303,3 +303,9 @@ impl ThreadTable {
|
||||
.filter(move |t| t.process() == process)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ThreadTable {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user