Skip to main content

ax_task/sync/irq/
cell.rs

1//! Single-waiter hard-IRQ notification cell.
2//!
3//! Multi-waiter events should target a fixed service thread through this cell;
4//! that thread performs any wait-queue fan-out in ordinary task context.
5//!
6//! The registration lifecycle follows Linux v7.1 `irq_work` ownership: the
7//! `Notifying` phase is the executor's `IRQ_WORK_BUSY` claim. It covers the
8//! direct wake, the cell-sentinel cleanup, and every other access to the
9//! registration and its wake payload, and the release publication back to
10//! `Detached` is the notifier's final action. A waiter that observes
11//! `Detached` for its generation owns the registration and its wake payload
12//! again, exactly like `irq_work_sync()` observing a cleared BUSY bit.
13
14use alloc::sync::Arc;
15use core::{
16    hint::spin_loop,
17    ptr,
18    sync::atomic::{AtomicPtr, AtomicU64, Ordering},
19};
20
21use crate::thread::ThreadWakeHandle;
22
23const REGISTRATION_PHASE_BITS: u32 = 2;
24const REGISTRATION_PHASE_MASK: u64 = (1 << REGISTRATION_PHASE_BITS) - 1;
25const REGISTRATION_GENERATION_MAX: u64 = u64::MAX >> REGISTRATION_PHASE_BITS;
26const IRQ_NOTIFY_CAS_BUDGET: usize = 8;
27
28#[derive(Clone, Copy, Debug, Eq, PartialEq)]
29#[repr(u64)]
30enum RegistrationPhase {
31    Detached  = 0,
32    Attached  = 1,
33    Notifying = 2,
34}
35
36const fn registration_state(generation: u64, phase: RegistrationPhase) -> u64 {
37    (generation << REGISTRATION_PHASE_BITS) | phase as u64
38}
39
40const fn registration_generation(state: u64) -> u64 {
41    state >> REGISTRATION_PHASE_BITS
42}
43
44#[repr(align(8))]
45struct WaiterSentinel {
46    _tag: u8,
47}
48
49static PENDING_WAITER_SENTINEL: WaiterSentinel = WaiterSentinel { _tag: 1 };
50static NOTIFYING_WAITER_SENTINEL: WaiterSentinel = WaiterSentinel { _tag: 2 };
51static NOTIFYING_PENDING_WAITER_SENTINEL: WaiterSentinel = WaiterSentinel { _tag: 3 };
52
53fn waiter_sentinel(sentinel: &'static WaiterSentinel) -> *mut IrqWaitNode {
54    ptr::from_ref(sentinel).cast_mut().cast()
55}
56
57fn pending_waiter() -> *mut IrqWaitNode {
58    waiter_sentinel(&PENDING_WAITER_SENTINEL)
59}
60
61fn notifying_waiter() -> *mut IrqWaitNode {
62    waiter_sentinel(&NOTIFYING_WAITER_SENTINEL)
63}
64
65fn notifying_pending_waiter() -> *mut IrqWaitNode {
66    waiter_sentinel(&NOTIFYING_PENDING_WAITER_SENTINEL)
67}
68
69fn is_notification_sentinel(waiter: *mut IrqWaitNode) -> bool {
70    waiter == notifying_waiter() || waiter == notifying_pending_waiter()
71}
72
73fn registration_phase(state: u64) -> RegistrationPhase {
74    match state & REGISTRATION_PHASE_MASK {
75        0 => RegistrationPhase::Detached,
76        1 => RegistrationPhase::Attached,
77        2 => RegistrationPhase::Notifying,
78        _ => unreachable!("registration phase exceeds its bit mask"),
79    }
80}
81
82#[derive(Debug)]
83enum IrqWaitWake {
84    Thread(ThreadWakeHandle),
85}
86
87impl IrqWaitWake {
88    fn wake(&self) -> crate::thread::WakeResult {
89        match self {
90            Self::Thread(wake) => wake.wake(),
91        }
92    }
93}
94
95/// Pinned storage published to one [`IrqWaitCell`].
96#[derive(Debug)]
97struct IrqWaitNode {
98    wake: IrqWaitWake,
99    state: AtomicU64,
100}
101
102impl IrqWaitNode {
103    fn new(wake: IrqWaitWake) -> Self {
104        Self {
105            wake,
106            state: AtomicU64::new(registration_state(0, RegistrationPhase::Detached)),
107        }
108    }
109
110    fn reserve(&self) -> Option<u64> {
111        let mut state = self.state.load(Ordering::Acquire);
112        loop {
113            if registration_phase(state) != RegistrationPhase::Detached {
114                return None;
115            }
116            let generation = registration_generation(state)
117                .checked_add(1)
118                .filter(|generation| *generation <= REGISTRATION_GENERATION_MAX)
119                .expect("IRQ wait registration generation exhausted");
120            let attached = registration_state(generation, RegistrationPhase::Attached);
121            match self.state.compare_exchange_weak(
122                state,
123                attached,
124                Ordering::AcqRel,
125                Ordering::Acquire,
126            ) {
127                Ok(_) => return Some(generation),
128                Err(observed) => state = observed,
129            }
130        }
131    }
132
133    fn cancel(&self, generation: u64) {
134        self.state
135            .compare_exchange(
136                registration_state(generation, RegistrationPhase::Attached),
137                registration_state(generation, RegistrationPhase::Detached),
138                Ordering::Release,
139                Ordering::Acquire,
140            )
141            .expect("only an attached IRQ wait registration can be cancelled");
142    }
143
144    fn begin_notification(&self) -> u64 {
145        let state = self.state.load(Ordering::Acquire);
146        let generation = registration_generation(state);
147        assert_eq!(
148            registration_phase(state),
149            RegistrationPhase::Attached,
150            "an IRQ wait cell took a registration it no longer owned"
151        );
152        self.state
153            .compare_exchange(
154                state,
155                registration_state(generation, RegistrationPhase::Notifying),
156                Ordering::AcqRel,
157                Ordering::Acquire,
158            )
159            .expect("IRQ wait registration ownership changed after cell removal");
160        generation
161    }
162
163    /// Releases the notification claim as the notifier's final node access.
164    ///
165    /// Linux `irq_work_single()` clears `IRQ_WORK_BUSY` only after the whole
166    /// callback has returned, and `irq_work_sync()` treats that clear as the
167    /// executor's last access to the work item. This transition carries the
168    /// same contract: a waiter that observes `Detached` for its generation
169    /// may reuse the registration and its wake payload because no notifier
170    /// touches them afterwards.
171    fn finish_notification(&self, generation: u64) {
172        self.state
173            .compare_exchange(
174                registration_state(generation, RegistrationPhase::Notifying),
175                registration_state(generation, RegistrationPhase::Detached),
176                Ordering::Release,
177                Ordering::Acquire,
178            )
179            .expect("IRQ wait notification generation changed while in flight");
180    }
181
182    fn is_attached(&self, generation: u64) -> bool {
183        self.state.load(Ordering::Acquire)
184            == registration_state(generation, RegistrationPhase::Attached)
185    }
186
187    fn is_quiescent(&self, generation: u64) -> bool {
188        let state = self.state.load(Ordering::Acquire);
189        registration_generation(state) != generation
190            || registration_phase(state) == RegistrationPhase::Detached
191    }
192}
193
194fn publish_cell_owner(node: Arc<IrqWaitNode>) -> *mut IrqWaitNode {
195    Arc::into_raw(node).cast_mut()
196}
197
198unsafe fn take_cell_owner(node: *mut IrqWaitNode) -> Arc<IrqWaitNode> {
199    unsafe {
200        // SAFETY: callers invoke this exactly once after atomically removing a
201        // real node pointer previously produced by publish_cell_owner().
202        Arc::from_raw(node)
203    }
204}
205
206/// Task-context owner of a reusable one-shot registration.
207///
208/// The node is reference-owned. Publishing it transfers one owning reference
209/// to the cell, while tokens and drains retain their own references. Dropping
210/// this task-context handle therefore never relies on a destructor leak and
211/// cannot invalidate an in-flight hard-IRQ reader.
212#[derive(Debug)]
213pub struct IrqWaitRegistration {
214    node: Arc<IrqWaitNode>,
215}
216
217impl IrqWaitRegistration {
218    /// Creates a detached registration reusable across one-shot waits.
219    pub fn new(wake: ThreadWakeHandle) -> Self {
220        Self {
221            node: Arc::new(IrqWaitNode::new(IrqWaitWake::Thread(wake))),
222        }
223    }
224}
225
226/// Published identity for one IRQ waiter registration generation.
227///
228/// A token remains attached while its cell owns the waiter. Once
229/// [`is_attached`](Self::is_attached) becomes false, the task may avoid or
230/// abort its park. It must then be consumed by [`detach`](Self::detach) before
231/// the backing registration or wake payload is reused.
232#[must_use = "an IRQ wait token must enter its drain lifetime before storage is reused"]
233pub struct IrqWaitToken<'cell> {
234    registration: Arc<IrqWaitNode>,
235    generation: u64,
236    cell: &'cell IrqWaitCell,
237}
238
239impl IrqWaitToken<'_> {
240    /// Returns this one-shot registration generation.
241    pub const fn generation(&self) -> u64 {
242        self.generation
243    }
244
245    /// Returns whether the cell still owns this generation.
246    ///
247    /// Once this becomes false, a waiter may safely avoid sleeping. It does not
248    /// imply that an IRQ notifier has finished reading the wake payload; only
249    /// the drain returned by [`Self::detach`] publishes that guarantee.
250    pub fn is_attached(&self) -> bool {
251        self.registration.is_attached(self.generation)
252    }
253
254    /// Stops publication of this generation and enters its drain lifetime.
255    ///
256    /// This operation never waits. If a notifier already removed the waiter,
257    /// the returned drain observes that notifier until it publishes its claim
258    /// release; [`IrqWaitDrain::finish`] performs that bounded wait. Hard-IRQ
259    /// teardown must defer the drain to a task-context worker.
260    pub fn detach(self) -> IrqWaitDrain {
261        let cell = self.cell;
262        cell.detach(self)
263    }
264
265    fn belongs_to(&self, cell: &IrqWaitCell) -> bool {
266        ptr::eq(self.cell, cell)
267    }
268}
269
270impl core::fmt::Debug for IrqWaitToken<'_> {
271    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
272        formatter
273            .debug_struct("IrqWaitToken")
274            .field("generation", &self.generation)
275            .field("attached", &self.is_attached())
276            .finish()
277    }
278}
279
280/// Revoked IRQ registration waiting out an in-flight notification claim.
281///
282/// A drain never writes registration state. The notifier's `Notifying` claim
283/// covers every access to the node and its wake payload, and publishes
284/// `Detached` as its final action, so this type only needs to observe that
285/// publication before the registration may be reused.
286#[must_use = "an IRQ wait drain must finish before registration storage is reused"]
287pub struct IrqWaitDrain {
288    registration: Arc<IrqWaitNode>,
289    generation: u64,
290}
291
292impl IrqWaitDrain {
293    /// Reports whether the in-flight notifier has published its release.
294    pub fn is_quiescent(&self) -> bool {
295        self.registration.is_quiescent(self.generation)
296    }
297
298    /// Waits until the in-flight notifier has published its release.
299    ///
300    /// Every notifier section runs in hard IRQ or with preemption disabled,
301    /// so the claim always ends in bounded time. This is Linux's hard-path
302    /// `irq_work_sync()` shape (`while (irq_work_is_busy(work)) cpu_relax();`):
303    /// a park would require exactly the post-release completion wake that the
304    /// `Notifying`-covers-everything ownership rule removes.
305    pub fn finish(self) {
306        while !self.is_quiescent() {
307            spin_loop();
308        }
309    }
310}
311
312impl core::fmt::Debug for IrqWaitDrain {
313    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
314        formatter
315            .debug_struct("IrqWaitDrain")
316            .field("generation", &self.generation)
317            .field("quiescent", &self.is_quiescent())
318            .finish()
319    }
320}
321
322/// Outcome of task-context waiter registration.
323#[derive(Debug)]
324pub enum IrqRegisterResult<'cell> {
325    /// The cell owns the sole waiter until notify or unregister.
326    Registered(IrqWaitToken<'cell>),
327    /// An earlier or concurrent interrupt consumed the registration.
328    ///
329    /// An earlier pending event is returned synchronously without waking the
330    /// currently running task. The registration is detached on return.
331    ConsumedPending,
332    /// A concurrent notifier owns the registration and will release and wake it.
333    ///
334    /// The task may abort its park once the token is detached, but it must
335    /// quiesce the token before reusing the registration or wake payload.
336    NotificationInFlight(IrqWaitToken<'cell>),
337    /// Another waiter is registered, or this registration is still in use.
338    Occupied,
339}
340
341/// Outcome of one bounded hard-IRQ notification.
342#[derive(Clone, Copy, Debug, Eq, PartialEq)]
343pub enum IrqNotifyResult {
344    /// One stable direct waiter was removed and woken.
345    ///
346    /// A scheduler wake already retained by the target park transition also
347    /// counts as delivered.
348    Notified,
349    /// No waiter was present, or direct delivery failed; one coalesced pending
350    /// bit was published.
351    Pending,
352}
353
354/// Pending-bit plus single-waiter hard-IRQ event cell.
355#[derive(Debug)]
356pub struct IrqWaitCell {
357    waiter: AtomicPtr<IrqWaitNode>,
358}
359
360impl IrqWaitCell {
361    /// Creates an empty notification cell.
362    pub const fn new() -> Self {
363        Self {
364            waiter: AtomicPtr::new(ptr::null_mut()),
365        }
366    }
367
368    /// Registers one stable waiter, consuming an earlier IRQ when present.
369    pub fn register<'cell>(
370        &'cell self,
371        registration: &IrqWaitRegistration,
372    ) -> IrqRegisterResult<'cell> {
373        let registration = Arc::clone(&registration.node);
374        let Some(generation) = registration.reserve() else {
375            return IrqRegisterResult::Occupied;
376        };
377        let registration_ptr = publish_cell_owner(Arc::clone(&registration));
378        let token = IrqWaitToken {
379            registration,
380            generation,
381            cell: self,
382        };
383        let pending = pending_waiter();
384        let mut observed = self.waiter.load(Ordering::Acquire);
385        loop {
386            if observed == pending {
387                match self.waiter.compare_exchange(
388                    pending,
389                    ptr::null_mut(),
390                    Ordering::AcqRel,
391                    Ordering::Acquire,
392                ) {
393                    Ok(_) => {
394                        token.registration.cancel(generation);
395                        // SAFETY: this raw reference was never published, so
396                        // the registering task still exclusively owns it.
397                        unsafe { drop(take_cell_owner(registration_ptr)) };
398                        return IrqRegisterResult::ConsumedPending;
399                    }
400                    Err(current) => {
401                        observed = current;
402                        continue;
403                    }
404                }
405            }
406            if !observed.is_null() {
407                token.registration.cancel(generation);
408                // SAFETY: this raw reference was never published, so the
409                // registering task still exclusively owns it.
410                unsafe { drop(take_cell_owner(registration_ptr)) };
411                return IrqRegisterResult::Occupied;
412            }
413            match self.waiter.compare_exchange(
414                ptr::null_mut(),
415                registration_ptr,
416                Ordering::Release,
417                Ordering::Acquire,
418            ) {
419                Ok(_) => break,
420                Err(current) => observed = current,
421            }
422        }
423
424        if self.waiter.load(Ordering::Acquire) == registration_ptr {
425            IrqRegisterResult::Registered(token)
426        } else {
427            // A concurrent notifier already owns and will wake the registration.
428            IrqRegisterResult::NotificationInFlight(token)
429        }
430    }
431
432    fn detach(&self, token: IrqWaitToken<'_>) -> IrqWaitDrain {
433        assert!(
434            token.belongs_to(self),
435            "an IRQ wait token must be detached by its publishing cell"
436        );
437        let registration = token.registration;
438        let state = registration.state.load(Ordering::Acquire);
439        if registration_generation(state) == token.generation {
440            let registration_ptr = Arc::as_ptr(&registration).cast_mut();
441            if self
442                .waiter
443                .compare_exchange(
444                    registration_ptr,
445                    ptr::null_mut(),
446                    Ordering::AcqRel,
447                    Ordering::Acquire,
448                )
449                .is_ok()
450            {
451                registration.cancel(token.generation);
452                // SAFETY: the successful CAS transferred the cell-owned raw
453                // reference to this task-context detach operation.
454                unsafe { drop(take_cell_owner(registration_ptr)) };
455            }
456        }
457        IrqWaitDrain {
458            registration,
459            generation: token.generation,
460        }
461    }
462
463    /// Wakes the sole registered thread or publishes one coalesced pending bit.
464    ///
465    /// This operation performs a bounded number of atomics and at most one
466    /// trusted direct wake. After repeated contention, it atomically installs
467    /// the sticky pending state and may retain one harmless extra service pass
468    /// after waking the displaced waiter. It never scans a wait queue or
469    /// allocates.
470    ///
471    /// The whole claim-to-release section runs non-preemptible, mirroring
472    /// Linux's irq_work execution boundary: hard IRQ context is inherently
473    /// non-preemptible, and ordinary task context enters a preemption scope,
474    /// matching how `irq_workd` disables migration around the claim. This
475    /// bounds how long a draining waiter can observe a `Notifying` claim.
476    pub fn notify(&self) -> IrqNotifyResult {
477        // SAFETY-free context probe: the runtime hook only reports the current
478        // interrupt state; the preemption scope below never nests into it.
479        let _preempt = (!crate::runtime::task_runtime::in_hard_irq())
480            .then(crate::runtime::lock::PreemptScope::enter);
481        self.notify_claimed()
482    }
483
484    fn notify_claimed(&self) -> IrqNotifyResult {
485        let pending = pending_waiter();
486        let notifying = notifying_waiter();
487        let notifying_pending = notifying_pending_waiter();
488        let mut observed = self.waiter.load(Ordering::Acquire);
489        for _ in 0..IRQ_NOTIFY_CAS_BUDGET {
490            if observed == pending {
491                match self.waiter.compare_exchange(
492                    pending,
493                    pending,
494                    Ordering::AcqRel,
495                    Ordering::Acquire,
496                ) {
497                    Ok(_) => return IrqNotifyResult::Pending,
498                    Err(current) => {
499                        observed = current;
500                        continue;
501                    }
502                }
503            }
504            if observed == notifying_pending {
505                return IrqNotifyResult::Pending;
506            }
507            if observed == notifying {
508                match self.waiter.compare_exchange(
509                    notifying,
510                    notifying_pending,
511                    Ordering::AcqRel,
512                    Ordering::Acquire,
513                ) {
514                    Ok(_) => return IrqNotifyResult::Pending,
515                    Err(current) => {
516                        observed = current;
517                        continue;
518                    }
519                }
520            }
521            if observed.is_null() {
522                match self.waiter.compare_exchange(
523                    ptr::null_mut(),
524                    pending,
525                    Ordering::Release,
526                    Ordering::Acquire,
527                ) {
528                    Ok(_) => return IrqNotifyResult::Pending,
529                    Err(current) => {
530                        observed = current;
531                        continue;
532                    }
533                }
534            }
535            match self.waiter.compare_exchange(
536                observed,
537                notifying,
538                Ordering::AcqRel,
539                Ordering::Acquire,
540            ) {
541                Ok(waiter) => {
542                    // SAFETY: the successful CAS transferred the cell-owned
543                    // raw reference to this notifier.
544                    let registration = unsafe { take_cell_owner(waiter) };
545                    let (generation, result) = Self::wake_registration(&registration);
546                    self.finish_notification(result);
547                    registration.finish_notification(generation);
548                    return Self::notification_result(result);
549                }
550                Err(current) => observed = current,
551            }
552        }
553
554        // Hard IRQ work remains wait-free under pathological cross-CPU churn.
555        // Keeping the sentinel after displacing a waiter may cause one
556        // task-context recheck, but it cannot lose the notification.
557        let waiter = self.waiter.swap(pending, Ordering::AcqRel);
558        if waiter.is_null() || waiter == pending || is_notification_sentinel(waiter) {
559            return IrqNotifyResult::Pending;
560        }
561        // SAFETY: swap transferred the displaced cell-owned raw reference to
562        // this notifier; null and the sentinel were rejected above.
563        let registration = unsafe { take_cell_owner(waiter) };
564        let (generation, result) = Self::wake_registration(&registration);
565        registration.finish_notification(generation);
566        Self::notification_result(result)
567    }
568
569    /// Reports whether an IRQ is coalesced for the next registration.
570    pub fn is_pending(&self) -> bool {
571        matches!(
572            self.waiter.load(Ordering::Acquire),
573            waiter if waiter == pending_waiter() || waiter == notifying_pending_waiter()
574        )
575    }
576
577    fn wake_registration(registration: &IrqWaitNode) -> (u64, crate::thread::WakeResult) {
578        let generation = registration.begin_notification();
579        let result = registration.wake.wake();
580        (generation, result)
581    }
582
583    fn finish_notification(&self, result: crate::thread::WakeResult) {
584        let pending = pending_waiter();
585        let notifying = notifying_waiter();
586        let notifying_pending = notifying_pending_waiter();
587        let delivered = matches!(
588            result,
589            crate::thread::WakeResult::Notified | crate::thread::WakeResult::AlreadyPending
590        );
591        let mut observed = self.waiter.load(Ordering::Acquire);
592        loop {
593            let next = if observed == notifying {
594                if delivered { ptr::null_mut() } else { pending }
595            } else if observed == notifying_pending {
596                pending
597            } else if observed == pending {
598                return;
599            } else {
600                panic!("IRQ wait cell notification ownership changed while wake was in flight");
601            };
602            match self.waiter.compare_exchange_weak(
603                observed,
604                next,
605                Ordering::AcqRel,
606                Ordering::Acquire,
607            ) {
608                Ok(_) => return,
609                Err(current) => observed = current,
610            }
611        }
612    }
613
614    const fn notification_result(result: crate::thread::WakeResult) -> IrqNotifyResult {
615        match result {
616            crate::thread::WakeResult::Notified | crate::thread::WakeResult::AlreadyPending => {
617                IrqNotifyResult::Notified
618            }
619            crate::thread::WakeResult::Exited | crate::thread::WakeResult::Unavailable => {
620                IrqNotifyResult::Pending
621            }
622        }
623    }
624}
625
626impl Drop for IrqWaitCell {
627    fn drop(&mut self) {
628        let waiter = core::mem::replace(self.waiter.get_mut(), ptr::null_mut());
629        if waiter.is_null() || waiter == pending_waiter() {
630            return;
631        }
632        assert!(
633            !is_notification_sentinel(waiter),
634            "exclusive IRQ wait cell teardown found an in-flight notifier",
635        );
636
637        // SAFETY: exclusive cell teardown removes the sole cell-owned raw
638        // reference, and safe Rust prevents a concurrent notifier borrow.
639        let registration = unsafe { take_cell_owner(waiter) };
640        let state = registration.state.load(Ordering::Acquire);
641        assert_eq!(
642            registration_phase(state),
643            RegistrationPhase::Attached,
644            "exclusive IRQ wait cell teardown found an in-flight notifier",
645        );
646        registration.cancel(registration_generation(state));
647    }
648}
649
650impl Default for IrqWaitCell {
651    fn default() -> Self {
652        Self::new()
653    }
654}
655
656#[cfg(all(test, not(miri)))]
657mod loom_tests;