danos/system/kernel/irq.zig

228 lines
11 KiB
Zig

//! IRQ-as-IPC: delivering a hardware interrupt to a user-space driver.
//!
//! A microkernel can't run driver code in the ISR — the driver is a ring-3 process
//! in another address space. So the kernel's ISR does the least it can: quiet the
//! line, acknowledge the CPU, and post an asynchronous notification to the endpoint
//! the driver is blocked on (`ipc_sync.notifyFromIsr`). The driver wakes out of
//! `IPC_ReplyWait`, services the device, and calls `irq_ack` to re-arm.
//!
//! The full cycle, and why each step is where it is:
//!
//! ISR irqMask(gsi) -- the line is still asserted; stop it reaching a CPU
//! irqEoi() -- now safe to tell the LAPIC we're done
//! notifyFromIsr() -- wake the driver (it runs much later)
//! driver <services device> -- reads/clears the device's status register
//! driver irq_ack(device,resource) -- irqUnmask(gsi): the line is quiet, let it through
//!
//! Mask-before-EOI is the load-bearing part. A level-triggered line stays asserted
//! until the *device* is quieted, which only the ring-3 driver can do. EOI with the
//! entry unmasked and the I/O APIC redelivers immediately, forever, before the
//! driver is ever scheduled. Masking converts "level" into something a deferred
//! handler can cope with; `irq_ack` is what closes the loop.
//!
//! Binding is capability-gated exactly like `mmio_map`: the caller must have
//! `device_claim`ed the device, and the GSI must come from one of that device's `irq`
//! resources in the discovered device table (system/kernel/devices-broker.zig). A driver can
//! therefore never bind an interrupt it doesn't own — a raw-GSI system_call would let
//! any process steal the keyboard's line.
//!
//! KNOWN ISSUE (real hardware, not QEMU). Masking a level-triggered redirection entry
//! while its remote-IRR bit is set does not clear remote-IRR on some chipsets, and the
//! line then never fires again — a driver would take exactly one interrupt and block
//! forever. QEMU's I/O APIC clears remote-IRR on EOI regardless of the mask, so the
//! `hpet` test cannot see this. Linux's workaround is to flush remote-IRR by briefly
//! flipping the entry to edge-trigger and back. Revisit when danos first boots on
//! metal; MSI (no mask cycle at all) sidesteps it entirely.
const architecture = @import("architecture");
const sync = @import("sync.zig");
const ipc_sync = @import("ipc-synchronous.zig");
/// GSIs a single I/O APIC covers. 24 is the standard redirection-table size; a
/// second I/O APIC (none on QEMU's q35) would extend this.
pub const maximum_gsi = 24;
/// The endpoint to notify for each bound GSI, or null if unbound. Read from the ISR
/// and written from syscalls, always under the big kernel lock.
var bound: [maximum_gsi]?*ipc_sync.Endpoint = .{null} ** maximum_gsi;
/// Task that owns each binding. Teardown is keyed on *this*, not on the endpoint
/// pointer: an endpoint can be shared between processes (ipc_register/ipc_lookup hand
/// out extra references), so "every GSI pointing at this endpoint" is not the same
/// set as "every GSI this process bound", and releasing the former on exit would mask
/// a live sibling's device line.
var bound_owner: [maximum_gsi]u32 = .{0} ** maximum_gsi;
/// No GSI assigned to this vector.
const no_gsi: u32 = 0xFFFF_FFFF;
/// Reverse map for the ISR: which GSI does this vector carry? Populated at bind.
/// `interruptDispatch` hands a handler no arguments, so the vector→GSI edge has to
/// be recovered from somewhere — the trampolines below capture the vector at
/// comptime, and this turns it back into a GSI. Trampolines are installed on every
/// vector in the window at boot, so an unbound one must be distinguishable from
/// GSI 0 — hence the sentinel rather than a zero default.
var vector_gsi: [256]u32 = .{no_gsi} ** 256;
/// Vector currently assigned to each GSI (0 = none), so a rebind reuses it.
var gsi_vector: [maximum_gsi]u8 = .{0} ** maximum_gsi;
/// MSI bindings, keyed by **vector** (not GSI): an MSI has no I/O APIC redirection
/// entry, it is a per-device edge-triggered vector the device raises by writing the
/// (address, data) pair `msi_bind` hands back. Read from the ISR and written from
/// syscalls, always under the big kernel lock, like `bound`.
var msi_bound: [256]?*ipc_sync.Endpoint = .{null} ** 256;
var msi_owner: [256]u32 = .{0} ** 256;
/// Set once the trampolines are installed.
var installed = false;
/// The ISR body for a bound device line. Runs with interrupts off, on the
/// interrupted task's kernel stack, on whichever core the I/O APIC picked.
fn dispatch(vector: u8) void {
const gsi = vector_gsi[vector];
if (gsi == no_gsi) {
// No GSI is routed here. It may be an MSI vector (edge-triggered, unshared, no
// I/O APIC entry): just EOI and notify — there is no line to mask and no ack
// cycle. Or it's spurious, and the EOI alone keeps the LAPIC from wedging on an
// in-service bit. Same lock discipline as below: msi_bound[vector] is read and
// used under the acquisition `msiRelease` writes it under.
_ = sync.enter();
defer sync.leaveIsr();
architecture.irqEoi();
if (msi_bound[vector]) |endpoint| ipc_sync.notifyLocked(endpoint, vector);
return;
}
// One lock region for the whole cycle. Two reasons, and the second is subtle:
//
// - The I/O APIC is an index/data register pair, so two cores interleaving a
// read-modify-write of a redirection entry would corrupt it.
// - `bound[gsi]` must be *read and used* under the same acquisition that
// `unbind` writes it under. Dropping the lock between the load and
// `notifyLocked` would let a driver exiting on another core free the endpoint
// in the gap, and we would post a notification into freed memory. The GSI is
// routed to the core that bound it, but a driver may migrate and exit
// elsewhere, so this is reachable on SMP.
//
// No deadlock: the lock is non-recursive, but a core holding it runs with
// interrupts disabled and so cannot interrupt itself into here.
_ = sync.enter();
defer sync.leaveIsr();
architecture.irqMask(gsi); // the line is still asserted; stop it reaching a CPU
architecture.irqEoi(); // now safe to release the LAPIC's in-service bit
// Wakes the driver if it's blocked in ReplyWait; otherwise queues the badge on
// the endpoint's notify ring, so an interrupt taken while the driver is off
// doing something else is not lost.
if (bound[gsi]) |endpoint| ipc_sync.notifyLocked(endpoint, gsi);
}
/// Install one no-argument trampoline per usable vector. Each closes over its own
/// `vector` as a comptime constant — that's the trick that gets an argument into
/// `idt.Handler` (`*const fn () void`) without a per-vector hand-written stub.
pub fn init() void {
if (installed) return;
inline for (0..architecture.irq_vector_count) |i| {
const vector: u8 = @intCast(@as(usize, architecture.irq_vector_base) + i);
architecture.irqSetHandler(vector, &struct {
fn trampoline() void {
dispatch(vector);
}
}.trampoline);
}
installed = true;
}
/// Lowest unused vector in the device window, or null if they're all spoken for.
fn allocVector() ?u8 {
var v: u8 = architecture.irq_vector_base;
while (v < architecture.irq_vector_base + architecture.irq_vector_count) : (v += 1) {
var used = msi_bound[v] != null; // taken by an MSI binding
for (gsi_vector) |gv| {
if (gv == v) used = true; // taken by a GSI binding
}
if (!used) return v;
}
return null;
}
pub const BindError = error{ BadGsi, InUse, NoVector };
/// Deliver `gsi` to `endpoint` as an IPC notification, on behalf of task `owner`. Routes the
/// line to this core, installs the binding, and unmasks. Caller must hold the big
/// kernel lock, and must already have checked that `owner` claimed the device this GSI
/// belongs to.
pub fn bind(gsi: u32, endpoint: *ipc_sync.Endpoint, owner: u32) BindError!void {
if (gsi >= maximum_gsi or !architecture.irqOwnsGsi(gsi)) return error.BadGsi;
if (bound[gsi] != null) return error.InUse;
const vector = allocVector() orelse return error.NoVector;
vector_gsi[vector] = gsi;
gsi_vector[gsi] = vector;
bound[gsi] = endpoint;
bound_owner[gsi] = owner;
// Level-triggered, active-high. Level is the general case a driver must survive
// (and what hpet configures its comparator for); an edge source simply never
// leaves the line asserted, so the mask/ack cycle is harmless there.
//
// Hardcoded for now: a device whose MADT interrupt-source override declares the
// line active-*low* (most legacy PCI INTx) will need the polarity threaded
// through from discovery. Nothing danos binds today is such a device.
architecture.irqRoute(gsi, vector, true, false);
architecture.irqUnmask(gsi);
}
pub const MsiError = error{NoVector};
/// Allocate an interrupt vector and bind it to `endpoint` for task `owner`, returning
/// the vector. The `msi_bind` mechanism: MSI is edge-triggered and unshared, so there
/// is no GSI, no I/O APIC redirection entry, no mask, and no ack cycle — the device
/// raises the interrupt by *writing* the (address, data) pair the syscall derives from
/// this vector, and `dispatch` just EOIs and notifies. Caller holds the big kernel
/// lock and has verified `owner` claimed the device. See docs/driver-model.md (M15).
pub fn msiBind(endpoint: *ipc_sync.Endpoint, owner: u32) MsiError!u8 {
const vector = allocVector() orelse return error.NoVector;
msi_bound[vector] = endpoint;
msi_owner[vector] = owner;
return vector;
}
/// Re-arm `gsi` after the driver has quieted the device. Caller holds the big lock
/// and has verified ownership.
pub fn ack(gsi: u32) bool {
if (gsi >= maximum_gsi or bound[gsi] == null) return false;
architecture.irqUnmask(gsi);
return true;
}
/// Drop every binding made by task `owner` — called as that task exits, *before* its
/// endpoints are freed. Each line is left masked, so a dead driver's device goes quiet
/// rather than interrupting into a freed endpoint. Caller holds the big kernel lock,
/// which is what makes this safe against a concurrent `dispatch` on another core.
///
/// Keyed on the owner, not the endpoint: endpoints are shared (a registered service's
/// endpoint has references in several processes), so releasing "everything pointing at
/// this endpoint" would tear down bindings this task never made.
pub fn releaseOwner(owner: u32) void {
for (&bound, 0..) |*slot, gsi| {
if (slot.* != null and bound_owner[gsi] == owner) {
architecture.irqMask(@intCast(gsi));
slot.* = null;
bound_owner[gsi] = 0;
gsi_vector[gsi] = 0;
}
}
// MSI bindings carry no I/O APIC entry to mask — just drop them so a stale vector
// stops notifying a freed endpoint. A late edge on a released vector is spurious
// (msi_bound is null) and `dispatch` EOIs it harmlessly.
for (&msi_bound, 0..) |*slot, vector| {
if (slot.* != null and msi_owner[vector] == owner) {
slot.* = null;
msi_owner[vector] = 0;
}
}
}