Built device interrupts
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
# Device interrupts
|
||||
|
||||
CPU exceptions ([interrupts.md](interrupts.md)) are the kernel reacting to its own
|
||||
mistakes. **Device interrupts** are the opposite: hardware asking for attention —
|
||||
a timer firing, a key pressed, a packet arriving. They share the IDT, but differ
|
||||
in one fundamental way: an exception here is terminal (we report and halt), while a
|
||||
device interrupt is *handled and returned from*, so the interrupted code resumes as
|
||||
if nothing happened. This is danos's first code that takes an interrupt and comes
|
||||
back — the same mechanism a scheduler will later use to preempt tasks.
|
||||
|
||||
The first device we bring up is the **timer**, because it's the simplest: it lives
|
||||
entirely on the CPU's local interrupt controller, needing no external routing.
|
||||
It's all x86_64-specific, behind the [arch](arch.md) boundary.
|
||||
|
||||
## The APIC, not the PIC
|
||||
|
||||
Interrupt delivery on modern x86 goes through the **APIC**, not the legacy 8259
|
||||
PIC. There are two halves; we only need one so far:
|
||||
|
||||
- The **Local APIC** (per-CPU, memory-mapped at physical `0xFEE00000`) handles the
|
||||
CPU's own timer and receives interrupts routed to it. `src/arch/x86_64/apic.zig`.
|
||||
- The **IO-APIC** routes *external* device lines (keyboard, etc.) to LAPIC vectors.
|
||||
Not needed for the timer — it'll arrive with the keyboard.
|
||||
|
||||
The old PIC has to be dealt with first, though: left alone it would deliver
|
||||
interrupts on vectors `0x08-0x0F`, which **collide with the CPU exception
|
||||
vectors** — a spurious IRQ would look like a double fault. So `init` remaps the
|
||||
PIC's vectors to `0x20-0x2F` and masks every line, taking it out of the picture.
|
||||
|
||||
Then the LAPIC is enabled in two places: the `IA32_APIC_BASE` MSR's global-enable
|
||||
bit, and the LAPIC's own spurious-vector register (bit 8 = software enable). The
|
||||
spurious vector is `0x2F` — low nibble `F` by convention, and inside our gate
|
||||
range so a stray spurious interrupt lands on a valid no-op.
|
||||
|
||||
## The timer
|
||||
|
||||
The LAPIC timer is three register writes (`initTimer`): a divide setting, then the
|
||||
LVT-timer entry giving it a **vector** (32) and **periodic** mode, then an initial
|
||||
count that becomes the reload value. From then on it fires vector 32 repeatedly, on
|
||||
its own, forever.
|
||||
|
||||
> The count isn't calibrated to real time yet — the tick *rate* is arbitrary
|
||||
> (bus-clock dependent). Turning it into a known frequency (say 100 Hz) needs a
|
||||
> reference clock to measure against (the PIT, HPET, or the TSC). That's a later
|
||||
> step; for now it just needs to tick.
|
||||
|
||||
## Two kinds of vector, one dispatch
|
||||
|
||||
The IDT now installs gates `0-47`: the 32 exceptions plus the device range. Every
|
||||
gate still funnels through the same stub tail (`isr_common`), which calls one
|
||||
dispatcher that branches on the vector (`interruptDispatch` in `idt.zig`):
|
||||
|
||||
```zig
|
||||
if (state.vector < 32) {
|
||||
on_fault(state); // exception: report and halt (never returns)
|
||||
} else if (handlers[state.vector]) |handler| {
|
||||
handler(); // device: run the registered handler
|
||||
apic.eoi(); // ...acknowledge the LAPIC
|
||||
}
|
||||
// else: spurious/unhandled — deliberately no EOI
|
||||
```
|
||||
|
||||
Two things make device interrupts *return* where exceptions don't:
|
||||
|
||||
1. **The handler returns.** The timer handler just bumps a tick counter. Control
|
||||
flows back to `isr_common`, which restores every register it saved and executes
|
||||
`iretq` — resuming the interrupted instruction exactly. (This is why the stub
|
||||
saves *all* the general registers.)
|
||||
2. **End-of-interrupt.** After handling, we write the LAPIC's EOI register. Miss
|
||||
this and the LAPIC thinks we're still busy and never delivers the next
|
||||
interrupt. It's the single most common "my timer fired once and stopped" bug.
|
||||
|
||||
A device handler is a plain `fn () void` — a timer or keyboard handler doesn't need
|
||||
the interrupted registers. (Note: the stubs don't save the SSE/vector registers, so
|
||||
a handler must not use them; ours don't.)
|
||||
|
||||
## Turning them on
|
||||
|
||||
Exceptions can't be masked, which is why they worked all along. Maskable device
|
||||
interrupts don't fire until the CPU's interrupt flag is set — so the final step is
|
||||
`sti` (`arch.enableInterrupts()`), after the APIC and timer are configured. From
|
||||
that instant the kernel has a heartbeat, and its idle `hlt` loop
|
||||
([halting.md](halting.md)) wakes on every tick and dozes off again.
|
||||
|
||||
## Verifying it
|
||||
|
||||
The `timer` test (see [testing.md](testing.md)) is the proof that an interrupt both
|
||||
*fires* and *returns*: it records the tick count, busy-waits, and checks the count
|
||||
advanced on its own.
|
||||
|
||||
```
|
||||
$ python3 test/qemu_test.py timer
|
||||
timer ... PASS (matched 'DANOS-TEST-RESULT: PASS')
|
||||
```
|
||||
|
||||
If the APIC weren't enabled, or `sti` were missing, or EOI were forgotten, the
|
||||
count would stay put and the test would fail. That it advances — while the CPU was
|
||||
spinning in unrelated code — is the whole mechanism working end to end.
|
||||
|
||||
## What's next (not done here)
|
||||
|
||||
- **The keyboard**: bring up the IO-APIC, route its IRQ to a vector, and read
|
||||
scancodes from the PS/2 controller — the first *input* device.
|
||||
- **A calibrated timer** at a known frequency, and a monotonic clock.
|
||||
- **Uncacheable MMIO**: the LAPIC page is currently mapped writeback-cacheable like
|
||||
the rest of the identity map. QEMU tolerates it, but real hardware wants MMIO
|
||||
marked uncacheable (via the page's cache bits or an MTRR).
|
||||
- **Preemption**: once there are tasks, the timer handler is where the scheduler
|
||||
decides to switch — the reason a *returning* interrupt matters.
|
||||
Reference in New Issue
Block a user