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/WakeByAddressSinglefor 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
- Scan slots for one with
state == FREE. - CAS that slot’s state from FREE to a transient RESERVED state.
- Write
target_seq(the sequence we want to be woken at). - Store state from RESERVED to PARKED with Release ordering -
this publishes the slot to producers and is the
happens-before edge for
target_seq. - Call the platform’s wait syscall on
&slot.statewith expected = PARKED. The kernel verifiesstate == PARKEDbefore 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. - 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:
- Acquire-load
state. If not PARKED, skip. - Relaxed-load
target_seq. The Acquire onstateacquired the parker’s Release-store, so prior writes (incl. target_seq) are visible. - If
producer_seq >= target_seq, CAS state from PARKED to WOKEN. On CAS success, call the platform’s wake-one syscall on&slot.stateand 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§
- Cross
Process Waker - Cross-process wake list. See module docs for the protocol.
- Waker
Token - Returned by
try_park; identifies which slot the parker reserved. Pass back intowaitandrelease.
Enums§
- Waker
Error - 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
opento reject the wrong kind of MMF.
Functions§
- waker_
region_ size - Bytes required for a waker region holding
capacityslots.