Skip to main content

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}