use crate::sync::atomic::{AtomicU32, AtomicUsize, Ordering};
use super::tcb::{TaskState, MAX_PTASKS, NO_TASK, TASKS};
const MAX_HARTS: usize = crate::config::MAX_HARTS;
#[cfg(feature = "latency-histograms")]
static READY_AT_CYCLE: [AtomicU32; MAX_PTASKS] = [const { AtomicU32::new(0) }; MAX_PTASKS];
#[cfg(not(loom))]
static READY_BITMAP: AtomicU32 = AtomicU32::new(0);
#[cfg(loom)]
loom::lazy_static! {
static ref READY_BITMAP: AtomicU32 = AtomicU32::new(0);
}
#[cfg(not(loom))]
static QUEUES: [AtomicU32; 32] = [const { AtomicU32::new(0) }; 32];
#[cfg(loom)]
loom::lazy_static! {
static ref QUEUES: [AtomicU32; 32] = core::array::from_fn(|_| AtomicU32::new(0));
}
#[cfg(not(loom))]
static CURRENT: [AtomicUsize; MAX_HARTS] = [const { AtomicUsize::new(NO_TASK) }; MAX_HARTS];
#[cfg(loom)]
loom::lazy_static! {
static ref CURRENT: [AtomicUsize; MAX_HARTS] = core::array::from_fn(|_| AtomicUsize::new(NO_TASK));
}
#[cfg(not(loom))]
static DISPATCH_COUNTER: AtomicU32 = AtomicU32::new(0);
#[cfg(loom)]
loom::lazy_static! {
static ref DISPATCH_COUNTER: AtomicU32 = AtomicU32::new(0);
}
#[cfg(not(loom))]
static DISPATCH_SEQ: [AtomicU32; MAX_PTASKS] = [const { AtomicU32::new(0) }; MAX_PTASKS];
#[cfg(loom)]
loom::lazy_static! {
static ref DISPATCH_SEQ: [AtomicU32; MAX_PTASKS] = core::array::from_fn(|_| AtomicU32::new(0));
}
pub fn current() -> Option<usize> {
let c = CURRENT[crate::port::arch::hart_id()].load(Ordering::Acquire);
if c == NO_TASK {
None
} else {
Some(c)
}
}
pub fn set_current(id: usize) {
CURRENT[crate::port::arch::hart_id()].store(id, Ordering::Release);
}
pub fn ready_add(id: usize) {
if id >= MAX_PTASKS {
return;
}
#[cfg(feature = "latency-histograms")]
READY_AT_CYCLE[id].store(
crate::port::arch::cycle_count() as u32,
Ordering::Relaxed,
);
let prio = TASKS[id].effective_priority.load(Ordering::Acquire) as usize;
QUEUES[prio].fetch_or(1u32 << id, Ordering::Release);
READY_BITMAP.fetch_or(1u32 << prio, Ordering::Release);
wake_other_harts();
}
#[inline]
fn wake_other_harts() {
if MAX_HARTS > 1 {
let me = crate::port::arch::hart_id();
for hart in 0..MAX_HARTS {
if hart != me {
crate::port::arch::request_reschedule_on(hart);
}
}
}
}
pub fn ready_remove(id: usize) {
if id >= MAX_PTASKS {
return;
}
let prio = TASKS[id].effective_priority.load(Ordering::Acquire) as usize;
let prev = QUEUES[prio].fetch_and(!(1u32 << id), Ordering::AcqRel);
if prev == (1u32 << id) {
READY_BITMAP.fetch_and(!(1u32 << prio), Ordering::AcqRel);
}
}
pub fn on_effective_priority_change(id: usize, old_prio: u8, new_prio: u8) {
if id >= MAX_PTASKS || old_prio == new_prio {
return;
}
if TASKS[id].state() != TaskState::Ready {
return;
}
let bit = 1u32 << id;
let prev = QUEUES[old_prio as usize].fetch_and(!bit, Ordering::AcqRel);
if prev == bit {
READY_BITMAP.fetch_and(!(1u32 << old_prio), Ordering::AcqRel);
}
QUEUES[new_prio as usize].fetch_or(bit, Ordering::Release);
READY_BITMAP.fetch_or(1u32 << new_prio, Ordering::Release);
}
pub fn schedule() -> Option<usize> {
let mut bitmap = READY_BITMAP.load(Ordering::Acquire);
loop {
if bitmap == 0 {
return None;
}
let prio = (31 - bitmap.leading_zeros()) as usize;
let word = QUEUES[prio].load(Ordering::Acquire);
if word == 0 {
bitmap = READY_BITMAP.fetch_and(!(1u32 << prio), Ordering::AcqRel) & !(1u32 << prio);
continue;
}
let mut best_id = None;
let mut best_seq = u32::MAX;
let mut w = word;
while w != 0 {
let id = w.trailing_zeros() as usize;
w &= w - 1; if id < MAX_PTASKS {
let seq = DISPATCH_SEQ[id].load(Ordering::Relaxed);
if seq < best_seq {
best_seq = seq;
best_id = Some(id);
}
}
}
if best_id.is_some() {
return best_id;
}
return word
.trailing_zeros()
.checked_rem(MAX_PTASKS as u32)
.map(|i| i as usize);
}
}
pub fn on_dispatch(id: usize) {
if id < MAX_PTASKS {
let seq = DISPATCH_COUNTER.fetch_add(1, Ordering::Relaxed).wrapping_add(1);
DISPATCH_SEQ[id].store(seq, Ordering::Relaxed);
}
#[cfg(feature = "latency-histograms")]
if id < MAX_PTASKS {
let ready_at = READY_AT_CYCLE[id].load(Ordering::Relaxed);
if ready_at != 0 {
let now = crate::port::arch::cycle_count() as u32;
crate::latency::record(
crate::latency::Kind::SchedulingWake,
now.wrapping_sub(ready_at) as u64,
);
}
}
}
pub fn should_preempt(candidate: usize, running: usize) -> bool {
if candidate == running {
return false;
}
let cand_prio = TASKS[candidate].effective_priority.load(Ordering::Acquire);
let run_prio = TASKS[running].effective_priority.load(Ordering::Acquire);
cand_prio >= run_prio
}
pub fn unblock(id: usize) {
if let Some(tcb) = super::tcb::get(id) {
tcb.set_state(id, TaskState::Ready);
}
}
pub fn block_current() {
if let Some(id) = current() {
if let Some(tcb) = super::tcb::get(id) {
tcb.set_state(id, TaskState::Blocked);
}
}
}
#[cfg(feature = "test-support")]
pub(crate) fn reset_for_test() {
for c in CURRENT.iter() {
c.store(NO_TASK, Ordering::Release);
}
DISPATCH_COUNTER.store(0, Ordering::Release);
for s in DISPATCH_SEQ.iter() {
s.store(0, Ordering::Release);
}
READY_BITMAP.store(0, Ordering::Release);
for q in QUEUES.iter() {
q.store(0, Ordering::Release);
}
#[cfg(feature = "latency-histograms")]
for s in READY_AT_CYCLE.iter() {
s.store(0, Ordering::Release);
}
}
#[cfg(feature = "test-support")]
pub fn ready_bitmap_for_test() -> u32 {
READY_BITMAP.load(Ordering::Acquire)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::preempt::tcb;
#[test]
fn schedule_picks_highest_priority() {
crate::kernel_test! {
let _a = tcb::register(0x1000, 0).unwrap();
let b = tcb::register(0x2000, 5).unwrap();
let _c = tcb::register(0x3000, 2).unwrap();
assert_eq!(schedule(), Some(b));
}
}
#[test]
fn schedule_round_robins_same_priority() {
crate::kernel_test! {
let a = tcb::register(0x1000, 1).unwrap();
let b = tcb::register(0x2000, 1).unwrap();
let mut saw_a = false;
let mut saw_b = false;
for _ in 0..10 {
let id = schedule().unwrap();
match id {
id if id == a => saw_a = true,
id if id == b => saw_b = true,
_ => {}
}
on_dispatch(id); }
assert!(saw_a && saw_b, "both tied tasks must be dispatched");
}
}
#[test]
fn rr_advances_only_on_dispatch() {
crate::kernel_test! {
let a = tcb::register(0x1000, 1).unwrap();
let b = tcb::register(0x2000, 1).unwrap();
assert_eq!(schedule(), Some(a));
assert_eq!(schedule(), Some(a));
on_dispatch(a);
assert_eq!(schedule(), Some(b));
on_dispatch(b);
assert_eq!(schedule(), Some(a));
}
}
#[test]
fn blocked_tasks_not_scheduled() {
crate::kernel_test! {
let a = tcb::register(0x1000, 3).unwrap();
let b = tcb::register(0x2000, 1).unwrap();
tcb::get(a).unwrap().set_state(a, TaskState::Blocked);
assert_eq!(schedule(), Some(b));
}
}
#[test]
fn should_preempt_logic() {
crate::kernel_test! {
let low = tcb::register(0x1000, 1).unwrap();
let high = tcb::register(0x2000, 5).unwrap();
assert!(should_preempt(high, low));
assert!(!should_preempt(low, high));
assert!(!should_preempt(low, low));
}
}
#[test]
fn effective_priority_used_for_scheduling() {
crate::kernel_test! {
let low = tcb::register(0x1000, 1).unwrap();
let _high = tcb::register(0x2000, 5).unwrap();
tcb::get(low)
.unwrap()
.set_effective_priority(low, 9);
assert_eq!(schedule(), Some(low));
}
}
#[test]
fn ready_queue_consistent_with_state() {
crate::kernel_test! {
let a = tcb::register(0x1000, 2).unwrap();
let b = tcb::register(0x2000, 2).unwrap();
assert_ne!(ready_bitmap_for_test() & (1 << 2), 0);
tcb::get(b).unwrap().set_state(b, TaskState::Blocked);
assert_eq!(schedule(), Some(a));
tcb::get(a).unwrap().set_state(a, TaskState::Running);
assert_eq!(schedule(), None, "nothing ready");
tcb::get(b).unwrap().set_state(b, TaskState::Ready);
assert_eq!(schedule(), Some(b));
}
}
}
#[cfg(kani)]
mod kani_proofs {
use super::*;
fn assert_bitmap_matches_queues() {
let bitmap = READY_BITMAP.load(Ordering::Acquire);
for prio in 0..32usize {
let word = QUEUES[prio].load(Ordering::Acquire);
let bit_set = (bitmap & (1u32 << prio)) != 0;
assert_eq!(
bit_set,
word != 0,
"READY_BITMAP bit {prio} = {bit_set} but QUEUES[{prio}] = {word:#x}"
);
}
}
#[kani::proof]
fn ready_add_remove_preserves_invariant() {
reset_for_test();
assert_bitmap_matches_queues();
let id: usize = kani::any();
kani::assume(id < MAX_PTASKS);
let prio: u8 = kani::any();
kani::assume((prio as usize) < 32);
TASKS[id].effective_priority.store(prio, Ordering::Release);
ready_add(id);
assert_bitmap_matches_queues();
ready_remove(id);
assert_bitmap_matches_queues();
}
#[kani::proof]
fn two_task_interleaving_preserves_invariant() {
reset_for_test();
let id_a: usize = kani::any();
let id_b: usize = kani::any();
kani::assume(id_a < MAX_PTASKS);
kani::assume(id_b < MAX_PTASKS);
kani::assume(id_a != id_b);
let prio_a: u8 = kani::any();
let prio_b: u8 = kani::any();
kani::assume((prio_a as usize) < 32);
kani::assume((prio_b as usize) < 32);
TASKS[id_a]
.effective_priority
.store(prio_a, Ordering::Release);
TASKS[id_b]
.effective_priority
.store(prio_b, Ordering::Release);
ready_add(id_a);
assert_bitmap_matches_queues();
ready_add(id_b);
assert_bitmap_matches_queues();
ready_remove(id_a);
assert_bitmap_matches_queues();
ready_remove(id_b);
assert_bitmap_matches_queues();
}
}