133 lines
6.8 KiB
Markdown
133 lines
6.8 KiB
Markdown
# 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/kernel/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 reload count isn't picked arbitrarily — it's **calibrated to real time**,
|
|
which the [real-time](vision.md) scheduling guarantees depend on. Since the LAPIC
|
|
timer's raw rate is bus-clock dependent and unknown up front, `calibrate` measures
|
|
it against the **PIT** (the legacy 8254, whose 1.193182 MHz is fixed): run the
|
|
LAPIC timer one-shot from its maximum count while the PIT counts out a known 10 ms
|
|
(polling channel 2, no interrupt needed), then see how far the LAPIC got. That
|
|
yields its counts-per-millisecond, from which `initTimer(hz)` computes the reload
|
|
count for any target frequency. danos runs it at **1000 Hz** (a 1 ms tick).
|
|
|
|
## The high-resolution clock (TSC)
|
|
|
|
The timer tick gives *scheduling* — a 1 ms quantum — but 1 ms is coarse for a
|
|
real-time system to *measure* with (interrupt latency, jitter, timeouts). So the
|
|
same calibration also measures the **TSC** (Time Stamp Counter): a per-core cycle
|
|
counter read with `rdtsc` in a couple of cycles, giving roughly **nanosecond**
|
|
resolution — a million times finer than the tick. We snapshot the TSC across the
|
|
same 10 ms PIT window to get its frequency (measured ~3.6 GHz on the test host).
|
|
|
|
The monotonic clock is exposed as one function per resolution — `nanos()`,
|
|
`micros()`, `millis()` — each scaling the cycle delta directly at its unit (with a
|
|
128-bit intermediate so a long uptime doesn't overflow) rather than chaining
|
|
divisions. `millis()` is what the scheduler uses for `sleep` deadlines; `nanos()`
|
|
is there for fine measurement. Note the two clocks are distinct: the **tick** drives
|
|
preemption and wakeups (1 ms granularity); the **TSC** is the resolution you read
|
|
time at. Making `sleep` itself sub-millisecond would take a tickless one-shot
|
|
timer — a later step.
|
|
|
|
## 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.
|
|
- **`sleep()` / timeouts** built on the calibrated clock (the monotonic
|
|
`uptimeMs()` is in place).
|
|
- **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.
|