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}