2026-06-15
Legacy PCI interrupts used four physical wires (INTA-INTD) shared across every device on the bus. If your NIC and your SATA controller shared INTA, every packet interrupt woke the disk driver too, which had to poll its registers to discover it had nothing to do. MSI-X (Message Signaled Interrupts, eXtended) replaces wires with memory writes: the device sends an interrupt by performing a DMA write to a magic address, and the chipset turns that write into an interrupt vector on a specific CPU.
Each MSI-X-capable device exposes a table in one of its BARs. Each entry is 16 bytes: a 64-bit message address, a 32-bit message data, and a 32-bit vector control (the low bit is a mask). When the device wants to fire vector N, it writes data[N] to address[N]. On x86, the address encodes the destination CPU's Local APIC ID and delivery mode; the data encodes the vector number (0-255) the CPU should dispatch to via the IDT.
The win: each entry can target a different CPU and a different vector. A modern NIC with 64 receive queues programs 64 MSI-X entries, one per queue, each pointing at a different core's APIC. Packets hashed to queue 7 always interrupt core 7, which runs the softirq, which finds the descriptor warm in L1. No spinlock, no cache-line bounce, no shared state.
The spec allows up to 2048 entries per function. A 16-port NVMe controller can give each submission queue its own vector, and the kernel pins each vector's affinity to the CPU that submitted to that queue. This is how nvme achieves millions of IOPS without contention.
Concrete example. Inspect a device on Linux:
$ cat /proc/interrupts | grep nvme0 124: 482011 0 0 0 ... PCI-MSI 524288-edge nvme0q0 125: 0 391284 0 0 ... PCI-MSI 524289-edge nvme0q1 126: 0 0 402117 0 ... PCI-MSI 524290-edge nvme0q2
Each queue's interrupts land on exactly one CPU — the zeros across rows are the affinity working. If you see counts spread across every CPU column, IRQ affinity is broken and you're paying cache-coherence costs on every packet.
Rule of thumb. One MSI-X vector per CPU per high-rate device. For a 32-core box with two 100GbE NICs, expect 64 vectors. If your device only exposes a single MSI-X entry, that one core becomes a bottleneck around ~1.5M interrupts/sec — every queue funnels through it.
The magic address on Intel is 0xFEE00000 | (apic_id << 12). That fixed range is why the MTRRs and PAT mark it uncacheable write-combining — a cached interrupt would be a disaster.
