Flash 232 lines
// klog_ring: kernel log byte-ring (overwrite-oldest) — the dmesg backend.
//
// A single static byte ring that main_output (src/utilc.zig) tees every
// emitted line into, so the boot log survives in RAM and a userland
// `dmesg` can read it back through sys_klog_read (slot 38). This makes the
// Mini-UART / FTDI adapter unnecessary for post-boot diagnosis over the
// USB-C console: the boot log becomes a syscall away.
//
// Pure data + pure logic — no MMIO, no extern — so it host-unit-tests with
// no hardware (mirrors src/usb_tx_ring.zig and src/console.zig's RX ring).
// The kernel imports this as the named module "klog_ring": src/utilc.zig
// pushes into `klog`, src/sys.zig snapshots out of it; both share the one
// instance because Zig analyses a named module once.
//
// Monotone u64 head/tail with modulo indexing, exactly as usb_tx_ring —
// with ONE deliberate inversion. usb_tx_ring is drop-NEWEST (push returns
// false when full: backpressure on a live TX path that must not lose the
// in-flight bytes). A log is the opposite: it always keeps the MOST RECENT
// bytes and lets the oldest scroll off. So push here never fails; when the
// window is full it advances `tail` to drop the oldest byte.
//
// Concurrency / memory-safety. main_output is called from kernel, syscall,
// AND IRQ context, and as early as boot.S before `current` exists — so the
// tee CANNOT take preempt_disable (which dereferences `current`). push() is
// therefore lock-free and best-effort: every buffer index is masked
// `% SIZE` and every read length is clamped to `available()`, so the worst
// an IRQ-interrupts-push interleaving can do is garble a byte or mis-count
// by one — never an out-of-bounds access, never a wild pointer. Torn UART
// output is already accepted here (mini_uart_send_string holds no lock
// either); torn klog bytes are the same posture. A future SMP/locking pass
// revisits, exactly as console.zig documents for its RX ring.
const defs = #import("syscall_defs")
// Ring capacity. Lives in lib/syscall_defs.zig because userland `dmesg`
// sizes its read buffer against the same number (an ABI-visible constant,
// like Dirent). Big enough to hold a full interactive boot log
// (firmware marker → `Reached target Shell`); the much longer in-harness log wraps,
// which is fine — [TEST] klog only asserts a recent marker survives.
pub const SIZE u64 = defs.KLOG_SIZE
pub const KlogRing = struct {
buf [SIZE]u8 = .{0} ** SIZE,
head u64 = 0, // total bytes ever pushed (monotone)
tail u64 = 0, // start of the retained window; head - tail <= SIZE
// Bytes currently retained (head/tail are monotone; -% wraps cleanly).
pub fn available(self *KlogRing) u64 {
return self.head -% self.tail
}
// The byte at absolute monotone position `pos`, masked into the ring.
// Only meaningful for pos in [tail, head); callers clamp to that.
pub fn byteAt(self *KlogRing, pos u64) u8 {
return self.buf[pos % SIZE]
}
// Append one byte, overwriting the oldest when the ring is full.
pub fn push(self *mut KlogRing, byte u8) void {
self.buf[self.head % SIZE] = byte
self.head +%= 1
// Full → advance tail so the window never exceeds SIZE. A clamp
// (not a decrement) so a racy double-push can never leave the
// window larger than SIZE — it self-heals to exactly SIZE.
if (self.head -% self.tail > SIZE) { self.tail = self.head -% SIZE }
}
// Append a NUL-terminated string. The main_output tee. NOT recursive
// and allocation-free, so it is safe to call from inside main_output
// (no re-entry through main_output, no free-page perturbation).
pub fn pushStr(self *mut KlogRing, str [*:0]u8) void {
var i u64 = 0
while (str[i] != 0) {
self.push(str[i])
i += 1
}
}
// Copy the most-recent min(dst.len, available) bytes into dst,
// oldest-of-that-window first, and return the count. A snapshot: it
// neither consumes nor blocks. dmesg passes a buffer >= SIZE to get
// the whole retained log; a smaller buffer yields the most recent
// tail — the sensible default for a log viewer. The kernel
// sys_klog_read handler reproduces this windowing directly (it must
// bounce through a 512-byte kernel buffer for copy_to_user), so this
// method is the host-tested reference for that arithmetic.
pub fn snapshot(self *KlogRing, dst []mut u8) u64 {
const n = #min(self.available(), #as(u64, dst.len))
const start = self.head -% n // most recent n bytes
var i u64 = 0
while (i < n) {
dst[#intCast(i)] = self.byteAt(start +% i)
i += 1
}
return n
}
}
// The one kernel-wide log ring. BSS-resident: zero-initialised at boot,
// never allocated, so teeing into it cannot perturb the free-page
// baseline the harness asserts.
pub var klog KlogRing = .{}
// ---- Host tests ----
const std = #import("std")
const testing = std.testing
// A small ring so wrap + overwrite are cheap to exercise. The host build
// reaches the SIZE-typed `KlogRing` via the kernel; the tests below build
// their own small TestRing so SIZE (16 KiB) need not be filled byte by
// byte. The arithmetic is identical — only the modulus differs.
fn TestRing(comptime size u64) type {
return struct {
buf [size]u8 = .{0} ** size,
head u64 = 0,
tail u64 = 0,
const Self = #This()
fn available(self *Self) u64 {
return self.head -% self.tail
}
fn push(self *mut Self, byte u8) void {
self.buf[self.head % size] = byte
self.head +%= 1
if (self.head -% self.tail > size) { self.tail = self.head -% size }
}
fn pushStr(self *mut Self, s []u8) void {
for b in s { self.push(b) }
}
fn snapshot(self *Self, dst []mut u8) u64 {
const n = #min(self.available(), #as(u64, dst.len))
const start = self.head -% n
var i u64 = 0
while (i < n) {
dst[#intCast(i)] = self.buf[(start +% i) % size]
i += 1
}
return n
}
}
}
const Ring8 = TestRing(8)
test "push then snapshot round-trips bytes in order" {
var r = Ring8{}
try testing.expectEqual(#as(u64, 0), r.available())
r.pushStr("abc")
try testing.expectEqual(#as(u64, 3), r.available())
var buf [8]u8 = undefined
try testing.expectEqual(#as(u64, 3), r.snapshot(buf[0..]))
try testing.expectEqualStrings("abc", buf[0..3])
try testing.expectEqual(#as(u64, 3), r.available()) // snapshot did not consume
}
test "overwrite-oldest: a full ring keeps the most recent SIZE bytes" {
var r = Ring8{}
r.pushStr("0123456789") // 10 bytes into an 8-byte ring → drop "01"
try testing.expectEqual(#as(u64, 8), r.available())
var buf [8]u8 = undefined
try testing.expectEqual(#as(u64, 8), r.snapshot(buf[0..]))
try testing.expectEqualStrings("23456789", buf[0..8]) // oldest two scrolled off
}
test "snapshot caps to dst.len and returns the most recent tail" {
var r = Ring8{}
r.pushStr("ABCDE")
var small [3]u8 = undefined // dst < available → most recent 3
try testing.expectEqual(#as(u64, 3), r.snapshot(small[0..]))
try testing.expectEqualStrings("CDE", small[0..3])
}
test "snapshot on an empty ring copies nothing" {
var r = Ring8{}
var buf [8]u8 = undefined
try testing.expectEqual(#as(u64, 0), r.snapshot(buf[0..]))
}
test "snapshot clamps to available when dst is larger" {
var r = Ring8{}
r.pushStr("hi")
var big [8]u8 = undefined
try testing.expectEqual(#as(u64, 2), r.snapshot(big[0..]))
try testing.expectEqualStrings("hi", big[0..2])
}
test "a marker pushed last survives an overwrite and ends the snapshot" {
// Mirrors the [TEST] klog assertion: flood the ring, then push a
// marker, and confirm the marker is the tail of the snapshot.
var r = Ring8{}
var i u8 = 0
while (i < 50) {
r.push('.')
i += 1
}
r.pushStr("klog")
var buf [8]u8 = undefined
const n = r.snapshot(buf[0..])
try testing.expectEqualStrings("klog", buf[n - 4 .. n])
}
test "counters stay correctly ordered across u64 wraparound" {
var r = Ring8{}
r.head = std.math.maxInt(u64) - 2
r.tail = std.math.maxInt(u64) - 2
try testing.expectEqual(#as(u64, 0), r.available())
r.pushStr("XYZ") // head crosses the u64 wrap
try testing.expectEqual(#as(u64, 3), r.available())
var buf [8]u8 = undefined
try testing.expectEqual(#as(u64, 3), r.snapshot(buf[0..]))
try testing.expectEqualStrings("XYZ", buf[0..3])
}
// The shipping KlogRing (SIZE = 16 KiB) shares the exact arithmetic; one
// direct test guards the real type + the overwrite boundary at SIZE.
test "KlogRing overwrites at exactly SIZE and keeps the newest byte" {
var r = KlogRing{}
var i u64 = 0
while (i < SIZE) {
r.push('a')
i += 1
}
try testing.expectEqual(SIZE, r.available())
r.push('Z') // one past full → oldest 'a' drops, window stays SIZE
try testing.expectEqual(SIZE, r.available())
var tail [1]u8 = undefined
try testing.expectEqual(#as(u64, 1), r.snapshot(tail[0..]))
try testing.expectEqual(#as(u8, 'Z'), tail[0])
}