mirror of
https://github.com/rdmsr/zag.git
synced 2026-11-01 23:58:21 +00:00
docs(ke): add more comments in log ring
This commit is contained in:
+83
-7
@@ -1,3 +1,62 @@
|
|||||||
|
//! Lock-free multi-producer multi-consumer ring buffer designed for kernel logging.
|
||||||
|
//!
|
||||||
|
//! # Overview
|
||||||
|
//!
|
||||||
|
//! `RingBuffer` is a concurrent ring buffer that allows multiple writers and readers
|
||||||
|
//! to operate simultaneously without locks, making it safe to use from interrupt and
|
||||||
|
//! NMI context. It is modelled after Linux's `printk_ringbuffer`, but is simpler (no "committed" state).
|
||||||
|
//! See linux's printk_ringbuffer.c for more details.
|
||||||
|
//!
|
||||||
|
//! The ring buffer is composed of three parallel arrays:
|
||||||
|
//!
|
||||||
|
//! - `descs` - descriptor ring: tracks the state and data location of each record
|
||||||
|
//! - `data` - data ring: byte storage for the actual messages
|
||||||
|
//! - `infos` - array containing metadata: stores sequence number, timestamp, and length per record
|
||||||
|
//!
|
||||||
|
//! `infos` is indexed by descriptor IDs, so it doesn't *really* count as a ring.
|
||||||
|
//! `head_id` is the ID of the newest descriptor, the last slot claimed by a writer.
|
||||||
|
//! `tail_id` is the ID of the oldest descriptor, the oldest slot which is the next to be evicted.
|
||||||
|
//! `tail_id` must always point to a free or published descriptor.
|
||||||
|
//! The number of live descriptors at any moment is `head_id - tail_id`,
|
||||||
|
//! when this reaches the maximum descriptor count (`desc_count`), the ring is full and `tail_id` must be advanced.
|
||||||
|
//! `head_lpos` and `tail_lpos` are the logical positions of the next data block to be written and the oldest data block, respectively.
|
||||||
|
//!
|
||||||
|
//! # Descriptor states
|
||||||
|
//!
|
||||||
|
//! Each descriptor transitions through the following states:
|
||||||
|
//!
|
||||||
|
//! Reserved -> Published -> Free
|
||||||
|
//!
|
||||||
|
//! `Reserved` A writer has claimed this slot and is actively writing.
|
||||||
|
//! `Published` The record is complete and visible to readers.
|
||||||
|
//! `Free` The slot has been recycled and is available for reuse.
|
||||||
|
//! `Miss` Pseudo-state: the slot has been recycled to a newer generation.
|
||||||
|
//! Returned when the ID embedded in a descriptor state does not match
|
||||||
|
//! the expected ID, indicating the record is gone.
|
||||||
|
//!
|
||||||
|
//! # IDs and logical positions
|
||||||
|
//!
|
||||||
|
//! Both descriptor IDs and data logical positions (`lpos`) are monotonically
|
||||||
|
//! increasing integers that never reset. The physical array index is derived
|
||||||
|
//! by masking off the lower bits:
|
||||||
|
//!
|
||||||
|
//! desc index = id & desc_mask
|
||||||
|
//! data index = lpos & data_mask
|
||||||
|
//!
|
||||||
|
//! The upper bits act as a generation/wrap counter. This means two IDs that map
|
||||||
|
//! to the same physical slot can always be distinguished by comparing the full
|
||||||
|
//! integer, which prevents ABA bugs.
|
||||||
|
//! The `State` atomic packs both the descriptor ID and its state into a single
|
||||||
|
//! `usize`, allowing both to be read and updated atomically via CAS (a CAS is used to prevent ABA where the generation could overflow):
|
||||||
|
//!
|
||||||
|
//! [ id (upper 62 bits) | state (lower 2 bits) ] (on 64-bit platforms)
|
||||||
|
//!
|
||||||
|
//! # Wrap handling
|
||||||
|
//!
|
||||||
|
//! Data blocks are never split across the physical boundary of the data array.
|
||||||
|
//! If an allocation would cross the boundary, the block is placed at offset 0 of
|
||||||
|
//! the next generation and a stub (containing only the descriptor ID) is left at
|
||||||
|
//! the original position so the data ring can be walked linearly during eviction.
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const base = @import("base");
|
const base = @import("base");
|
||||||
const rtl = @import("rtl");
|
const rtl = @import("rtl");
|
||||||
@@ -87,7 +146,7 @@ pub fn RingBuffer(data_size_bits: usize, avg_msg_bits: usize) type {
|
|||||||
|
|
||||||
const Self = @This();
|
const Self = @This();
|
||||||
|
|
||||||
// Returns the exact Id of the descriptor that occupied this physical slot one loop ago.
|
// Return the exact Id of the descriptor that occupied this physical slot one generation ago.
|
||||||
fn prev_wrap(id: Id) Id {
|
fn prev_wrap(id: Id) Id {
|
||||||
return id -% desc_count;
|
return id -% desc_count;
|
||||||
}
|
}
|
||||||
@@ -100,6 +159,7 @@ pub fn RingBuffer(data_size_bits: usize, avg_msg_bits: usize) type {
|
|||||||
return expected.state;
|
return expected.state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure block sizes are always aligned to usize.
|
||||||
fn to_block_size(size: usize) usize {
|
fn to_block_size(size: usize) usize {
|
||||||
var sz = size;
|
var sz = size;
|
||||||
sz += @sizeOf(DataBlock);
|
sz += @sizeOf(DataBlock);
|
||||||
@@ -124,9 +184,12 @@ pub fn RingBuffer(data_size_bits: usize, avg_msg_bits: usize) type {
|
|||||||
return size <= data_size / 2;
|
return size <= data_size / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Return true if `lpos_current` has not yet reached `lpos_next`.
|
||||||
|
// Uses wrapping subtraction so that:
|
||||||
|
// - if lpos_current > lpos_next (another CPU already advanced past the target), returns false.
|
||||||
|
// - if lpos_current == lpos_next (already at target), the -% 1 wraps to usize max, returns false.
|
||||||
|
// - if the distance exceeds data_size (should never happen), returns false.
|
||||||
fn need_more_space(lpos_current: usize, lpos_next: usize) bool {
|
fn need_more_space(lpos_current: usize, lpos_next: usize) bool {
|
||||||
// Unsigned wrapping arithmetic safely calculates distance between positions
|
|
||||||
// across overflow, checking if it exceeds the ring's physical capacity.
|
|
||||||
return lpos_next -% lpos_current -% 1 < data_size;
|
return lpos_next -% lpos_current -% 1 < data_size;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,8 +419,9 @@ pub fn RingBuffer(data_size_bits: usize, avg_msg_bits: usize) type {
|
|||||||
return new_id;
|
return new_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if a block crosses the buffer boundary.
|
// Return true if the range [begin, end) crosses the buffer boundary.
|
||||||
// `lpos` encodes a "generation" in its upper bits to track wrapping.
|
// Compares the generation counters (upper bits) of begin and end-1.
|
||||||
|
// The -% 1 ensures a block that fits exactly at the end does not count as wrapping.
|
||||||
fn data_wraps(begin: usize, end: usize) bool {
|
fn data_wraps(begin: usize, end: usize) bool {
|
||||||
return (begin >> data_size_bits) != ((end -% 1) >> data_size_bits);
|
return (begin >> data_size_bits) != ((end -% 1) >> data_size_bits);
|
||||||
}
|
}
|
||||||
@@ -453,17 +517,25 @@ pub fn RingBuffer(data_size_bits: usize, avg_msg_bits: usize) type {
|
|||||||
|
|
||||||
/// Initialize and return a new ring buffer instance.
|
/// Initialize and return a new ring buffer instance.
|
||||||
pub fn init() Self {
|
pub fn init() Self {
|
||||||
|
// Start near usize max so that the first wrap of the ID/lpos space happens
|
||||||
|
// almost immediately, testing overflow handling early rather than after
|
||||||
|
// billions of records.
|
||||||
const dummy_id: Id = 0 -% @as(Id, desc_count + 1);
|
const dummy_id: Id = 0 -% @as(Id, desc_count + 1);
|
||||||
const blk0: usize = -%@as(usize, data_size);
|
const blk0: usize = -%@as(usize, data_size);
|
||||||
|
|
||||||
var self: Self = std.mem.zeroes(Self);
|
var self: Self = std.mem.zeroes(Self);
|
||||||
|
|
||||||
|
// Place a dummy descriptor in the last slot. head_id and tail_id both start
|
||||||
|
// here so that the first reservation (head_id + 1) lands in slot 0, giving
|
||||||
|
// the first real record sequence number 0.
|
||||||
self.descs[desc_count - 1].state.store(.{ .state = .Free, .id = dummy_id }, .monotonic);
|
self.descs[desc_count - 1].state.store(.{ .state = .Free, .id = dummy_id }, .monotonic);
|
||||||
self.descs[desc_count - 1].blk_pos = .{ .begin = lpos_no_data, .end = lpos_no_data };
|
self.descs[desc_count - 1].blk_pos = .{ .begin = lpos_no_data, .end = lpos_no_data };
|
||||||
|
|
||||||
self.head_id.store(dummy_id, .monotonic);
|
self.head_id.store(dummy_id, .monotonic);
|
||||||
self.tail_id.store(dummy_id, .monotonic);
|
self.tail_id.store(dummy_id, .monotonic);
|
||||||
|
|
||||||
|
// Make it -(desc_count) so that when the slot is first reserved and the sequence is advanced by desc_count,
|
||||||
|
// the first record gets sequence number 0.
|
||||||
self.infos[0].sequence = -%(@as(u64, desc_count));
|
self.infos[0].sequence = -%(@as(u64, desc_count));
|
||||||
|
|
||||||
self.head_lpos.store(blk0, .monotonic);
|
self.head_lpos.store(blk0, .monotonic);
|
||||||
@@ -491,10 +563,14 @@ pub fn RingBuffer(data_size_bits: usize, avg_msg_bits: usize) type {
|
|||||||
reservation.id = id;
|
reservation.id = id;
|
||||||
|
|
||||||
if (seq == 0 and (id & desc_mask) != 0) {
|
if (seq == 0 and (id & desc_mask) != 0) {
|
||||||
// Bootstrap sequence number for initial descriptors.
|
// First time this slot has been used.
|
||||||
|
// Slot 0 never hits this branch, its seq is set to -(desc_count)
|
||||||
|
// in init() so that the else branch below produces seq=0 on its first use.
|
||||||
info.sequence = @as(u64, id & desc_mask);
|
info.sequence = @as(u64, id & desc_mask);
|
||||||
} else {
|
} else {
|
||||||
// Advance sequence by ring size for subsequent iterations.
|
// Slot is being recycled (or is slot 0 on first use).
|
||||||
|
// Adding desc_count advances the sequence by one full ring wrap,
|
||||||
|
// keeping sequence numbers monotonically increasing across generations.
|
||||||
info.sequence = seq +% desc_count;
|
info.sequence = seq +% desc_count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user