Skip to main content

strat9_kernel/ipc/
lockfree_ring.rs

1//! Lock-free single-producer single-consumer ring buffer for zero-copy IPC.
2//!
3//! # Memory layout
4//!
5//! Each ring is backed by physically-contiguous DMA-accessible pages.
6//! The first page holds a [`RingHeader`] with cache-line-padded atomic
7//! head/tail indexes. Subsequent pages hold [`RingSlot`] entries.
8//!
9//! # Ordering invariants
10//!
11//! The producer writes data before publishing via `tail.store(Release)`.
12//! The consumer reads data after observing via `tail.load(Acquire)`.
13//! This guarantees correct ordering on x86 and ARM/POWER (no store-store
14//! reordering past the Release barrier).  See also [`LockFreeRing::write`].
15
16use core::{
17    mem::size_of,
18    sync::atomic::{AtomicU16, AtomicU32, Ordering},
19};
20
21use alloc::{sync::Arc, vec::Vec};
22
23use crate::{
24    memory::{allocate_frame, free_frame, phys_to_virt, PhysFrame},
25    sync::with_irqs_disabled,
26};
27
28// ---------------------------------------------------------------------------
29// RingHeader : cache-line padded, always in the first page
30// ---------------------------------------------------------------------------
31
32/// Shared ring header, placed at the start of the first physical page.
33///
34/// Cache-line layout (x86-64, 64-byte lines):
35/// - Line 0: magic, capacity, slot_size, flags (read-mostly, rarely written)
36/// - Line 1: head, notify_prod (written by **consumer**, read by producer)
37/// - Line 2: tail, notify_cons (written by **producer**, read by consumer)
38#[repr(C, align(64))]
39pub struct RingHeader {
40    /// Magic number for validating shared mappings.
41    magic: u32,
42    /// Total number of slots (must be power of two).
43    capacity: u32,
44    /// Usable bytes per slot.
45    slot_size: u32,
46    /// Flags field (bit 0 = initialised, bit 1 = producer_ready).
47    flags: AtomicU32,
48    _pad1: [u8; 48],
49
50    // Cache-line 1 : consumer hot
51    /// Next slot to read.  Written by consumer; read by producer.
52    head: AtomicU32,
53    /// Notification counter for the producer (written by consumer).
54    notify_prod: AtomicU32,
55    _pad2: [u8; 56],
56
57    // Cache-line 2 : producer hot
58    /// Next slot to write.  Written by producer; read by consumer.
59    tail: AtomicU32,
60    /// Notification counter for the consumer (written by producer).
61    notify_cons: AtomicU32,
62    _pad3: [u8; 56],
63}
64
65// ---------------------------------------------------------------------------
66// RingSlot
67// ---------------------------------------------------------------------------
68
69/// A single slot within the ring.
70#[repr(C)]
71pub struct RingSlot {
72    /// Length of data in this slot (0 = empty).
73    len: AtomicU16,
74    /// Flags (bit 0 = SLOT_FLAG_COMMITTED; used in MPSC variants).
75    flags: AtomicU16,
76    /// Payload bytes.
77    data: [u8; Self::SLOT_SIZE],
78}
79
80impl RingSlot {
81    /// Default slot size, large enough for an Ethernet frame (1514 B) plus
82    /// headers and padding.
83    pub const SLOT_SIZE: usize = 2048;
84}
85
86const SLOT_FLAG_COMMITTED: u16 = 0x0001;
87
88// ---------------------------------------------------------------------------
89// RingError
90// ---------------------------------------------------------------------------
91
92/// Errors returned by ring operations.
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum RingError {
95    /// Ring has no free slots.
96    Full,
97    /// Ring contains no messages.
98    Empty,
99    /// Message exceeds slot capacity.
100    MessageTooLarge,
101    /// Provided buffer is too small for the pending message.
102    BufferTooSmall,
103    /// Memory allocation failed during ring creation.
104    AllocFailed,
105    /// Invalid ring parameters (size, capacity, etc.).
106    InvalidParameters,
107}
108
109// ---------------------------------------------------------------------------
110// LockFreeRing
111// ---------------------------------------------------------------------------
112
113/// A lock-free single-producer single-consumer ring buffer.
114///
115/// # SPSC guarantee
116///
117/// `write` must only be called from the **producer** thread; `read` must
118/// only be called from the **consumer** thread.  No locks are used : only
119/// atomic loads/stores with acquire/release ordering.
120///
121/// For multi-queue NIC RSS, create one `LockFreeRing` per RSS queue rather
122/// than sharing a single MPSC ring; this avoids CAS contention and
123/// head-of-line blocking.
124#[derive(Debug)]
125pub struct LockFreeRing {
126    /// Total size in bytes of the shared memory region.
127    size: usize,
128    /// Number of physical frames backing the ring.
129    frame_count: usize,
130    /// Physical frames.
131    frames: Vec<PhysFrame>,
132    /// Virtual address of the header (first page).
133    header: *mut RingHeader,
134    /// Virtual address of the first slot (immediately after the header in
135    /// the first page, or in subsequent pages when slots spill over).
136    slots: *mut RingSlot,
137    /// Number of slots (derived from capacity in the header).
138    capacity: u32,
139    /// Slot size in bytes.
140    slot_size: u32,
141}
142
143// SAFETY: LockFreeRing is Send + Sync because all mutable access is
144// partitioned by role (producer vs consumer) and enforced by the type
145// system via TypedLockFreeRing (see transport.rs).
146unsafe impl Send for LockFreeRing {}
147unsafe impl Sync for LockFreeRing {}
148
149impl LockFreeRing {
150    /// Default slot count (power of two).
151    const DEFAULT_CAPACITY: u32 = 256;
152
153    /// Create a new ring with `slot_count` slots of `slot_size` bytes.
154    ///
155    /// The ring is allocated from physically-contiguous DMA-accessible
156    /// frames and zeroed.  Returns an error if allocation fails.
157    pub fn new(slot_count: u32, slot_size: usize) -> Result<Arc<Self>, RingError> {
158        if !slot_count.is_power_of_two() || slot_count == 0 {
159            return Err(RingError::InvalidParameters);
160        }
161        if slot_size == 0 || slot_size > RingSlot::SLOT_SIZE {
162            return Err(RingError::InvalidParameters);
163        }
164
165        let header_size = size_of::<RingHeader>();
166        let slots_size = slot_count as usize * size_of::<RingSlot>();
167        let total_size = header_size + slots_size;
168        let page_count = total_size.checked_add(4095).map(|p| p / 4096).unwrap_or(0);
169
170        if page_count == 0 {
171            return Err(RingError::InvalidParameters);
172        }
173
174        // Allocate frames
175        let mut frames = Vec::with_capacity(page_count);
176        for _ in 0..page_count {
177            let frame = with_irqs_disabled(|token| allocate_frame(token))
178                .map_err(|_| RingError::AllocFailed)?;
179            frames.push(frame);
180        }
181
182        // Map frames into virtual address space
183        let base_virt = phys_to_virt(frames[0].start_address.as_u64());
184        let header = base_virt as *mut RingHeader;
185
186        // Zero each frame individually : they may not be physically contiguous.
187        for &frame in &frames {
188            let virt = phys_to_virt(frame.start_address.as_u64());
189            unsafe {
190                core::ptr::write_bytes(virt as *mut u8, 0, 4096);
191            }
192        }
193
194        // Initialise header
195        unsafe {
196            (*header).magic = 0x5254494e; // "RING"
197            (*header).capacity = slot_count;
198            (*header).slot_size = slot_size as u32;
199            (*header).flags = AtomicU32::new(0x0001); // bit 0 = initialised
200            (*header).notify_cons = AtomicU32::new(0);
201            (*header).notify_prod = AtomicU32::new(0);
202            (*header).head = AtomicU32::new(0);
203            (*header).tail = AtomicU32::new(0);
204        }
205
206        // Calculate slot base address
207        let slots_base = base_virt + header_size as u64;
208
209        let ring = Arc::new(LockFreeRing {
210            size: total_size,
211            frame_count: page_count,
212            frames,
213            header,
214            slots: slots_base as *mut RingSlot,
215            capacity: slot_count,
216            slot_size: slot_size as u32,
217        });
218
219        Ok(ring)
220    }
221
222    /// The number of slots in this ring.
223    #[inline]
224    pub fn capacity(&self) -> u32 {
225        self.capacity
226    }
227
228    /// The size in bytes of each slot.
229    #[inline]
230    pub fn slot_size(&self) -> u32 {
231        self.slot_size
232    }
233
234    /// Physical addresses of the backing frames (for DMA and for mapping
235    /// into other address spaces via capabilities).
236    pub fn frame_phys_addrs(&self) -> Vec<u64> {
237        self.frames
238            .iter()
239            .map(|f| f.start_address.as_u64())
240            .collect()
241    }
242
243    /// Write `data` into the ring.  Returns `Full` if no slot is available,
244    /// or `MessageTooLarge` if the data exceeds the slot capacity.
245    ///
246    /// # Ordering
247    ///
248    /// 1. Non-atomic copy into the slot (`copy_nonoverlapping`).
249    /// 2. `len.store(Release)` : makes the length visible *after* data.
250    /// 3. `tail.store(Release)` : publishes the slot.
251    ///
252    /// On ARM/POWER the Release barrier on `len` prevents the consumer
253    /// from seeing `len != 0` before the data stores are visible.
254    #[inline]
255    pub fn write(&self, data: &[u8]) -> Result<(), RingError> {
256        let mask = self.capacity - 1;
257
258        let tail = self.tail().load(Ordering::Relaxed);
259        let head = self.head().load(Ordering::Acquire);
260
261        if ((tail + 1) & mask) == (head & mask) {
262            return Err(RingError::Full);
263        }
264
265        if data.len() > self.slot_size as usize {
266            return Err(RingError::MessageTooLarge);
267        }
268
269        let slot = self.slot_at(tail & mask);
270        // SAFETY: `slot` points into the ring's own physical memory.
271        // The producer owns the write index so no other thread writes here.
272        unsafe {
273            core::ptr::copy_nonoverlapping(data.as_ptr(), (*slot).data.as_mut_ptr(), data.len());
274        }
275
276        // Ensure data stores are visible before the consumer sees len != 0.
277        unsafe {
278            (*slot).len.store(data.len() as u16, Ordering::Release);
279        }
280        // Publish the slot.
281        self.tail().store(tail.wrapping_add(1), Ordering::Release);
282        Ok(())
283    }
284
285    /// Read one message from the ring into `buf`.
286    ///
287    /// Returns `Empty` if no message is available, or `BufferTooSmall` if
288    /// the waiting message is larger than `buf`.
289    #[inline]
290    pub fn read(&self, buf: &mut [u8]) -> Result<usize, RingError> {
291        let mask = self.capacity - 1;
292
293        let head = self.head().load(Ordering::Relaxed);
294        let tail = self.tail().load(Ordering::Acquire);
295
296        if (head & mask) == (tail & mask) {
297            return Err(RingError::Empty);
298        }
299
300        let slot = self.slot_at(head & mask);
301        // SAFETY: `slot` points into the ring's own physical memory.
302        // The consumer owns the read index so no other thread reads here.
303        let len = unsafe { (*slot).len.load(Ordering::Acquire) as usize };
304
305        if len > buf.len() {
306            return Err(RingError::BufferTooSmall);
307        }
308
309        unsafe {
310            core::ptr::copy_nonoverlapping((*slot).data.as_ptr(), buf.as_mut_ptr(), len);
311        }
312
313        // SAFETY: consumer-owned slot, no aliasing with producer.
314        unsafe {
315            (*slot).len.store(0, Ordering::Release);
316        }
317        self.head().store(head.wrapping_add(1), Ordering::Release);
318        Ok(len)
319    }
320
321    /// Non-blocking write.  Equivalent to [`write`](Self::write).
322    #[inline]
323    pub fn try_write(&self, data: &[u8]) -> Result<(), RingError> {
324        self.write(data)
325    }
326
327    /// Write multiple buffers in a single slot (scatter-gather).
328    /// Concatenates `bufs` and writes them as one message.
329    pub fn write_vectored(&self, bufs: &[&[u8]]) -> Result<(), RingError> {
330        let total: usize = bufs.iter().map(|b| b.len()).sum();
331        if total > self.slot_size as usize {
332            return Err(RingError::MessageTooLarge);
333        }
334        let mask = self.capacity - 1;
335        let tail = self.tail().load(Ordering::Relaxed);
336        let head = self.head().load(Ordering::Acquire);
337        if ((tail + 1) & mask) == (head & mask) {
338            return Err(RingError::Full);
339        }
340        let slot = self.slot_at(tail & mask);
341        let mut offset = 0;
342        unsafe {
343            for buf in bufs {
344                core::ptr::copy_nonoverlapping(
345                    buf.as_ptr(),
346                    (*slot).data.as_mut_ptr().add(offset),
347                    buf.len(),
348                );
349                offset += buf.len();
350            }
351        }
352        unsafe {
353            (*slot).len.store(total as u16, Ordering::Release);
354        }
355        self.tail().store(tail.wrapping_add(1), Ordering::Release);
356        Ok(())
357    }
358
359    /// Non-blocking read.  Returns `Ok(None)` when the ring is empty.
360    #[inline]
361    pub fn try_read(&self, buf: &mut [u8]) -> Result<Option<usize>, RingError> {
362        match self.read(buf) {
363            Ok(n) => Ok(Some(n)),
364            Err(RingError::Empty) => Ok(None),
365            Err(e) => Err(e),
366        }
367    }
368
369    /// Notify the consumer that new data is available (futex wake).
370    /// Increments the consumer notification counter.
371    pub fn notify_consumer_raw(&self) {
372        unsafe {
373            (*self.header).notify_cons.fetch_add(1, Ordering::Release);
374        }
375    }
376
377    /// Notify the producer that space has been freed (futex wake).
378    /// Increments the producer notification counter.
379    pub fn notify_producer_raw(&self) {
380        unsafe {
381            (*self.header).notify_prod.fetch_add(1, Ordering::Release);
382        }
383    }
384
385    /// Query whether the consumer should wake.  Returns true if data is
386    /// available (tail != head).  Used after `futex_wait` returns.
387    pub fn has_data(&self) -> bool {
388        unsafe {
389            let h = (*self.header).head.load(Ordering::Relaxed);
390            let t = (*self.header).tail.load(Ordering::Acquire);
391            (h & (self.capacity - 1)) != (t & (self.capacity - 1))
392        }
393    }
394
395    /// Query whether the producer can write without blocking.
396    /// Returns true if at least one slot is free.
397    pub fn has_space(&self) -> bool {
398        unsafe {
399            let t = (*self.header).tail.load(Ordering::Relaxed);
400            let h = (*self.header).head.load(Ordering::Acquire);
401            ((t + 1) & (self.capacity - 1)) != (h & (self.capacity - 1))
402        }
403    }
404
405    /// Provide a DMA-accessible buffer descriptor for zero-copy NIC
406    /// operation.  The physical address points directly into the slot's
407    /// data area so the NIC can DMA into the ring without an intermediate
408    /// copy.
409    pub fn dma_buffer(&self, slot_index: u32) -> DmaBuffer {
410        let mask = self.capacity - 1;
411        let slot_idx = slot_index & mask;
412        let header_size = size_of::<RingHeader>() as u64;
413        let slot_offset = header_size + (slot_idx as u64) * size_of::<RingSlot>() as u64;
414        const DATA_OFFSET: u64 = 4; // AtomicU16 len + AtomicU16 flags = 4 bytes
415        let phys = self.frames[0].start_address.as_u64() + slot_offset + DATA_OFFSET;
416        let virt = unsafe { &(*self.slot_at(slot_idx)).data as *const _ };
417        DmaBuffer {
418            phys_addr: phys,
419            virt_addr: virt as *const u8,
420        }
421    }
422
423    // ------------------------------------------------------------------
424    // Internal helpers
425    // ------------------------------------------------------------------
426
427    #[inline]
428    fn head(&self) -> &AtomicU32 {
429        unsafe { &(*self.header).head }
430    }
431
432    #[inline]
433    fn tail(&self) -> &AtomicU32 {
434        unsafe { &(*self.header).tail }
435    }
436
437    #[inline]
438    fn slot_at(&self, index: u32) -> *mut RingSlot {
439        unsafe { self.slots.add(index as usize) }
440    }
441}
442
443// Manual Drop to free frames.
444impl Drop for LockFreeRing {
445    fn drop(&mut self) {
446        for frame in self.frames.drain(..) {
447            with_irqs_disabled(|token| free_frame(token, frame));
448        }
449    }
450}
451
452// ---------------------------------------------------------------------------
453// IpcNotification impl
454// ---------------------------------------------------------------------------
455
456use super::transport::{IpcError, IpcNotification};
457
458impl IpcNotification for LockFreeRing {
459    fn notify_consumer(&self) {
460        self.notify_consumer_raw();
461    }
462
463    fn notify_producer(&self) {
464        self.notify_producer_raw();
465    }
466
467    fn wait_notification(&self) -> Result<(), IpcError> {
468        // Spin-then-futex: check once, then wait.
469        // The caller should busy-poll for N2a or use futex for N2b.
470        // This implementation provides the futex (blocking) path.
471        loop {
472            if self.has_data() {
473                return Ok(());
474            }
475            // NB: in production this would call futex_wait on notify_cons.
476            // For now, yield and retry (architecture-specific).
477            core::hint::spin_loop();
478        }
479    }
480}
481
482// ---------------------------------------------------------------------------
483// DmaBuffer
484// ---------------------------------------------------------------------------
485
486/// Descriptor for a DMA-accessible buffer region within the ring.
487pub struct DmaBuffer {
488    /// Physical address for the NIC's DMA engine.
489    pub phys_addr: u64,
490    /// Virtual address for the kernel/strate-net to read/write.
491    pub virt_addr: *const u8,
492}
493
494unsafe impl Send for DmaBuffer {}
495unsafe impl Sync for DmaBuffer {}
496
497// ---------------------------------------------------------------------------
498// Tests
499// ---------------------------------------------------------------------------
500
501#[cfg(test)]
502mod tests {
503    use super::*;
504
505    #[test]
506    fn ping_pong_64b() {
507        let ring = LockFreeRing::new(64, 256).unwrap();
508        let msg = [0xABu8; 64];
509        ring.write(&msg).unwrap();
510        let mut buf = [0u8; 64];
511        let n = ring.read(&mut buf).unwrap();
512        assert_eq!(n, 64);
513        assert_eq!(buf, [0xAB; 64]);
514    }
515
516    #[test]
517    fn full_ring() {
518        let ring = LockFreeRing::new(4, 64).unwrap();
519        // 4 slots, circular : 3 writes succeed, 1 reserved slot guards overflow
520        for _ in 0..3 {
521            ring.write(b"hello").unwrap();
522        }
523        assert_eq!(ring.write(b"world"), Err(RingError::Full));
524        // Read one, then write again
525        let mut buf = [0u8; 64];
526        ring.read(&mut buf).unwrap();
527        ring.write(b"world").unwrap();
528    }
529
530    #[test]
531    fn message_too_large() {
532        let ring = LockFreeRing::new(8, 64).unwrap();
533        let oversized = [0u8; 128];
534        assert_eq!(ring.write(&oversized), Err(RingError::MessageTooLarge));
535    }
536
537    #[test]
538    fn buffer_too_small() {
539        let ring = LockFreeRing::new(8, 256).unwrap();
540        ring.write(b"hello world").unwrap();
541        let mut tiny = [0u8; 4];
542        assert_eq!(ring.read(&mut tiny), Err(RingError::BufferTooSmall));
543    }
544
545    #[test]
546    fn empty_ring() {
547        let ring = LockFreeRing::new(8, 64).unwrap();
548        let mut buf = [0u8; 64];
549        assert_eq!(ring.read(&mut buf), Err(RingError::Empty));
550    }
551}