diff options
| author | salmonik <salmonik@tuta.io> | 2026-08-01 14:09:14 +0200 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-08-01 14:09:14 +0200 |
| commit | c7540c31d45dac68fa1852f5d25645d569b2da9d (patch) | |
| tree | c102aacb40f99d4f9bb87ab2a996cb08df0dfae0 /rsnix/src | |
| parent | shell: cat <path> TIL <marker> sugar + precise ordering errors (diff) | |
| download | rs-dos-c7540c31d45dac68fa1852f5d25645d569b2da9d.tar.gz rs-dos-c7540c31d45dac68fa1852f5d25645d569b2da9d.zip | |
klog: kernel log ring + /dev/klog + klog shell command
- rsnix/src/klog.rs: RAM ring of the last 128 serial log lines (128
chars each), fed by serial_println! so every boot/low-level message
is captured for free. Sequence numbers per line, wrap/drop
accounting; the ring never allocates (const-constructible static).
- /dev/klog char device; shell 'klog' is a thin alias for cat
/dev/klog with the same filters/redirection, so 'klog -t 5',
'klog -g fat' and 'klog WRITING /disk/.../boot.log' all work.
klog rejects positional args (usage) and input keywords.
- Shell user-facing output (banner, prompt, term_print/term_println,
pipeline sink, halt/poweroff) now writes to serial directly instead
of through serial_println!, keeping echo/cat output out of the boot
log (a streaming cat /dev/mem would otherwise wrap the ring).
- serial_println! macro hooks klog::append_fmt via format_args! (no
alloc at call sites); klog::dump formats the log for the device.
Verified in QEMU: 28 boot lines captured (seq 0..27), shell output
absent from the ring, klog WRITING persists a boot log (mtype-checked),
wrap verified with a temporary 140-line boot loop (128 kept / 40
dropped, seq 40..167, oldest-live correct; loop reverted); full
test-disk round trip still fsck-clean.
Diffstat (limited to 'rsnix/src')
| -rw-r--r-- | rsnix/src/fs.rs | 8 | ||||
| -rw-r--r-- | rsnix/src/klog.rs | 130 | ||||
| -rw-r--r-- | rsnix/src/lib.rs | 1 | ||||
| -rw-r--r-- | rsnix/src/serial.rs | 11 |
4 files changed, 149 insertions, 1 deletions
diff --git a/rsnix/src/fs.rs b/rsnix/src/fs.rs index a71e51a..063560a 100644 --- a/rsnix/src/fs.rs +++ b/rsnix/src/fs.rs | |||
| @@ -41,6 +41,7 @@ enum CharDevice { | |||
| 41 | Pci, | 41 | Pci, |
| 42 | Version, | 42 | Version, |
| 43 | Meminfo, | 43 | Meminfo, |
| 44 | Klog, | ||
| 44 | Null, | 45 | Null, |
| 45 | Zero, | 46 | Zero, |
| 46 | Console, | 47 | Console, |
| @@ -141,6 +142,12 @@ pub fn init() -> (usize, usize) { | |||
| 141 | NodeData::Char(CharDevice::Meminfo), | 142 | NodeData::Char(CharDevice::Meminfo), |
| 142 | ); | 143 | ); |
| 143 | add( | 144 | add( |
| 145 | "klog", | ||
| 146 | Some(dev), | ||
| 147 | FileType::Char, | ||
| 148 | NodeData::Char(CharDevice::Klog), | ||
| 149 | ); | ||
| 150 | add( | ||
| 144 | "null", | 151 | "null", |
| 145 | Some(dev), | 152 | Some(dev), |
| 146 | FileType::Char, | 153 | FileType::Char, |
| @@ -650,6 +657,7 @@ fn dev_text(dev: CharDevice, _pos: u64) -> String { | |||
| 650 | CharDevice::Pci => pci_listing(), | 657 | CharDevice::Pci => pci_listing(), |
| 651 | CharDevice::Version => version_text(), | 658 | CharDevice::Version => version_text(), |
| 652 | CharDevice::Meminfo => meminfo_text(), | 659 | CharDevice::Meminfo => meminfo_text(), |
| 660 | CharDevice::Klog => crate::klog::dump(), | ||
| 653 | CharDevice::Null | CharDevice::Console | CharDevice::Serial => String::new(), | 661 | CharDevice::Null | CharDevice::Console | CharDevice::Serial => String::new(), |
| 654 | CharDevice::Zero => String::new(), | 662 | CharDevice::Zero => String::new(), |
| 655 | } | 663 | } |
diff --git a/rsnix/src/klog.rs b/rsnix/src/klog.rs new file mode 100644 index 0000000..1e23460 --- /dev/null +++ b/rsnix/src/klog.rs | |||
| @@ -0,0 +1,130 @@ | |||
| 1 | // --------------------------------------------------------------------------- | ||
| 2 | // klog — kernel log ring buffer. | ||
| 3 | // | ||
| 4 | // A RAM ring of the last MAX_LINES serial log lines, fed by | ||
| 5 | // `serial_println!` (one choke point, so every boot/low-level message is | ||
| 6 | // captured for free). Exposed to the shell as the `/dev/klog` char | ||
| 7 | // device, so `klog` (alias for `cat /dev/klog`) shows the boot sequence | ||
| 8 | // and `klog WRITING /disk/.../boot.log` persists it via normal | ||
| 9 | // redirection. | ||
| 10 | // | ||
| 11 | // Line-based: the ring stores whole lines up to MAX_LINE bytes; longer | ||
| 12 | // lines are truncated. Each line gets a sequence number; when the ring | ||
| 13 | // wraps it drops the oldest line and counts it, so a dump can report how | ||
| 14 | // much history was lost. The ring never allocates (const-constructible | ||
| 15 | // static), so it works from the very first log line; only `dump()` | ||
| 16 | // allocates, and that is called at shell time. | ||
| 17 | // --------------------------------------------------------------------------- | ||
| 18 | |||
| 19 | extern crate alloc; | ||
| 20 | |||
| 21 | use alloc::string::String; | ||
| 22 | |||
| 23 | pub const MAX_LINES: usize = 128; | ||
| 24 | pub const MAX_LINE: usize = 128; | ||
| 25 | |||
| 26 | struct Ring { | ||
| 27 | /// Sequence counter for the next line to be appended (total ever). | ||
| 28 | seq: u64, | ||
| 29 | /// Lines dropped because the ring was full (wrap overwrites). | ||
| 30 | dropped: u64, | ||
| 31 | /// Live lines currently in the ring (0..=MAX_LINES). | ||
| 32 | count: usize, | ||
| 33 | /// Slot the next line will be written to (oldest once full). | ||
| 34 | next: usize, | ||
| 35 | lens: [u8; MAX_LINES], | ||
| 36 | bytes: [[u8; MAX_LINE]; MAX_LINES], | ||
| 37 | } | ||
| 38 | |||
| 39 | static mut RING: Ring = Ring { | ||
| 40 | seq: 0, | ||
| 41 | dropped: 0, | ||
| 42 | count: 0, | ||
| 43 | next: 0, | ||
| 44 | lens: [0; MAX_LINES], | ||
| 45 | bytes: [[0; MAX_LINE]; MAX_LINES], | ||
| 46 | }; | ||
| 47 | |||
| 48 | fn ring() -> &'static mut Ring { | ||
| 49 | unsafe { &mut *core::ptr::addr_of_mut!(RING) } | ||
| 50 | } | ||
| 51 | |||
| 52 | /// Append one line to the ring (line-oriented; newlines are stripped). | ||
| 53 | /// Truncated at MAX_LINE bytes. Never allocates, never logs. | ||
| 54 | pub fn append(line: &str) { | ||
| 55 | let r = ring(); | ||
| 56 | let b = line.as_bytes(); | ||
| 57 | let n = b.len().min(MAX_LINE); | ||
| 58 | if r.count == MAX_LINES { | ||
| 59 | r.dropped += 1; // overwrite the oldest line | ||
| 60 | } else { | ||
| 61 | r.count += 1; | ||
| 62 | } | ||
| 63 | r.bytes[r.next][..n].copy_from_slice(&b[..n]); | ||
| 64 | r.lens[r.next] = n as u8; | ||
| 65 | r.next = (r.next + 1) % MAX_LINES; | ||
| 66 | r.seq += 1; | ||
| 67 | } | ||
| 68 | |||
| 69 | /// Append the formatted output of a `serial_println!` — the macro hook. | ||
| 70 | /// `fmt::Write` may deliver the line in several chunks, so accumulate | ||
| 71 | /// into one line buffer first, then append the whole line. | ||
| 72 | pub fn append_fmt(args: core::fmt::Arguments<'_>) { | ||
| 73 | let mut line = LineBuf::new(); | ||
| 74 | let _ = core::fmt::write(&mut line, args); | ||
| 75 | append(line.as_str()); | ||
| 76 | } | ||
| 77 | |||
| 78 | struct LineBuf { | ||
| 79 | bytes: [u8; MAX_LINE], | ||
| 80 | len: usize, | ||
| 81 | } | ||
| 82 | |||
| 83 | impl LineBuf { | ||
| 84 | fn new() -> Self { | ||
| 85 | LineBuf { | ||
| 86 | bytes: [0; MAX_LINE], | ||
| 87 | len: 0, | ||
| 88 | } | ||
| 89 | } | ||
| 90 | fn as_str(&self) -> &str { | ||
| 91 | core::str::from_utf8(&self.bytes[..self.len]).unwrap_or("") | ||
| 92 | } | ||
| 93 | } | ||
| 94 | |||
| 95 | impl core::fmt::Write for LineBuf { | ||
| 96 | fn write_str(&mut self, s: &str) -> core::fmt::Result { | ||
| 97 | let n = s.len().min(MAX_LINE - self.len); | ||
| 98 | self.bytes[self.len..self.len + n].copy_from_slice(&s.as_bytes()[..n]); | ||
| 99 | self.len += n; | ||
| 100 | Ok(()) | ||
| 101 | } | ||
| 102 | } | ||
| 103 | |||
| 104 | /// The whole log as text: a header with the wrap/drop accounting, then | ||
| 105 | /// one line per entry, each prefixed with its sequence number. | ||
| 106 | pub fn dump() -> String { | ||
| 107 | let r = ring(); | ||
| 108 | let mut out = String::new(); | ||
| 109 | if r.count == 0 { | ||
| 110 | out += "klog: no messages logged yet\n"; | ||
| 111 | return out; | ||
| 112 | } | ||
| 113 | let first = r.seq - r.count as u64; | ||
| 114 | let last = r.seq - 1; | ||
| 115 | out += &alloc::format!( | ||
| 116 | "klog: {} line(s) kept, {} dropped on wrap, seq {}..{}\n", | ||
| 117 | r.count, | ||
| 118 | r.dropped, | ||
| 119 | first, | ||
| 120 | last | ||
| 121 | ); | ||
| 122 | let start = (r.next + MAX_LINES - r.count) % MAX_LINES; | ||
| 123 | for i in 0..r.count { | ||
| 124 | let idx = (start + i) % MAX_LINES; | ||
| 125 | let n = r.lens[idx] as usize; | ||
| 126 | let text = core::str::from_utf8(&r.bytes[idx][..n]).unwrap_or(""); | ||
| 127 | out += &alloc::format!("{:06} {}\n", first + i as u64, text); | ||
| 128 | } | ||
| 129 | out | ||
| 130 | } | ||
diff --git a/rsnix/src/lib.rs b/rsnix/src/lib.rs index fc823ea..a2d41da 100644 --- a/rsnix/src/lib.rs +++ b/rsnix/src/lib.rs | |||
| @@ -8,6 +8,7 @@ pub mod font; | |||
| 8 | pub mod fs; | 8 | pub mod fs; |
| 9 | pub mod gop; | 9 | pub mod gop; |
| 10 | pub mod interrupts; | 10 | pub mod interrupts; |
| 11 | pub mod klog; | ||
| 11 | pub mod memory; | 12 | pub mod memory; |
| 12 | pub mod paging; | 13 | pub mod paging; |
| 13 | pub mod pci; | 14 | pub mod pci; |
diff --git a/rsnix/src/serial.rs b/rsnix/src/serial.rs index 03fa570..484a114 100644 --- a/rsnix/src/serial.rs +++ b/rsnix/src/serial.rs | |||
| @@ -91,12 +91,21 @@ macro_rules! serial_print { | |||
| 91 | }}; | 91 | }}; |
| 92 | } | 92 | } |
| 93 | 93 | ||
| 94 | /// Prints a line to serial and appends it to the kernel log ring | ||
| 95 | /// (klog — see /dev/klog). Every kernel-side message funnels through | ||
| 96 | /// here, so the ring captures the whole boot sequence for free. | ||
| 97 | /// User-facing shell output must NOT use this macro (shell output would | ||
| 98 | /// pollute the boot log); it writes to the serial port directly. | ||
| 94 | #[macro_export] | 99 | #[macro_export] |
| 95 | macro_rules! serial_println { | 100 | macro_rules! serial_println { |
| 96 | () => { $crate::serial::write_str("\r\n"); }; | 101 | () => { |
| 102 | $crate::serial::write_str("\r\n"); | ||
| 103 | $crate::klog::append_fmt(core::format_args!("")); | ||
| 104 | }; | ||
| 97 | ($($arg:tt)*) => {{ | 105 | ($($arg:tt)*) => {{ |
| 98 | $crate::serial_print!($($arg)*); | 106 | $crate::serial_print!($($arg)*); |
| 99 | $crate::serial::write_str("\r\n"); | 107 | $crate::serial::write_str("\r\n"); |
| 108 | $crate::klog::append_fmt(core::format_args!($($arg)*)); | ||
| 100 | }}; | 109 | }}; |
| 101 | } | 110 | } |
| 102 | 111 | ||
