137 lines
5.9 KiB
Markdown
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, `system/kernel/architecture/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 `system/kernel/kernel.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 `boot/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.
|