danos/docs/halting.md

137 lines
5.9 KiB
Markdown

# Halting
## Why a kernel needs to halt
An ordinary program ends by *returning*`main` finishes, the C runtime calls
`exit`, and the OS reclaims the process. A kernel has none of that. There is no
OS underneath it, no runtime to return to, and no caller waiting. `_start` is the
end of the line. So when the kernel has nothing left to do — whether it finished
its work or hit a fatal error — it can't "quit". It has to explicitly park the
CPU forever, because if execution ever ran off the end it would just keep fetching
whatever bytes follow in memory and execute garbage.
That's what halting is: deliberately stopping the processor so it does nothing,
safely, until the machine is reset or powered off.
## The core of it: `hlt`
Everything comes down to one x86 instruction. It's CPU-specific, so it lives in
the arch module, `src/arch/x86_64/cpu.zig` (see [arch.md](arch.md)), and the
generic kernel calls it as `arch.halt()`:
```zig
/// Park the core forever. `hlt` drops it into a low-power idle until the next
/// interrupt; the loop re-halts on every wake so the stop is permanent.
pub fn halt() noreturn {
while (true) asm volatile ("hlt");
}
```
**`hlt`** ("halt") tells the CPU core to stop executing instructions and drop into
a low-power idle state. It's not a busy-wait — the core genuinely stops, drawing
almost no power and generating almost no heat, until something wakes it.
This is much better than the naïve alternative, a spin loop:
```zig
while (true) {} // "busy-wait" — DON'T do this to idle
```
A bare `while (true) {}` keeps the core running flat out, executing the jump
back to the top of the loop billions of times a second — 100% CPU, hot, and (on a
laptop) draining the battery, all to accomplish nothing. `hlt` achieves the same
"do nothing" outcome while letting the core sleep.
## Why the loop around it?
Here's the subtlety the comment points at: **`hlt` is not permanent.** It halts
the core only until the *next interrupt* arrives. An interrupt is a signal — from
a timer, a keypress, a device — that wakes the CPU so it can respond. When one
fires, the core comes out of `hlt` and executes the next instruction.
If we wrote just a single `hlt`, the very first stray interrupt would wake the
core and execution would continue past it — falling off the end of the function
into whatever comes next in memory. Wrapping it in `while (true)` closes that
door: every time an interrupt wakes the core, the loop immediately runs `hlt`
again and it goes back to sleep. The net effect is a permanent halt that still
sleeps between the interrupts it can't prevent.
(At this stage danos hasn't set up an interrupt descriptor table, so most
interrupts aren't even something we handle — but non-maskable interrupts and
system-management interrupts can still wake a halted core regardless. The loop
makes us robust to all of them.)
## The `asm volatile` part
`hlt` has no equivalent in plain Zig, so we drop to inline assembly:
- **`asm`** emits the raw instruction directly into the function.
- **`volatile`** tells the compiler *"this has side effects you can't see — do
not optimize it away or reorder it."* Without it, an optimizing compiler might
reason that the assembly produces no value anyone uses and delete it, or hoist
it somewhere wrong. `volatile` pins it exactly where we wrote it.
## `noreturn`: telling the compiler it's the end
`halt()` is typed `noreturn` — a real Zig type meaning "this function never gives
control back to its caller." That isn't decoration; it changes how the compiler
treats the call:
- Code *after* a `noreturn` call is unreachable, so the compiler needn't emit a
return sequence, and won't warn about "missing return value" in the callers.
- It lets `kmain` and `_start` themselves be `noreturn`, which is the honest
signature for a kernel entry point — the bootloader jumps in and nothing ever
jumps back out.
You can see the chain in `src/main.zig`: `_start` is `noreturn`, it calls
`kmain` which is `noreturn`, which ends by calling `arch.halt()` which is
`noreturn`. The "never returns" property is threaded all the way down.
## Where danos halts
There are three halt sites, and they're all the same idea:
1. **Normal end of kernel work**`kmain` prints its status, then calls
`arch.halt()`:
```zig
con.write("\nkernel initialised; nothing left to do, halting.\n");
arch.halt();
```
There's genuinely nothing more to do yet, so the kernel parks itself.
2. **Kernel panic** — the freestanding panic handler has no OS to report to, so
it prints the message in red (if the console is up) and halts via the same
`arch.halt()`. A panic is unrecoverable here, so stopping the machine — rather
than limping on with corrupted state — is the safe response.
3. **Bootloader failure** — in `src/efi.zig`, if `boot()` fails *before* handing
off to the kernel, `main` logs the error and parks the machine with the same
loop so the message stays on screen:
```zig
boot() catch |err| {
log("\r\ndanos: boot failed: ");
logBytes(@errorName(err));
log("\r\n");
while (true) asm volatile ("hlt");
};
```
(Here it's an inline loop rather than `arch.halt()` because that lives in the
kernel's arch module, and the loader is a separate binary from the kernel.)
## Summary
- A kernel can't "exit" — it must explicitly stop the CPU or it runs off into
garbage.
- **`hlt`** parks the core in a low-power idle until the next interrupt — far
better than a 100%-CPU spin loop.
- **`while (true) hlt`** makes that halt permanent, since any interrupt would
otherwise wake the core and let execution continue.
- **`asm volatile`** emits the instruction and forbids the compiler from removing
it; **`noreturn`** encodes "control never comes back" into the type system.
- danos halts on normal completion, on a kernel panic, and on a bootloader error
— the same "stop safely and stay stopped" in all three.