re-org docs
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user