Skip to main content

Module cross_process_waker

Module cross_process_waker 

Source
Expand description

CrossProcessWaker: a futex-shaped wait/wake primitive sitting in shared memory (MMF or named-shm), portable across Linux / Windows / macOS / FreeBSD.

§The problem

SubEtha’s bounded rings deliver bytes between threads / processes without any kernel involvement on the hot path. That’s a win when the consumer can keep up - try_recv either returns an item or returns Empty and the caller decides what to do. The pattern breaks down when the consumer wants to BLOCK on an empty ring without spinning: there’s no kernel-side handle to wait on, and a busy-wait burns one CPU per blocked consumer.

CrossProcessWaker closes the gap. It’s the userspace futex, ported to the substrate. The producer publishes a monotonic sequence atom on every push; a blocked consumer parks on a wake list in shared memory, registering the sequence it wants to be woken at. When the producer’s sequence advances past that target, the producer’s post-publish path fires a single syscall-level wake and the consumer’s wait returns.

§Cross-platform wake

The primitive calls the platform’s wait / wake syscalls directly (NOT via the atomic-wait crate, which hard-codes FUTEX_PRIVATE_FLAG on Linux and so cannot work across processes):

  • Linux / Android: futex(FUTEX_WAIT) / futex(FUTEX_WAKE) without the PRIVATE flag - the kernel hashes by the page’s physical address so any process that mapped the same MMF page joins the same wait queue.
  • FreeBSD: _umtx_op(UMTX_OP_WAIT_UINT) / _umtx_op(UMTX_OP_WAKE) - the non-PRIVATE umtx ops, whose sleep queues the kernel keys by PHYSICAL address exactly so process-shared synchronization works (per _umtx_op(2)). Same cross-process semantics as the Linux arm.
  • Windows: WaitOnAddress / WakeByAddressSingle for process-private (anon-backed) wakers - those calls are INTRA-PROCESS only per Microsoft’s docs. Cross-process (file / named-shm backed) wakers wait on the hardware MONITOR tier instead (crate::monitor_wait): monitors are physical-address based, so a store from another process to the shared MMF line wakes the waiter - the platform’s only non-polling cross-process wake. On Windows hosts without MONITORX/WAITPKG, cross-process waits fall back to the wait-timeout + re-check recovery the blocking wrappers already run.
  • macOS / other: polling fallback (correct, but wastes CPU when idle).

All shipping syscalls operate on a user-space address (no kernel handle bookkeeping per primitive), so the cross-process Linux case needs only that the waker’s atomic lives in a mapping both processes have (named-shm or file-backed mmap). The kernel sees the same physical page from both sides and the wake reaches the parker.

§Storage layout

+--------------------------------------+ offset 0
| WakerHeader (64 bytes, one cache line)|
|   magic: u64                          |
|   capacity: u32                       |
|   _pad                                |
+--------------------------------------+ offset 64
| WakerSlot[0] (64 bytes)              |
|   state: AtomicU32 (FREE/PARKED/WOKEN)|
|   _pad                                |
|   target_seq: AtomicU64               |
|   _pad                                |
+--------------------------------------+
| WakerSlot[1] ... WakerSlot[N-1]      |
+--------------------------------------+

Each slot is one cache line so producer’s wake-scan and parker’s state writes don’t false-share across slots.

§Wake protocol

§Consumer (parker) side

  1. Scan slots for one with state == FREE.
  2. CAS that slot’s state from FREE to a transient RESERVED state.
  3. Write target_seq (the sequence we want to be woken at).
  4. Store state from RESERVED to PARKED with Release ordering - this publishes the slot to producers and is the happens-before edge for target_seq.
  5. Call the platform’s wait syscall on &slot.state with expected = PARKED. The kernel verifies state == PARKED before sleeping (Linux’s futex_wait semantics; Windows’ WaitOnAddress likewise); if a producer’s wake-CAS already landed (state == WOKEN), wait returns immediately without entering the kernel sleep path.
  6. On return, store state back to FREE and release the slot.

§Producer (waker) side

On every successful publish, call wake_up_to(producer_seq). That scans slots:

  1. Acquire-load state. If not PARKED, skip.
  2. Relaxed-load target_seq. The Acquire on state acquired the parker’s Release-store, so prior writes (incl. target_seq) are visible.
  3. If producer_seq >= target_seq, CAS state from PARKED to WOKEN. On CAS success, call the platform’s wake-one syscall on &slot.state and increment the wake counter.

The CAS guards against a double-wake when multiple producers race to wake the same slot.

§Wake-before-park race

Between a blocked-recv’s “try_recv returned Empty” check and its try_park call, a producer can publish AND call wake_up_to that finds zero parked slots. The standard recovery is the double-check in the blocking-recv wrapper: after parking, re-call try_recv before calling wait. If try_recv succeeds, release the token and return. Only if it still returns Empty does the consumer call wait.

§Linux-futex-raw escape hatch

The Cargo feature linux-futex-raw exposes the direct libc::syscall(SYS_futex, ...) surface to callers that need FUTEX_WAIT_BITSET, FUTEX_REQUEUE, or other ops the portable atomic-wait abstraction does not expose. Linux-only.

Structs§

CrossProcessWaker
Cross-process wake list. See module docs for the protocol.
WakerToken
Returned by try_park; identifies which slot the parker reserved. Pass back into wait and release.

Enums§

WakerError
Errors returned by waker operations.

Constants§

MAX_WAITERS_DEFAULT
Default slot capacity. Caller-overridable on construction.
WAKER_MAGIC
Magic bytes identifying a CrossProcessWaker region. Used by open to reject the wrong kind of MMF.

Functions§

waker_region_size
Bytes required for a waker region holding capacity slots.