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}