kernel: tagged log ring — per-line pid/name/level records, klog_status

Replace the linear keep-earliest RAM buffer with a 512 KiB ring of
framed records (log-ring.zig, host-tested): every debug_write becomes
one record per payload line, stamped by the kernel with the sender's
pid, task name (its binary path), level, per-boot sequence number, and
monotonic timestamp. Attribution is structural — a payload cannot forge
another sender's tag, and newline injection lands inside the forger's
own next record. Oldest records are overwritten when full; sequence
gaps make the loss countable.

debug_write gains a level argument (err/warn/info/debug/raw; old
two-arg callers clamp to raw). klog_read becomes a stream-offset read
that fails once the cursor falls behind the ring's tail; the new
klog_status (#45) returns the cursors plus the boot wall-clock anchor —
what the logger service will name per-boot log directories with.

The log now guards itself with a dedicated spinlock (BKL -> log lock
order, never the reverse); panic paths use a bounded try-acquire and
fall back to sinks-only. Serial rendering keeps the historical
transcript byte-identical for kernel and legacy raw output; leveled
records get a kernel-rendered name prefix. log-flush/init's interim
drains start at the ring tail and write framed records until the logger
service replaces them.
This commit is contained in:
Daniel Samson
2026-07-21 15:30:28 +01:00
parent 0a84e52bf8
commit 9f18d8340e
10 changed files with 549 additions and 80 deletions
+36 -11
View File
@@ -21,10 +21,24 @@ pub fn yield() void {
_ = sc.systemCall0(.yield);
}
/// Write raw bytes to the kernel log (a bring-up diagnostic; real output goes
/// through the console/VFS later). Returns the byte count, or a wrapped -1.
/// The tagged-log level of a record — re-exported so runtime.log and the logger
/// service don't import `abi` themselves.
pub const KlogLevel = abi.KlogLevel;
pub const KlogStatus = abi.KlogStatus;
pub const KlogRecordHeader = abi.KlogRecordHeader;
/// Write raw bytes to the kernel log (bring-up/panic diagnostics; ordinary
/// output goes through std.log -> writeRecord). The kernel stamps the record
/// with this process's id and name. Returns the byte count, or a wrapped -1.
pub fn write(message: []const u8) usize {
return sc.systemCall2(.debug_write, @intFromPtr(message.ptr), message.len);
return writeRecord(.raw, message);
}
/// Emit one leveled record into the tagged kernel log ring. The kernel stamps
/// pid/name/sequence/timestamp; the payload should be a single line (embedded
/// newlines split into further records).
pub fn writeRecord(level: KlogLevel, message: []const u8) usize {
return sc.systemCall3(.debug_write, @intFromPtr(message.ptr), message.len, @intFromEnum(level));
}
/// Block the caller for `ms` milliseconds.
@@ -59,14 +73,25 @@ pub fn wallClock() u64 {
return @intCast(sc.systemCall0(.wall_clock));
}
/// Copy bytes out of the kernel's in-memory diagnostic log — the accumulated
/// stream of everything `write` (and the kernel itself) has emitted — starting at
/// `offset`, into `out`. Returns the number of bytes copied (0 at end of buffer).
/// A program reads the whole log by looping from offset 0, advancing by the return
/// value, until it gets 0. This is how the boot log is persisted to disk on a
/// headless/real machine where serial output is otherwise lost.
pub fn klogRead(offset: usize, out: []u8) usize {
return sc.systemCall3(.klog_read, offset, @intFromPtr(out.ptr), out.len);
/// Copy bytes out of the tagged kernel log ring — framed records of everything
/// every process (and the kernel) has emitted — starting at stream offset
/// `offset`, into `out`. Returns the byte count (0 = caught up), or null when
/// `offset` fell behind the ring's tail (those records were overwritten) or
/// lies past its head; re-sync via `klogStatus`. A reader parses
/// [KlogRecordHeader][name][message] frames (8-byte aligned) from the bytes.
pub fn klogRead(offset: u64, out: []u8) ?usize {
const r = sc.systemCall3(.klog_read, offset, @intFromPtr(out.ptr), out.len);
if (@as(isize, @bitCast(r)) < 0) return null;
return r;
}
/// The log ring's live cursors (oldest retained offset, end of stream, next
/// sequence number) plus the wall-clock time of boot — how a log reader starts,
/// detects loss, and names a per-boot log directory.
pub fn klogStatus() ?KlogStatus {
var status: KlogStatus = undefined;
if (@as(isize, @bitCast(sc.systemCall1(.klog_status, @intFromPtr(&status)))) != 0) return null;
return status;
}
/// End the process. Never returns.