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
+49 -1
View File
@@ -58,7 +58,7 @@ pub const SystemCall = enum(u64) {
signal_bind = 29, // signal_bind(endpoint) -> 0/-errno: nominate the endpoint this process's signals arrive on
process_signal = 30, // process_signal(id, signal) -> 0/-errno: post a signal to a child (or to yourself)
timer_bind = 31, // timer_bind(endpoint, ms) -> 0/-errno: one-shot timer — posts a notification when ms elapse
klog_read = 32, // klog_read(offset, ptr, len) -> bytes copied: copy the kernel RAM log buffer out to a user buffer (for persisting the boot log to disk)
klog_read = 32, // klog_read(offset, ptr, len) -> bytes copied: copy tagged log-ring stream bytes from `offset` out to a user buffer; fails once `offset` falls behind the ring's tail (re-sync via klog_status)
wall_clock = 33, // wall_clock() -> Unix epoch seconds (UTC): the RTC wall-clock time, for filesystem timestamps (mtime). Monotonic time is `clock`.
shm_create = 34, // shm_create(len) -> virtual_address (rax), handle (rdx): a shareable, zeroed, cacheable RAM region mapped into this AS; the handle is a capability passed to another process as an ipc_call send_cap (docs/display-v2.md)
shm_map = 35, // shm_map(cap) -> virtual_address: map the shared region named by a received capability into this address space (the same physical pages the creator sees)
@@ -71,6 +71,7 @@ pub const SystemCall = enum(u64) {
thread_self = 42, // thread_self() -> tid: the calling thread's kernel task id (runtime.Thread.getCurrentId)
thread_join = 43, // thread_join(tid) -> 0: block until the thread with id `tid` has exited (runtime.Thread.join; no per-thread IPC endpoint) (docs/threading.md)
set_thread_pointer = 44, // set_thread_pointer(addr) -> 0: set the caller's thread pointer (user-space TLS base; x86_64 IA32_FS_BASE, aarch64 TPIDR_EL0); restored per task across context switches (docs/threading-plan.md M10)
klog_status = 45, // klog_status(ptr) -> 0: copy a KlogStatus (ring cursors + the boot wall-clock anchor) out to a user buffer
_,
};
@@ -187,6 +188,53 @@ pub const ProcessDescriptor = extern struct {
name: [maximum_process_name]u8, // argv[0] at spawn; empty for kernel tasks
};
// --- the tagged kernel log ring (klog) ---------------------------------------
// Every `debug_write` becomes one RECORD per payload line, stamped by the kernel
// with the sender's pid, task name (its binary path), level, a per-boot sequence
// number, and a monotonic timestamp. `klog_read` copies raw stream bytes — a
// reader parses [KlogRecordHeader][name][message] frames, each padded to
// `klog_record_alignment`. Sequence gaps tell a reader exactly how many records
// the ring overwrote while it wasn't looking.
/// Log level of a klog record — std.log's levels plus `raw` (untagged bytes:
/// kernel prints and legacy runtime.system.write output).
pub const KlogLevel = enum(u8) { err = 0, warn = 1, info = 2, debug = 3, raw = 4 };
/// "RK" — leads every record; a parser's resync/corruption guard.
pub const klog_record_magic: u16 = 0x4B52;
/// KlogRecordHeader.flags bit: the emitter truncated the payload to fit.
pub const klog_flag_truncated: u8 = 1;
/// Header of one ring record, followed by `name_len` bytes of task name and
/// `message_len` bytes of payload; the whole record is padded to 8 bytes.
pub const KlogRecordHeader = extern struct {
magic: u16, // klog_record_magic
level: KlogLevel,
name_len: u8, // 0..maximum_process_name
pid: u32, // sender process id; 0 = the kernel itself
sequence: u64, // per-boot monotonic record number (gaps = lost records)
timestamp_ns: u64, // monotonic ns since boot (the `clock` timebase)
message_len: u16, // payload bytes (excludes the record's trailing pad)
flags: u8, // klog_flag_* bits
_reserved: [5]u8,
};
pub const klog_record_header_size: usize = 32; // @sizeOf(KlogRecordHeader), pinned by a test
pub const klog_record_alignment: usize = 8;
/// Per-record payload cap (one line; longer emitter lines are truncated).
pub const klog_maximum_message: usize = 256;
/// The klog_status copy-out: the ring's live cursors plus the wall-clock time
/// of boot — the anchor a log persister names its per-boot directory with and
/// combines with record timestamps for wall-clock line stamps.
pub const KlogStatus = extern struct {
tail: u64, // oldest retained stream offset — always a record boundary
head: u64, // next byte to be written (end of stream)
next_sequence: u64, // the sequence the next record will get
boot_unix_seconds: u64, // wall-clock time of boot (RTC anchor)
};
/// Well-known IPC service ids for the bootstrap name registry (create_ipc_endpoint +
/// ipc_register/ipc_lookup). Small integers, so no string interning is needed
/// during bring-up. The VFS server registers under `vfs`; clients look it up.