document the multi-sink logging model
This commit is contained in:
@@ -73,6 +73,10 @@ Cutting across all of these:
|
|||||||
- **[testing.md](testing.md) — testing.** How the kernel is tested by booting it in
|
- **[testing.md](testing.md) — testing.** How the kernel is tested by booting it in
|
||||||
QEMU and asserting on its serial output — reproducibly, and structured so the
|
QEMU and asserting on its serial output — reproducibly, and structured so the
|
||||||
same tests run across architectures.
|
same tests run across architectures.
|
||||||
|
- **[logging.md](logging.md) — logging.** The multi-sink diagnostic log (serial,
|
||||||
|
0xE9 debugcon, file later) kept separate from the framebuffer display, plus the
|
||||||
|
robustness path: optional framebuffer, POST-code checkpoints, and a persistent
|
||||||
|
panic breadcrumb so the kernel survives — and can be diagnosed — with no output.
|
||||||
|
|
||||||
## How the pieces relate
|
## How the pieces relate
|
||||||
|
|
||||||
|
|||||||
+104
@@ -0,0 +1,104 @@
|
|||||||
|
# Logging: the diagnostic log vs. the display
|
||||||
|
|
||||||
|
danos separates two things that are easy to conflate: the **diagnostic log** — the
|
||||||
|
machine-readable stream of *what the kernel is doing* — and the **display**, the
|
||||||
|
framebuffer surface the OS draws on. They are different concerns with different
|
||||||
|
lifetimes, so they're different code paths.
|
||||||
|
|
||||||
|
The guiding rule: **output is a diagnostic convenience, never a correctness
|
||||||
|
dependency.** The kernel must boot and run correctly with *zero* output channels —
|
||||||
|
no serial, no screen. Logging that can take the kernel down isn't robust; it's a
|
||||||
|
liability. This is the same [resilience](resilience.md) posture the rest of the
|
||||||
|
kernel follows.
|
||||||
|
|
||||||
|
## The log is multi-sink
|
||||||
|
|
||||||
|
`src/kernel/log.zig` is the diagnostic log. It fans a message out to a set of
|
||||||
|
registered **sinks**, each best-effort and self-guarding:
|
||||||
|
|
||||||
|
```zig
|
||||||
|
log.addSink(arch.serialWrite); // the serial UART
|
||||||
|
if (arch.debugconPresent()) log.addSink(arch.debugconWrite); // 0xE9 debug console
|
||||||
|
// later: log.addSink(fs.logWrite); // a file on a ramdisk / USB / SSD
|
||||||
|
log.write("…"); log.print("x={d}\n", .{x});
|
||||||
|
```
|
||||||
|
|
||||||
|
Properties that matter:
|
||||||
|
|
||||||
|
- **No allocation.** The sink table is a fixed array, so the log works before the
|
||||||
|
heap is up and inside a panic.
|
||||||
|
- **Best-effort.** A sink whose device is absent is a no-op (e.g. writing to a
|
||||||
|
missing UART just goes nowhere — the TX-wait is bounded so it can't hang). A
|
||||||
|
message reaches whatever channels exist; if none do, the kernel runs on, silent.
|
||||||
|
- **Order-independent.** Every registered sink gets every message. Adding the file
|
||||||
|
logger later is one `addSink` call and **zero** changes to call sites.
|
||||||
|
|
||||||
|
## The framebuffer is *not* a log sink
|
||||||
|
|
||||||
|
The framebuffer is a general graphics surface, **not inherently a text terminal**.
|
||||||
|
Today `src/kernel/console.zig` paints a text grid on it as a *bootstrap* console, but
|
||||||
|
that's a stop-gap: once the driver machinery exists the framebuffer becomes a proper
|
||||||
|
**graphics device driver**, and the text crutch goes away. So the log must not assume
|
||||||
|
it — routing the verbose log through a pixel console would bake in "the OS is text".
|
||||||
|
|
||||||
|
Instead the two paths are explicit:
|
||||||
|
|
||||||
|
```
|
||||||
|
verbose diagnostics ──► log ──► serial, debugcon, (file later)
|
||||||
|
user status / panics ──► status() ──► log (above) + framebuffer (if present)
|
||||||
|
```
|
||||||
|
|
||||||
|
A handful of user-facing lines (`kernel initialised`, a panic) go through
|
||||||
|
`main.zig`'s `status()` / `statusPrint()`, which write to the log **and** paint the
|
||||||
|
framebuffer when one is present. Everything else uses `log.*` and never touches the
|
||||||
|
screen. `console.write` is a no-op when the firmware gave us no framebuffer.
|
||||||
|
|
||||||
|
## Optional framebuffer (headless machines)
|
||||||
|
|
||||||
|
A framebuffer is not guaranteed — a headless server exposes no UEFI Graphics Output
|
||||||
|
Protocol. That used to be *fatal* (the loader failed the boot). Now the loader hands
|
||||||
|
over a "no framebuffer" descriptor (`base == 0`) rather than failing, and
|
||||||
|
`Framebuffer.present()` (in `src/root.zig`) gates every on-screen path. A headless,
|
||||||
|
serial-less machine boots and runs correctly — it just goes quiet.
|
||||||
|
|
||||||
|
## Last-resort channels (no text output at all)
|
||||||
|
|
||||||
|
Two signals bypass the sink list, because they must survive even a total
|
||||||
|
output-channel failure:
|
||||||
|
|
||||||
|
- **`log.checkpoint(code)`** — a one-byte **POST code** to I/O port `0x80` (a POST
|
||||||
|
card or BMC shows it). `main.zig` emits one at each boot milestone (`cp_paging`,
|
||||||
|
`cp_heap`, …) and on a fault/panic, so "where did it hang?" is answerable with no
|
||||||
|
text output whatsoever. Writing `0x80` is universally safe.
|
||||||
|
- **`log.recordPanic(msg)`** — stamps the panic message into a fixed record
|
||||||
|
(`log.panic_record`, with a `magic` written last). A post-mortem — an attached
|
||||||
|
debugger, a RAM dump, or a future file/pstore reader — recovers *what killed it*
|
||||||
|
even though nothing was on screen.
|
||||||
|
|
||||||
|
The panic and CPU-exception handlers fan out to every sink, emit a POST code, and
|
||||||
|
drop the breadcrumb — they never assume a console.
|
||||||
|
|
||||||
|
## The 0xE9 debug console
|
||||||
|
|
||||||
|
Port `0xE9` is the Bochs/QEMU debug console. It's detected safely: the port returns
|
||||||
|
`0xE9` when read if present, and `0xFF` on real hardware, so `debugconPresent()`
|
||||||
|
only enables the sink when it's really there. Under QEMU it's captured with
|
||||||
|
`-debugcon file:…`, giving CI a log channel independent of `-serial`.
|
||||||
|
|
||||||
|
## The robustness spectrum
|
||||||
|
|
||||||
|
The result handles every combination — framebuffer only, serial only, both, or
|
||||||
|
**neither**. With no channels at all the kernel still boots and runs; port-`0x80`
|
||||||
|
checkpoints track progress and the panic breadcrumb captures failures. *Runs blind
|
||||||
|
but correct* is the goal, not *always has output*.
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [framebuffer.md](framebuffer.md) — the display surface itself (pitch, format), the
|
||||||
|
thing that becomes a graphics device driver.
|
||||||
|
- [efi.md](efi.md) — where the loader captures (or, headless, doesn't capture) the
|
||||||
|
framebuffer before `ExitBootServices`.
|
||||||
|
- [device-interrupts.md](device-interrupts.md) — the serial UART bring-up the log's
|
||||||
|
primary sink rides on.
|
||||||
|
- [resilience.md](resilience.md) — why "never let a missing peripheral take the
|
||||||
|
kernel down" is a core design stance.
|
||||||
Reference in New Issue
Block a user