145 lines
6.5 KiB
Markdown
145 lines
6.5 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 [architecture.md](architecture.md)), and the
|
|
generic kernel calls it as `architecture.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.
|
|
|
|
(danos handles plenty of interrupts through its interrupt descriptor table —
|
|
the timer tick waking a halted idle core is exactly how scheduling works — and
|
|
non-maskable and system-management interrupts can wake a halted core regardless
|
|
of what we handle. The loop makes the halt 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 the exported entry shim `kmainEntry` 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 the code: `_start` (an assembly stub in
|
|
`system/kernel/architecture/x86_64/isr.s`) installs a kernel-owned stack and calls
|
|
`kmainEntry` in `system/kernel/kernel.zig`, which is `noreturn`; it calls `kmain`,
|
|
also `noreturn`, which ends by calling `architecture.halt()`, again `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. **The BSP's idle loop** — `kmain` no longer runs out of work: it spawns
|
|
`/system/services/init` as PID 1, drops itself to priority 0, and ends as the
|
|
bootstrap core's idle task — still by calling `architecture.halt()`:
|
|
|
|
```zig
|
|
scheduler.setPriority(0);
|
|
status("\n/system/kernel: kernel idle; user space is running.\n");
|
|
architecture.halt();
|
|
```
|
|
|
|
The timer keeps preempting the idle context into init and whatever else is
|
|
ready; between those interrupts, the halt loop is exactly the low-power park
|
|
described above.
|
|
|
|
2. **Kernel panic** — the freestanding panic handler has no OS to report to, so
|
|
it prints the message to the diagnostic log and the on-screen console —
|
|
forcing the console back on even if a display service had it suppressed — and
|
|
halts via the same `architecture.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\nEFI: boot failed: ");
|
|
logBytes(@errorName(err));
|
|
log("\r\n");
|
|
while (true) asm volatile ("hlt");
|
|
};
|
|
```
|
|
|
|
(Here it's an inline loop rather than `architecture.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 in the kernel's idle loop, on a kernel panic, and on a bootloader
|
|
error — the same "park the core safely" in all three.
|