docs/plan/hybrid.md mechanism 3. A process asks for a device's interrupts with
PCIIOC_IRQ and then reads them, which is how Linux's uio does it and for the
same reason: a descriptor composes with poll, and a driver that stops reading
falls behind rather than losing events. read() returns a count rather than a
flag, so a driver that was busy sees it missed three and acts once.
Proven against an e1000e by asserting a cause through its ICS register and
reading the interrupt back out in userspace:
interrupts routed here (line 10)
interrupt delivered, count = 1
ICR = 0x80000004
0x80000004 is the cause the test set plus INT_ASSERTED -- the device's own
register, read by userspace as its acknowledgement.
**The masking is the design, not a detail.** A PCI line is level-triggered and
stays asserted until the cause is cleared at the device, which is a different
register on every chip and so is something only the driver can do. The kernel
masks the line on arrival and userspace's write() unmasks it. Signalling EOI on
a still-asserted line re-enters the handler immediately and for ever; this
kernel did exactly that by accident at 72,000 interrupts a second, and what it
looked like from outside was a clock running seven times slow (§13).
Which decides what happens when a driver dies mid-interrupt: the line is left
**masked**. The instinct is to unmask -- do not leave the device dead -- and it
is wrong, because unmasking an asserted line with no handler is a crashed
driver taking the machine down with it. Masked is safe and not permanent: the
next PCIIOC_IRQ arms it. Both halves verified by crashing a driver on purpose
and then starting another one.
A line already carrying kernel handlers is refused outright. Masking is
per-line, so handing out a shared one would silence whichever kernel driver is
also there, with nothing logged. MSI is the real answer and pci_msi_enable() is
already in the tree waiting for a caller.
Also fixed, found by running the crash twice: DMA buffers were never returned
when a process exited. The arena is 4 MB and contiguous, so a driver that
restarts a few times exhausted it -- "out of DMA memory, 3584 free" -- which is
a poor property for an interface whose whole promise is that a driver can crash
and come back.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
44 lines
2 KiB
C
44 lines
2 KiB
C
#pragma once
|
|
|
|
// Hardware interrupt registration.
|
|
//
|
|
// Until this existed, trap() was a switch statement listing every interrupt
|
|
// the kernel knew about, and adding a device meant editing the trap handler.
|
|
// That is fine while the devices are the four the PC has always had at fixed
|
|
// pins; it stops being fine the moment a device's IRQ is whatever the BIOS
|
|
// decided to put in its PCI config space this boot. A driver cannot write
|
|
// `case T_IRQ0 + IRQ_XHCI:` because there is no such constant -- it only
|
|
// learns its line at probe time.
|
|
//
|
|
// So: a line has a chain of handlers, a driver adds itself to one at probe
|
|
// time, and enabling the line is a consequence of registering rather than a
|
|
// separate call somebody can forget.
|
|
//
|
|
// Chains matter more than they look. PCI interrupt pins are *shared*: four
|
|
// INTx lines are routed to a handful of IOAPIC inputs, so two unrelated
|
|
// devices routinely land on the same one. A handler must therefore check
|
|
// whether its own device actually raised the interrupt and do nothing if it
|
|
// did not.
|
|
|
|
#define NIRQ 24 // IOAPIC redirection entries on a PC
|
|
#define NCHAIN 4 // handlers that may share one line
|
|
|
|
typedef void (*irq_handler_t)(void *arg);
|
|
|
|
// Returns 0, or -1 if the line is out of range, reserved, or full.
|
|
// Registering enables the line; the last unregister masks it again.
|
|
//
|
|
// `level_low` selects PCI's signalling (level-triggered, active low) rather
|
|
// than ISA's (edge-triggered, active high). Getting this wrong does not fail
|
|
// cleanly: an edge-configured level interrupt fires once and then wedges,
|
|
// because the device keeps the line asserted and no further edge ever comes.
|
|
int irq_register(int irq, irq_handler_t fn, void *arg, const char *name,
|
|
int level_low);
|
|
void irq_unregister(int irq, irq_handler_t fn, void *arg);
|
|
int irq_handler_count(int irq);
|
|
|
|
// Called from trap() with the IOAPIC pin number, not the vector.
|
|
void irq_dispatch(int irq);
|
|
|
|
void irq_init(void);
|
|
int irq_info(char *buf, int cap); // /proc/interrupts
|