Skip to main content

strat9_kernel/sync/
spinlock.rs

1//! Generic spinlock with configurable guard behaviour.
2//!
3//! # Choosing a guardian
4//!
5//! ```text
6//! SpinLock<T>                  →  SpinLock<T, IrqDisabled>   (default)
7//! SpinLock<T, IrqDisabled>     →  saves RFLAGS + clears IF (equiv. spin_lock_irqsave)
8//! SpinLock<T, PreemptDisabled> →  disables preemption only, IRQs untouched
9//! ```
10//!
11//! **Use `IrqDisabled` (the default) for**:
12//! - Data shared across CPUs (heap, VFS, IPC queues, network rings, …)
13//! - Any data touched from interrupt handlers
14//!
15//! **Use `PreemptDisabled` for**:
16//! - Per-CPU data never accessed from interrupt handlers
17//!   (scheduler run-queues, per-CPU frame caches, statistics counters …)
18//!
19//! All call sites that use `SpinLock<T>` without a type argument continue to
20//! compile unchanged : `IrqDisabled` is the default guardian.
21//!
22//! # Debug helpers
23//!
24//! `debug_set_watch_lock_addr` / `debug_clear_watch_lock_addr` emit a serial
25//! trace on every `drop` of a specific lock instance : useful when hunting
26//! deadlocks.
27
28use super::{
29    guardian::{Guardian, GuardianState, IrqDisabled},
30    IrqDisabledToken,
31};
32use core::{
33    cell::UnsafeCell,
34    marker::PhantomData,
35    mem::ManuallyDrop,
36    ops::{Deref, DerefMut},
37    sync::atomic::{AtomicBool, AtomicUsize, Ordering},
38};
39
40static DEBUG_WATCH_LOCK_ADDR: AtomicUsize = AtomicUsize::new(usize::MAX);
41
42// =========================== SpinLock ========================================
43
44/// A spinlock parameterised by a [`Guardian`].
45///
46/// The default guardian is [`IrqDisabled`], preserving existing call-site
47/// semantics with no source changes required.
48///
49/// Supports `T: ?Sized` for `SpinLock<dyn Trait>` and other DST types.
50/// The `data` field is last so that the struct can hold dynamically sized types.
51pub struct SpinLock<T: ?Sized, G: Guardian = IrqDisabled> {
52    locked: AtomicBool,
53    /// CPU index of the current lock holder (`usize::MAX` when unlocked).
54    /// Used for deadlock diagnostics only.
55    owner_cpu: AtomicUsize,
56    _guardian: PhantomData<G>,
57    /// Must be the last field: when `T: ?Sized`, this is a DST and Rust
58    /// requires the dynamically sized field to be last in the struct.
59    data: UnsafeCell<T>,
60}
61
62// SAFETY: The guardian ensures mutual exclusion; T must itself be Send.
63unsafe impl<T: ?Sized + Send, G: Guardian> Sync for SpinLock<T, G> {}
64unsafe impl<T: ?Sized + Send, G: Guardian> Send for SpinLock<T, G> {}
65
66impl<T, G: Guardian> SpinLock<T, G> {
67    /// Create a new, unlocked spinlock.
68    ///
69    /// `T` must be `Sized` here because we construct a new value.
70    pub const fn new(data: T) -> Self {
71        SpinLock {
72            locked: AtomicBool::new(false),
73            owner_cpu: AtomicUsize::new(usize::MAX),
74            _guardian: PhantomData,
75            data: UnsafeCell::new(data),
76        }
77    }
78}
79
80impl<T: ?Sized, G: Guardian> SpinLock<T, G> {
81    /// Acquire the lock, spinning until available.
82    ///
83    /// The guardian's `enter()` hook runs **before** the spin loop so that the
84    /// CPU is already in the protected mode while we wait. This closes the
85    /// window where an IRQ or preempt-switch could occur between protect and
86    /// acquire.
87    pub fn lock(&self) -> SpinLockGuard<'_, T, G> {
88        let state = G::enter();
89        let mut spins: usize = 0;
90        let this_cpu = crate::arch::x86_64::percpu::current_cpu_index();
91
92        while self
93            .locked
94            .compare_exchange_weak(false, true, Ordering::Acquire, Ordering::Relaxed)
95            .is_err()
96        {
97            spins = spins.saturating_add(1);
98            if spins == 5_000_000 {
99                let owner = self.owner_cpu.load(Ordering::Relaxed);
100                crate::serial_println!(
101                    "[trace][spin] long-wait lock={:#x} cpu={} owner_cpu={}",
102                    self as *const _ as *const () as usize,
103                    this_cpu,
104                    owner,
105                );
106                spins = 0;
107            }
108            core::hint::spin_loop();
109        }
110        self.owner_cpu.store(this_cpu, Ordering::Relaxed);
111        // Race/corruption diagnostic: E9 trace when watched locks are acquired.
112        emit_trace_e9(self as *const _ as *const () as usize, 0);
113
114        SpinLockGuard {
115            lock: self,
116            state: ManuallyDrop::new(state),
117        }
118    }
119
120    /// Try to acquire the lock without spinning.
121    pub fn try_lock(&self) -> Option<SpinLockGuard<'_, T, G>> {
122        let state = G::enter();
123        if self
124            .locked
125            .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
126            .is_ok()
127        {
128            let this_cpu = crate::arch::x86_64::percpu::current_cpu_index();
129            self.owner_cpu.store(this_cpu, Ordering::Relaxed);
130            emit_trace_e9(self as *const _ as *const () as usize, 0);
131            Some(SpinLockGuard {
132                lock: self,
133                state: ManuallyDrop::new(state),
134            })
135        } else {
136            G::exit(state);
137            None
138        }
139    }
140
141    /// Returns the owner CPU index (`usize::MAX` if unlocked).
142    pub fn owner_cpu(&self) -> usize {
143        self.owner_cpu.load(Ordering::Relaxed)
144    }
145
146    /// Returns a mutable reference to the underlying data.
147    ///
148    /// By holding `&mut self`, the compiler guarantees exclusive access to the
149    /// lock; no other reference exists, so the inner data can be accessed
150    /// without acquiring the lock.
151    pub fn get_mut(&mut self) -> &mut T {
152        self.data.get_mut()
153    }
154}
155
156// =========================== IrqDisabled-specific extensions
157
158impl<T: ?Sized> SpinLock<T, IrqDisabled> {
159    /// Try to acquire without touching RFLAGS.
160    ///
161    /// Returns `None` if IRQs are currently enabled (no `IrqDisabledToken` can
162    /// be produced) or the lock is already held. The caller must ensure that
163    /// IRQs remain disabled for the entire lifetime of the returned guard.
164    pub fn try_lock_no_irqsave(&self) -> Option<SpinLockGuard<'_, T, IrqDisabled>> {
165        let token = match IrqDisabledToken::verify() {
166            Some(token) => token,
167            None => {
168                // Diagnostic only: distinguish "IRQs enabled" from ordinary
169                // lock contention at call sites that intentionally treat both
170                // as a best-effort `None` return in hot paths.
171                unsafe { core::arch::asm!("mov al, 'V'; out 0xe9, al", out("al") _) };
172                return None;
173            }
174        };
175        if self
176            .locked
177            .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
178            .is_ok()
179        {
180            let this_cpu = crate::arch::x86_64::percpu::current_cpu_index();
181            self.owner_cpu.store(this_cpu, Ordering::Relaxed);
182            emit_trace_e9(self as *const _ as *const () as usize, 0);
183            Some(SpinLockGuard {
184                lock: self,
185                state: ManuallyDrop::new(GuardianState {
186                    token,
187                    saved_flags: 0,
188                    restore_flags: false,
189                }),
190            })
191        } else {
192            None
193        }
194    }
195}
196
197// =========================== Debug helpers ====================================
198
199/// Register a lock address to trace on every drop (serial console output).
200pub fn debug_set_watch_lock_addr(addr: usize) {
201    DEBUG_WATCH_LOCK_ADDR.store(addr, Ordering::Relaxed);
202}
203
204/// Clear the watched lock address.
205pub fn debug_clear_watch_lock_addr() {
206    DEBUG_WATCH_LOCK_ADDR.store(usize::MAX, Ordering::Relaxed);
207}
208
209/// Maximum number of locks that can be traced simultaneously via E9 port.
210const DEBUG_TRACE_WATCH_SLOTS: usize = 8;
211
212/// Fixed-size array of watched lock addresses for E9 trace.
213/// Each slot emits a unique ASCII tag ('A'..'H') on acquire/release.
214static DEBUG_TRACE_WATCH_ADDRS: [AtomicUsize; DEBUG_TRACE_WATCH_SLOTS] =
215    [const { AtomicUsize::new(usize::MAX) }; DEBUG_TRACE_WATCH_SLOTS];
216
217/// Register a lock address for E9 trace. Returns the slot index (0..7),
218/// or `None` if all slots are in use.
219pub fn debug_set_trace_lock_addr(addr: usize) -> Option<usize> {
220    for (i, slot) in DEBUG_TRACE_WATCH_ADDRS.iter().enumerate() {
221        if slot.load(Ordering::Relaxed) == usize::MAX {
222            slot.store(addr, Ordering::Relaxed);
223            return Some(i);
224        }
225    }
226    None
227}
228
229/// Clear a specific trace slot by index.
230pub fn debug_clear_trace_slot(index: usize) {
231    if index < DEBUG_TRACE_WATCH_SLOTS {
232        DEBUG_TRACE_WATCH_ADDRS[index].store(usize::MAX, Ordering::Relaxed);
233    }
234}
235
236/// Legacy alias: register the first available trace slot.
237pub fn debug_set_trace_buddy_addr(addr: usize) {
238    let _ = debug_set_trace_lock_addr(addr);
239}
240
241/// Legacy alias: register the first available trace slot.
242pub fn debug_set_trace_slab_addr(addr: usize) {
243    let _ = debug_set_trace_lock_addr(addr);
244}
245
246/// Emit an E9 byte for each matching trace slot (acquire = uppercase, release = lowercase).
247#[inline]
248fn emit_trace_e9(lock_addr: usize, tag_offset: u8) {
249    for (i, slot) in DEBUG_TRACE_WATCH_ADDRS.iter().enumerate() {
250        if slot.load(Ordering::Relaxed) == lock_addr {
251            let ch = b'A' + tag_offset + (i as u8);
252            unsafe { core::arch::asm!("out 0xe9, al", in("al") ch) };
253        }
254    }
255}
256
257// =========================== SpinLockGuard ====================================
258
259/// RAII guard that holds the lock and carries the guardian state.
260pub struct SpinLockGuard<'a, T: ?Sized, G: Guardian = IrqDisabled> {
261    lock: &'a SpinLock<T, G>,
262    /// Wrapped in ManuallyDrop so that `Drop::drop` can move it into
263    /// `G::exit()` without triggering a compiler-generated second drop of
264    /// the field.
265    state: ManuallyDrop<GuardianState<G::Token>>,
266}
267
268impl<'a, T: ?Sized> SpinLockGuard<'a, T, IrqDisabled> {
269    /// Return the typed proof that IRQs are disabled.
270    #[inline]
271    pub fn token(&self) -> &IrqDisabledToken {
272        &self.state.token
273    }
274
275    #[inline]
276    pub(crate) fn with_mut_and_token<R>(
277        &mut self,
278        f: impl FnOnce(&mut T, &IrqDisabledToken) -> R,
279    ) -> R {
280        let token = &self.state.token;
281        // SAFETY: this guard owns the lock, guaranteeing exclusive access.
282        let data = unsafe { &mut *self.lock.data.get() };
283        f(data, token)
284    }
285}
286
287impl<'a, T: ?Sized, G: Guardian> Deref for SpinLockGuard<'a, T, G> {
288    type Target = T;
289
290    fn deref(&self) -> &T {
291        // SAFETY: we hold the lock.
292        unsafe { &*self.lock.data.get() }
293    }
294}
295
296impl<'a, T: ?Sized, G: Guardian> DerefMut for SpinLockGuard<'a, T, G> {
297    fn deref_mut(&mut self) -> &mut T {
298        // SAFETY: we hold the lock.
299        unsafe { &mut *self.lock.data.get() }
300    }
301}
302
303impl<'a, T: ?Sized, G: Guardian> Drop for SpinLockGuard<'a, T, G> {
304    fn drop(&mut self) {
305        let lock_addr = self.lock as *const _ as *const () as usize;
306        let watched = DEBUG_WATCH_LOCK_ADDR.load(Ordering::Relaxed);
307        let trace = watched == lock_addr;
308
309        if trace {
310            crate::serial_force_println!(
311                "[trace][spin] drop begin lock={:#x} owner_cpu={} saved_flags={:#x}",
312                lock_addr,
313                self.lock.owner_cpu.load(Ordering::Relaxed),
314                self.state.saved_flags,
315            );
316        }
317
318        self.lock.owner_cpu.store(usize::MAX, Ordering::Relaxed);
319        self.lock.locked.store(false, Ordering::Release);
320
321        if trace {
322            crate::serial_force_println!("[trace][spin] drop unlocked lock={:#x}", lock_addr);
323        }
324        // Race/corruption diagnostic: E9 trace when watched locks are released.
325        // Use lowercase tags to distinguish release from acquire.
326        for (i, slot) in DEBUG_TRACE_WATCH_ADDRS.iter().enumerate() {
327            if slot.load(Ordering::Relaxed) == lock_addr {
328                let ch = b'a' + (i as u8);
329                unsafe { core::arch::asm!("out 0xe9, al", in("al") ch) };
330            }
331        }
332
333        // SAFETY: `state` is valid and initialised. We move it out of its
334        // ManuallyDrop wrapper so that G::exit() can consume it. The compiler
335        // will NOT run a second destructor on the field because ManuallyDrop
336        // suppresses automatic drops.
337        let state = unsafe { ManuallyDrop::take(&mut self.state) };
338        G::exit(state);
339
340        if trace {
341            crate::serial_force_println!(
342                "[trace][spin] drop guardian-exit done lock={:#x}",
343                lock_addr
344            );
345        }
346    }
347}
348
349// The guardian's invariant (e.g. preemption depth, IF flag) is per-CPU.
350// Sending the guard to another CPU would violate it.
351impl<T: ?Sized, G: Guardian> !Send for SpinLockGuard<'_, T, G> {}