Skip to main content

rivet/preempt/
tcb.rs

1//! Task Control Block and the preemptive task registry.
2//!
3//! Unlike the cooperative async tier (which stores task state in a
4//! compiler-generated `Future`), preemptive tasks each get their own
5//! statically-allocated stack. A context switch saves/restores the full
6//! callee-saved register set + stack pointer, so a preemptive task can be
7//! suspended at *any* point — not just at `.await` boundaries — which is
8//! what makes real priority preemption possible.
9
10use crate::sync::atomic::{AtomicU8, AtomicUsize, Ordering};
11
12/// Maximum number of preemptive tasks (RIVET_MAX_PTASKS).
13pub const MAX_PTASKS: usize = crate::config::MAX_PTASKS;
14
15/// Maximum number of mutexes a task may hold simultaneously (RIVET_MAX_HELD_MUTEXES).
16/// A task that nests deeper deadlocks its own inheritance bookkeeping — a
17/// documented, hard limit (plan.md §2.3).
18pub const MAX_HELD: usize = crate::config::MAX_HELD;
19
20/// Sentinel priority meaning "no task" in places that need one.
21pub const NO_TASK: usize = usize::MAX;
22
23#[derive(Clone, Copy, PartialEq, Eq, Debug)]
24pub enum TaskState {
25    Ready,
26    Running,
27    Blocked,
28}
29
30/// One entry in a task's held-mutex list. `ptr` is the type-erased
31/// `PriorityMutex` address (0 = empty); `hwp` is that mutex's monomorphized
32/// "highest waiter base priority" accessor, so the list can stay
33/// heterogeneous ([B11]: unlocking one mutex must not clobber the boost
34/// held for another).
35pub struct HeldMutex {
36    /// Type-erased `PriorityMutex` pointer; null = empty slot. `AtomicPtr`
37    /// (not an integer atomic) so pointer provenance survives the
38    /// store/load round-trip (miri strict-provenance requirement).
39    pub ptr: crate::sync::atomic::AtomicPtr<()>,
40    /// That mutex's "highest waiter base priority" accessor,
41    /// `fn(*const ()) -> u8` stored as a raw pointer. Written before `ptr`
42    /// (Release), read after loading `ptr` (Acquire).
43    pub hwp: crate::sync::atomic::AtomicPtr<()>,
44}
45
46impl HeldMutex {
47    #[cfg(not(loom))]
48    pub const fn empty() -> Self {
49        Self::empty_impl()
50    }
51
52    #[cfg(loom)]
53    pub fn empty() -> Self {
54        Self::empty_impl()
55    }
56
57    #[cfg(not(loom))]
58    const fn empty_impl() -> Self {
59        Self {
60            ptr: crate::sync::atomic::AtomicPtr::new(core::ptr::null_mut()),
61            hwp: crate::sync::atomic::AtomicPtr::new(core::ptr::null_mut()),
62        }
63    }
64
65    #[cfg(loom)]
66    fn empty_impl() -> Self {
67        Self {
68            ptr: crate::sync::atomic::AtomicPtr::new(core::ptr::null_mut()),
69            hwp: crate::sync::atomic::AtomicPtr::new(core::ptr::null_mut()),
70        }
71    }
72}
73
74/// Task Control Block. One per preemptive task, held in the static registry.
75pub struct Tcb {
76    /// Saved stack pointer. Valid only while the task is not running.
77    pub sp: AtomicUsize,
78    /// Priority as declared by the task (0 = lowest, 31 = highest).
79    pub base_priority: AtomicU8,
80    /// Priority currently in effect. Normally equals `base_priority`;
81    /// temporarily boosted by [`crate::preempt::mutex::PriorityMutex`] to
82    /// the priority of whichever higher-priority task is blocked waiting
83    /// on a resource this task holds (priority inheritance — prevents
84    /// priority inversion).
85    pub effective_priority: AtomicU8,
86    /// Current scheduling state.
87    pub state: crate::sync::atomic::AtomicU8, // encodes TaskState
88    /// Whether this slot holds a live task.
89    pub used: crate::sync::atomic::AtomicBool,
90    /// Stack base address (low end) and size in bytes, recorded at spawn.
91    /// Used by the CM3 MPU per-switch stack region, RISC-V PMP guard
92    /// attribution, and stack watermarking (plan.md §3).
93    pub stack_base: crate::sync::atomic::AtomicUsize,
94    pub stack_size: crate::sync::atomic::AtomicUsize,
95    /// Intrusive list of mutexes this task currently holds, for correct
96    /// nested priority-inheritance recomputation on unlock (plan.md [B11]).
97    pub held: [HeldMutex; MAX_HELD],
98    pub held_count: crate::sync::atomic::AtomicU8,
99    /// Last task-level watchdog checkin time (µs, low 32 bits); 0 = never
100    /// checked in (plan.md §3.5).
101    pub last_checkin: crate::sync::atomic::AtomicU32,
102
103    /// Slot generation counter, incremented on every register (slot
104    /// reuse). Lets `TaskHandle`-based APIs detect a stale handle (ABA on
105    /// slot recycling, plan.md §5.1).
106    pub generation: crate::sync::atomic::AtomicU32,
107    /// Type-erased return value storage (plan.md §5.2): `result_size` bytes
108    /// of `result_buf`, written exactly once by `rivet_task_exit` before
109    /// `exited` is published. Read via `TaskHandle::join`.
110    pub result_buf: core::cell::UnsafeCell<[u8; 32]>,
111    pub result_size: crate::sync::atomic::AtomicU8,
112    /// Type-erased `drop_in_place` for the stored result (0 = no-op).
113    pub result_drop: crate::sync::atomic::AtomicUsize,
114    /// Set by `rivet_task_exit` when the task's entry returned.
115    pub exited: crate::sync::atomic::AtomicBool,
116    /// Id of the task blocked in `join()` on this task (or NO_TASK).
117    pub joiner: crate::sync::atomic::AtomicUsize,
118    /// Cooperative cancellation flag (plan.md §5.4): set by
119    /// `TaskHandle::request_stop`, polled by `should_stop()`.
120    pub stop_requested: crate::sync::atomic::AtomicBool,
121}
122
123pub(crate) const READY: u8 = 0;
124pub(crate) const RUNNING: u8 = 1;
125pub(crate) const BLOCKED: u8 = 2;
126/// Transient claim state used only during `register()`: a slot whose state
127/// is RESERVED has been claimed but not yet published (its `used` flag is
128/// still false), so the scheduler can never observe it half-initialized
129/// (plan.md [B2]).
130pub(crate) const RESERVED: u8 = 3;
131/// Task paused by `TaskHandle::pause` (plan.md §5.5) — skipped by the
132/// scheduler until resumed. Never runnable on its own.
133pub(crate) const SUSPENDED: u8 = 4;
134
135impl Tcb {
136    /// The task's stack allocation `(base, size)` from the pool (0,0 if
137    /// none — host fallback stacks).
138    pub fn stack_info(&self) -> Option<(usize, usize)> {
139        let base = self.stack_base.load(Ordering::Acquire);
140        let size = self.stack_size.load(Ordering::Acquire);
141        if base == 0 || size == 0 {
142            None
143        } else {
144            Some((base, size))
145        }
146    }
147
148    #[cfg(not(loom))]
149    pub const fn new() -> Self {
150        Self {
151            sp: AtomicUsize::new(0),
152            base_priority: AtomicU8::new(0),
153            effective_priority: AtomicU8::new(0),
154            state: crate::sync::atomic::AtomicU8::new(READY),
155            used: crate::sync::atomic::AtomicBool::new(false),
156            stack_base: crate::sync::atomic::AtomicUsize::new(0),
157            stack_size: crate::sync::atomic::AtomicUsize::new(0),
158            held: [const { HeldMutex::empty() }; MAX_HELD],
159            held_count: crate::sync::atomic::AtomicU8::new(0),
160            last_checkin: crate::sync::atomic::AtomicU32::new(0),
161            generation: crate::sync::atomic::AtomicU32::new(0),
162            result_buf: core::cell::UnsafeCell::new([0u8; 32]),
163            result_size: crate::sync::atomic::AtomicU8::new(0),
164            result_drop: crate::sync::atomic::AtomicUsize::new(0),
165            exited: crate::sync::atomic::AtomicBool::new(false),
166            joiner: crate::sync::atomic::AtomicUsize::new(NO_TASK),
167            stop_requested: crate::sync::atomic::AtomicBool::new(false),
168        }
169    }
170
171    /// Loom's atomics are not const-constructible, so under `--cfg loom`
172    /// `new` is a runtime function (used by the loom models).
173    #[cfg(loom)]
174    pub fn new() -> Self {
175        Self {
176            sp: AtomicUsize::new(0),
177            base_priority: AtomicU8::new(0),
178            effective_priority: AtomicU8::new(0),
179            state: crate::sync::atomic::AtomicU8::new(READY),
180            used: crate::sync::atomic::AtomicBool::new(false),
181            stack_base: crate::sync::atomic::AtomicUsize::new(0),
182            stack_size: crate::sync::atomic::AtomicUsize::new(0),
183            held: core::array::from_fn(|_| HeldMutex::empty()),
184            held_count: crate::sync::atomic::AtomicU8::new(0),
185            last_checkin: crate::sync::atomic::AtomicU32::new(0),
186            generation: crate::sync::atomic::AtomicU32::new(0),
187            result_buf: core::cell::UnsafeCell::new([0u8; 32]),
188            result_size: crate::sync::atomic::AtomicU8::new(0),
189            result_drop: crate::sync::atomic::AtomicUsize::new(0),
190            exited: crate::sync::atomic::AtomicBool::new(false),
191            joiner: crate::sync::atomic::AtomicUsize::new(NO_TASK),
192            stop_requested: crate::sync::atomic::AtomicBool::new(false),
193        }
194    }
195
196    /// Record a mutex in this task's held list. Returns false if the list
197    /// is full ([`MAX_HELD`]).
198    pub fn push_held(&self, ptr: *const (), hwp: fn(*const ()) -> u8) -> bool {
199        if self.held_count.load(crate::sync::atomic::Ordering::Acquire) as usize >= MAX_HELD {
200            return false;
201        }
202        for slot in &self.held {
203            if slot
204                .ptr
205                .load(crate::sync::atomic::Ordering::Acquire)
206                .is_null()
207            {
208                // hwp is written before the ptr store; readers load ptr
209                // with Acquire, then hwp with Acquire, so the fn pointer
210                // is visible (task-context single writer per slot).
211                slot.hwp.store(
212                    hwp as *const () as *mut (),
213                    crate::sync::atomic::Ordering::Release,
214                );
215                slot.ptr
216                    .store(ptr as *mut (), crate::sync::atomic::Ordering::Release);
217                self.held_count
218                    .fetch_add(1, crate::sync::atomic::Ordering::Release);
219                return true;
220            }
221        }
222        false
223    }
224
225    /// Remove a mutex from this task's held list (no-op if absent).
226    pub fn remove_held(&self, ptr: *const ()) {
227        for slot in &self.held {
228            let loaded = slot.ptr.load(crate::sync::atomic::Ordering::Acquire);
229            if core::ptr::eq(loaded, ptr) {
230                slot.ptr.store(
231                    core::ptr::null_mut(),
232                    crate::sync::atomic::Ordering::Release,
233                );
234                self.held_count
235                    .fetch_sub(1, crate::sync::atomic::Ordering::Release);
236                return;
237            }
238        }
239    }
240
241    pub fn state(&self) -> TaskState {
242        match self.state.load(Ordering::Acquire) {
243            RUNNING => TaskState::Running,
244            BLOCKED => TaskState::Blocked,
245            _ => TaskState::Ready,
246        }
247    }
248
249    /// Set the scheduling state and keep the O(1) scheduler's ready
250    /// queues consistent (plan.md §4.2): Ready tasks are queued at their
251    /// effective priority; Running/Blocked tasks are not queued.
252    pub fn set_state(&self, id: usize, s: TaskState) {
253        let v = match s {
254            TaskState::Ready => READY,
255            TaskState::Running => RUNNING,
256            TaskState::Blocked => BLOCKED,
257        };
258        self.state.store(v, Ordering::Release);
259        // Deliberately NOT tracing Ready/Blocked transitions here: every
260        // caller of `set_state` (this function's own doc + grep confirms
261        // it — `on_tick_locked`, `sleep_until`, mutex blocking, `pause`/
262        // `resume`) holds `critical::enter` (PRIMASK masked) around the
263        // call. A trace emission is a blocking, byte-at-a-time polling
264        // UART write (`docs/wcet.md` §6.1's own documented hazard) — for
265        // two equal-priority tasks that round-robin every tick (a real
266        // case this exact bug was found on: two workers permanently
267        // starved because their *entire* tick budget went to a blocking
268        // trace_write of the outgoing task's Ready transition, leaving
269        // ~0 time to actually run before the next tick, already pending,
270        // fired) this alone was enough to prevent either task from ever
271        // making forward progress. `ContextSwitch` (emitted from
272        // `preempt::on_tick`, outside its own critical section — see
273        // that function's docs) already covers the common case; a
274        // `TaskBlocked`/`TaskReady` event would need the same deferred-
275        // outside-the-lock treatment before it's safe to add here.
276        match s {
277            TaskState::Ready => crate::preempt::sched::ready_add(id),
278            TaskState::Running | TaskState::Blocked => crate::preempt::sched::ready_remove(id),
279        }
280    }
281
282    /// Set the effective priority (priority inheritance) and move the task
283    /// between ready queues if it is currently Ready (plan.md §4.2).
284    pub fn set_effective_priority(&self, id: usize, new: u8) {
285        let old = self.effective_priority.load(Ordering::Acquire);
286        self.effective_priority.store(new, Ordering::Release);
287        crate::preempt::sched::on_effective_priority_change(id, old, new);
288    }
289}
290
291// Safety: all fields are atomics; Tcb is placed in a static array accessed
292// by the scheduler (task context) and timer ISR (interrupt context).
293unsafe impl Sync for Tcb {}
294
295impl Default for Tcb {
296    fn default() -> Self {
297        Self::new()
298    }
299}
300
301/// The static task registry. Fixed-size, no allocation.
302#[cfg(not(loom))]
303pub static TASKS: [Tcb; MAX_PTASKS] = [const { Tcb::new() }; MAX_PTASKS];
304
305#[cfg(loom)]
306loom::lazy_static! {
307    // Same registry under loom (`Tcb::new` is not const-constructible).
308    pub static ref TASKS: [Tcb; MAX_PTASKS] = core::array::from_fn(|_| Tcb::new());
309}
310
311/// Register a new preemptive task in the first free slot.
312/// `sp` is the pre-built initial stack pointer (see `port::arch::init_task_stack`).
313/// Returns the assigned task id, or `None` if the registry is full.
314///
315/// Publish ordering (plan.md [B2]): the slot is *claimed* by CASing its
316/// state READY→RESERVED (the `used` flag stays false, so the scheduler —
317/// which only considers `used` slots — cannot observe it), all fields are
318/// written, and only then is `used` published `true` (Release). A tick
319/// that lands mid-registration sees either a fully-initialized slot or no
320/// slot at all — never a `used`, `Ready` TCB with `sp == 0`.
321/// Register a preemptive task with its full stack description.
322/// `stack_base`/`stack_size` describe the task's stack allocation (used by
323/// MPU/PMP guards and watermarking, plan.md §3); pass (0, 0) when unknown.
324pub fn register_full(
325    sp: usize,
326    priority: u8,
327    stack_base: usize,
328    stack_size: usize,
329) -> Option<usize> {
330    for (id, tcb) in TASKS.iter().enumerate() {
331        if tcb.used.load(Ordering::Acquire) {
332            continue;
333        }
334        // Claim: only a free slot can be READY→RESERVED. A live task is
335        // never READY-with-used=false, so this can't steal a live slot.
336        if tcb
337            .state
338            .compare_exchange(READY, RESERVED, Ordering::AcqRel, Ordering::Acquire)
339            .is_ok()
340        {
341            // Publish the fields in dependency order; `used = true` last
342            // (Release) so any reader that sees `used` also sees every
343            // field (Release→Acquire chain).
344            tcb.sp.store(sp, Ordering::Release);
345            tcb.base_priority.store(priority, Ordering::Release);
346            tcb.effective_priority.store(priority, Ordering::Release);
347            tcb.stack_base.store(stack_base, Ordering::Release);
348            tcb.stack_size.store(stack_size, Ordering::Release);
349            // Drop any previously-stored result from a recycled slot.
350            let drop_fn = tcb.result_drop.load(Ordering::Acquire);
351            if drop_fn != 0 {
352                // SAFETY: the type-erased drop fn was registered by
353                // `spawn` for the exact T stored in the buffer.
354                let f: fn(*mut u8) = unsafe { core::mem::transmute(drop_fn) };
355                // SAFETY: result_buf holds a live T when result_drop != 0.
356                f(tcb.result_buf.get() as *mut u8);
357                tcb.result_drop.store(0, Ordering::Release);
358            }
359            // Slot recycling must be self-sufficient: don't rely on the
360            // previous occupant's own exit/fault path having drained
361            // `joiner` back to `NO_TASK` (found via soak testing at
362            // scale, plan.md Phase 17 — a task that exits at a *higher*
363            // priority than a not-yet-registered joiner drains a
364            // `joiner` field that's still `NO_TASK`, then the joiner's
365            // own CAS lands on a slot with nobody left to ever clear it,
366            // and the *next* occupant of the recycled slot inherits a
367            // permanently-stuck `joiner`). Resetting every join/exit-
368            // lifecycle field here, inside the RESERVED window (`used`
369            // is still `false`, so nothing else can observe this slot
370            // yet), makes this the single authoritative reset point
371            // regardless of how the previous occupant left.
372            tcb.joiner.store(NO_TASK, Ordering::Release);
373            tcb.exited.store(false, Ordering::Release);
374            tcb.stop_requested.store(false, Ordering::Release);
375            tcb.result_size.store(0, Ordering::Release);
376            tcb.held_count.store(0, Ordering::Release);
377            tcb.state.store(READY, Ordering::Release);
378            tcb.used.store(true, Ordering::Release);
379            tcb.generation.fetch_add(1, Ordering::Release);
380            crate::preempt::sched::ready_add(id);
381            return Some(id);
382        }
383    }
384    None
385}
386
387/// Register a new preemptive task in the first free slot.
388/// `sp` is the pre-built initial stack pointer (see `port::arch::init_task_stack`).
389/// Returns the assigned task id, or `None` if the registry is full.
390pub fn register(sp: usize, priority: u8) -> Option<usize> {
391    register_full(sp, priority, 0, 0)
392}
393
394pub fn get(id: usize) -> Option<&'static Tcb> {
395    TASKS.get(id).filter(|t| t.used.load(Ordering::Acquire))
396}
397
398/// Test-only: mark every TCB slot unused. Part of the global reset done by
399/// [`crate::kernel_test!`].
400#[cfg(feature = "test-support")]
401pub(crate) fn reset_for_test() {
402    for tcb in TASKS.iter() {
403        tcb.used.store(false, Ordering::Release);
404        tcb.state.store(READY, Ordering::Release);
405        tcb.sp.store(0, Ordering::Release);
406        tcb.base_priority.store(0, Ordering::Release);
407        tcb.effective_priority.store(0, Ordering::Release);
408        tcb.exited.store(false, Ordering::Release);
409        tcb.result_size.store(0, Ordering::Release);
410        tcb.result_drop.store(0, Ordering::Release);
411        tcb.joiner.store(NO_TASK, Ordering::Release);
412        tcb.stop_requested.store(false, Ordering::Release);
413    }
414}
415
416#[cfg(test)]
417mod tests {
418    use super::*;
419
420    #[test]
421    fn register_assigns_first_free_slot_and_sets_fields() {
422        crate::kernel_test! {
423            let a = register(0x1000, 3).unwrap();
424            assert_eq!(a, 0, "first free slot is 0");
425            let b = register(0x2000, 5).unwrap();
426            assert_eq!(b, 1);
427            // Fields must be fully published by the time register returns
428            // (plan.md [B2]).
429            let ta = get(a).unwrap();
430            assert_eq!(ta.sp.load(Ordering::Acquire), 0x1000);
431            assert_eq!(ta.base_priority.load(Ordering::Acquire), 3);
432            assert_eq!(ta.effective_priority.load(Ordering::Acquire), 3);
433            assert_eq!(ta.state(), TaskState::Ready);
434        }
435    }
436
437    #[test]
438    fn register_full_returns_none() {
439        crate::kernel_test! {
440            for i in 0..MAX_PTASKS {
441                assert!(register(0x1000 + i, 1).is_some(), "slot {i}");
442            }
443            assert_eq!(register(0x9000, 1), None, "registry full");
444        }
445    }
446}