//! 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 -- 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 (a capability passed in a message /// hands 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; } } }