nros_node/executor/exec_lock.rs
1//! phase-509 P9b item 1 (RFC-0108 "As built in P9b") -- the executor's lock.
2//!
3//! RFC-0108 runs one executor per core with one worker thread per preemption
4//! level. Every path that touches the executor's state -- a worker's pass, and
5//! every C / C++ FFI entry a callback or another thread calls -- then has to be
6//! serialised, and that is this lock: a port-provided, RECURSIVE,
7//! PRIORITY-INHERITING mutex (`nros_platform_mutex_rec_*` on every port), which
8//! the runner installs on the executor before a second thread can reach it.
9//!
10//! An executor with no lock installed (every one-level image, every Rust
11//! `&mut` user) takes the `None` arm: a guard is one predictable branch and
12//! nothing else, so this seam changes no behaviour where it is not installed.
13//!
14//! Why recursive: an owned pass (`spin_once(&mut self)`), and a pass whose
15//! lock has no thread identity, hold the lock across each user callback, so a
16//! callback that calls back into the executor (`timer_cancel`, `*_release`, a
17//! parameter call) re-enters it on the same thread.
18//!
19//! Item 2 -- the callback window. A shared or band pass
20//! (`Executor::spin_once_shared`, `Executor::spin_band_once`) whose lock
21//! carries the port's thread identity (`ExecLock::with_thread_id`) gives
22//! the lock for each user callback and takes it back after (`Window`). The
23//! entry stays marked running (its slot tag), so no other pass dispatches or
24//! probes it and its release is deferred; a `Running` record names the
25//! thread running it, so an entry operation from another thread is told apart
26//! from one the callback makes itself: the entry accessors answer as for a
27//! stale handle, and a timer cancel or reset is deferred to the callback's
28//! return.
29//!
30//! Why priority-inheriting: a less urgent worker holding the lock for a
31//! bookkeeping step must run at the waiter's priority, so a more urgent
32//! level's blocking is bounded by ONE critical section (RFC-0108 B_i).
33//!
34//! Interrupt handlers never take it: arrivals go through the scheduler's
35//! lock-free ready word and the wake object, exactly as before.
36
37use core::{ffi::c_void, ptr::NonNull};
38
39/// A port's lock or unlock entry, `nros_platform_mutex_rec_lock`'s shape:
40/// `0` on success, negative on failure.
41pub type ExecLockFn = unsafe extern "C" fn(*mut c_void) -> i8;
42
43/// The calling thread's identity, as the port names it (`k_current_get`,
44/// `pthread_self`): equal on one thread, distinct across live threads.
45pub type ThreadIdFn = unsafe extern "C" fn() -> usize;
46
47/// The executor's lock: the port's recursive priority-inheriting mutex and
48/// the two calls that take and give it. Installed once, before any second
49/// thread can reach the executor ([`super::Executor::set_exec_lock`]), and
50/// never changed while one can.
51#[derive(Clone, Copy, Debug)]
52pub struct ExecLock {
53 ctx: NonNull<c_void>,
54 lock: ExecLockFn,
55 unlock: ExecLockFn,
56 /// phase-509 P9b item 2 -- the calling thread's identity. With it, a pass
57 /// releases the lock around each user callback (a "window"), and an
58 /// operation on an entry whose callback runs in another thread's window
59 /// is told apart from one the callback makes itself. Without it the lock
60 /// is held for the whole pass, as in item 1.
61 #[cfg_attr(not(any(has_rmw, test)), allow(dead_code))] // read by the pass
62 thread_id: Option<ThreadIdFn>,
63}
64
65// SAFETY: the context is the port's mutex, whose whole purpose is to be shared
66// between threads; the two entries are plain functions.
67unsafe impl Send for ExecLock {}
68// SAFETY: as above -- `lock` / `unlock` are the port's thread-safe calls.
69unsafe impl Sync for ExecLock {}
70
71impl ExecLock {
72 /// A lock over the port's mutex at `ctx`, already initialised by the
73 /// caller. `None` for a NULL `ctx`.
74 ///
75 /// # Safety
76 /// `ctx` is NULL or an initialised recursive mutex that `lock` / `unlock`
77 /// take and give, and it outlives every executor the lock is installed on.
78 pub unsafe fn new(ctx: *mut c_void, lock: ExecLockFn, unlock: ExecLockFn) -> Option<Self> {
79 Some(Self {
80 ctx: NonNull::new(ctx)?,
81 lock,
82 unlock,
83 thread_id: None,
84 })
85 }
86
87 /// This lock, with the port's thread identity: a pass that holds it
88 /// releases it around each user callback (phase-509 P9b item 2).
89 ///
90 /// # Safety
91 /// `id` answers the calling thread's identity, from any thread, equal on
92 /// one thread and distinct across threads that are alive together.
93 pub unsafe fn with_thread_id(self, id: ThreadIdFn) -> Self {
94 Self {
95 thread_id: Some(id),
96 ..self
97 }
98 }
99
100 /// Whether a pass may release this lock around a callback: only when it
101 /// can tell the callback's own thread from the others.
102 #[cfg(any(has_rmw, test))]
103 pub(crate) fn opens_windows(&self) -> bool {
104 self.thread_id.is_some()
105 }
106
107 /// The calling thread's identity; `None` without a [`ThreadIdFn`].
108 #[cfg(any(has_rmw, test))]
109 pub(crate) fn current_thread(&self) -> Option<usize> {
110 // SAFETY: `with_thread_id`'s contract: callable from any thread.
111 self.thread_id.map(|id| unsafe { id() })
112 }
113
114 pub(crate) fn take(&self) {
115 // SAFETY: `new`'s contract: the context is the initialised mutex these
116 // entries take. A failed take is not recoverable here (the port's
117 // mutex never fails on a valid handle); the guard proceeds, which is
118 // the behaviour the image had before the lock existed.
119 let _ = unsafe { (self.lock)(self.ctx.as_ptr()) };
120 }
121
122 pub(crate) fn give(&self) {
123 // SAFETY: as `take`; only ever called by the guard that took it.
124 let _ = unsafe { (self.unlock)(self.ctx.as_ptr()) };
125 }
126}
127
128/// Holds the executor's lock for its own lifetime: taken at
129/// [`ExecGuard::acquire`], given at drop. With no lock installed it holds
130/// nothing.
131#[must_use = "the lock is given back when the guard drops"]
132pub struct ExecGuard {
133 lock: Option<ExecLock>,
134}
135
136impl ExecGuard {
137 /// Take `lock`, if there is one.
138 pub fn take(lock: Option<ExecLock>) -> Self {
139 if let Some(l) = &lock {
140 l.take();
141 }
142 Self { lock }
143 }
144
145 /// Take the lock installed on the executor at `exec`, BEFORE any reference
146 /// to that executor exists -- the order that keeps a second thread's
147 /// reference from overlapping this one.
148 ///
149 /// # Safety
150 /// `exec` points at a live, initialised executor. Its lock field is read
151 /// without a reference: it is written only while no second thread can
152 /// reach the executor ([`super::Executor::set_exec_lock`]).
153 #[cfg(any(has_rmw, test))]
154 pub unsafe fn acquire(exec: *const super::Executor<'_>) -> Self {
155 // SAFETY: the caller's contract; a field read through the raw pointer
156 // creates no reference to the executor.
157 let lock = unsafe { core::ptr::read(core::ptr::addr_of!((*exec).exec_lock)) };
158 Self::take(lock)
159 }
160
161 /// Whether this guard holds a lock.
162 pub fn holds(&self) -> bool {
163 self.lock.is_some()
164 }
165}
166
167impl Drop for ExecGuard {
168 fn drop(&mut self) {
169 if let Some(l) = &self.lock {
170 l.give();
171 }
172 }
173}
174
175/// phase-509 P9b item 2 -- a callback window: the lock is given for this
176/// value's lifetime and taken back when it drops, also on unwind, so the
177/// guard the pass's caller holds always gives a lock it owns.
178#[cfg(any(has_rmw, test))]
179#[must_use = "the lock is taken back when the window drops"]
180pub(crate) struct Window(ExecLock);
181
182#[cfg(any(has_rmw, test))]
183impl Window {
184 /// Give `lock`, which the calling thread holds exactly once.
185 pub(crate) fn open(lock: ExecLock) -> Self {
186 lock.give();
187 Self(lock)
188 }
189}
190
191#[cfg(any(has_rmw, test))]
192impl Drop for Window {
193 fn drop(&mut self) {
194 self.0.take();
195 }
196}
197
198/// phase-509 P9b item 2 -- which thread runs the callback of `slot` in an
199/// open window. Lives in the pass's frame for the window's length and is
200/// linked into the executor's list under the lock (`Executor::running`).
201#[cfg(any(has_rmw, test))]
202pub(crate) struct Running {
203 pub(crate) slot: usize,
204 pub(crate) thread: usize,
205 pub(crate) next: *mut Running,
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211 use core::sync::atomic::{AtomicI32, Ordering};
212
213 /// A counting "mutex": the depth it is held at, and how often it was taken.
214 #[derive(Default)]
215 struct Counter {
216 depth: AtomicI32,
217 takes: AtomicI32,
218 }
219
220 unsafe extern "C" fn lock(c: *mut c_void) -> i8 {
221 // SAFETY: the test passes a `Counter` it owns as the context.
222 let c = unsafe { &*(c as *const Counter) };
223 c.depth.fetch_add(1, Ordering::SeqCst);
224 c.takes.fetch_add(1, Ordering::SeqCst);
225 0
226 }
227
228 unsafe extern "C" fn unlock(c: *mut c_void) -> i8 {
229 // SAFETY: as `lock`.
230 let c = unsafe { &*(c as *const Counter) };
231 c.depth.fetch_sub(1, Ordering::SeqCst);
232 0
233 }
234
235 /// A guard takes once and gives once, nests (the recursive case a
236 /// callback re-entering the executor is), and with no lock does nothing.
237 #[test]
238 fn a_guard_takes_once_and_gives_on_drop_and_nests() {
239 let c = Counter::default();
240 // SAFETY: `c` outlives every guard taken below.
241 let l = unsafe { ExecLock::new(&c as *const Counter as *mut c_void, lock, unlock) };
242 {
243 let g = ExecGuard::take(l);
244 assert!(g.holds());
245 assert_eq!(c.depth.load(Ordering::SeqCst), 1);
246 {
247 let _inner = ExecGuard::take(l);
248 assert_eq!(c.depth.load(Ordering::SeqCst), 2);
249 }
250 assert_eq!(c.depth.load(Ordering::SeqCst), 1);
251 }
252 assert_eq!(c.depth.load(Ordering::SeqCst), 0);
253 assert_eq!(c.takes.load(Ordering::SeqCst), 2);
254 let none = ExecGuard::take(None);
255 assert!(!none.holds());
256 // SAFETY: a NULL context is refused before anything is called.
257 assert!(unsafe { ExecLock::new(core::ptr::null_mut(), lock, unlock) }.is_none());
258 }
259
260 /// A spin lock standing in for the port mutex.
261 struct Spin(core::sync::atomic::AtomicBool);
262
263 unsafe extern "C" fn spin_lock(c: *mut c_void) -> i8 {
264 // SAFETY: the test passes the static `Spin` as the context.
265 let s = unsafe { &*(c as *const Spin) };
266 while s
267 .0
268 .compare_exchange_weak(false, true, Ordering::Acquire, Ordering::Relaxed)
269 .is_err()
270 {
271 core::hint::spin_loop();
272 }
273 0
274 }
275
276 unsafe extern "C" fn spin_unlock(c: *mut c_void) -> i8 {
277 // SAFETY: as `spin_lock`.
278 let s = unsafe { &*(c as *const Spin) };
279 s.0.store(false, Ordering::Release);
280 0
281 }
282
283 /// Two threads that touch shared state only under the guard never
284 /// interleave: every unsynchronised increment survives.
285 #[test]
286 fn guarded_sections_on_two_threads_do_not_interleave() {
287 struct Shared(core::cell::UnsafeCell<u64>);
288 // SAFETY: only touched under the guard below.
289 unsafe impl Sync for Shared {}
290 static LOCK: Spin = Spin(core::sync::atomic::AtomicBool::new(false));
291 static SHARED: Shared = Shared(core::cell::UnsafeCell::new(0));
292 let l =
293 // SAFETY: `LOCK` is a static, so it outlives both threads.
294 unsafe { ExecLock::new(&LOCK as *const Spin as *mut c_void, spin_lock, spin_unlock) };
295 const N: u64 = 100_000;
296 let worker = move || {
297 for _ in 0..N {
298 let _g = ExecGuard::take(l);
299 // SAFETY: the guard serialises every access to SHARED.
300 unsafe {
301 let p = SHARED.0.get();
302 let v = core::ptr::read_volatile(p);
303 core::ptr::write_volatile(p, v + 1);
304 }
305 }
306 };
307 let a = std::thread::spawn(worker);
308 let b = std::thread::spawn(worker);
309 a.join().unwrap();
310 b.join().unwrap();
311 let _g = ExecGuard::take(l);
312 // SAFETY: both threads have joined and the guard is held.
313 assert_eq!(unsafe { *SHARED.0.get() }, 2 * N);
314 }
315}