The MSI-X Table: How PCIe Devices Deliver Thousands of Interrupts Without a Wire

2026-08-18

Legacy PCI interrupts were physical wires (INTA#, INTB#, INTC#, INTD#) shared across multiple devices. When one asserted, the OS had to poll every device on the line to find the culprit. Then came MSI (Message Signaled Interrupts): instead of a wire, the device performs a memory write to a magic address, and the CPU's interrupt controller intercepts that write and delivers it as an interrupt. No wires, no sharing, no polling.

MSI-X is the grown-up version. Where MSI gave a device up to 32 interrupts (all sharing one address/data pair with a low-bit variation), MSI-X gives a device up to 2048 independent interrupts, each with its own address and data. That address is typically a Local APIC's MSI region (on x86, 0xFEE00000-based), and the data encodes the vector number and delivery mode.

The MSI-X Table lives in device BAR-mapped memory. Each entry is 16 bytes:

When a device wants to signal interrupt #5, it reads entry 5 from its own table, then issues a PCIe memory write TLP to that address with that data. The Root Complex routes it, the IOMMU may remap it, and the LAPIC injects it into the target core.

Concrete example: A modern NVMe SSD exposes one MSI-X vector per queue pair. An enterprise drive with 64 completion queues configures 64 MSI-X entries, each targeting a different CPU core. When core 7 submits an I/O and it completes, the drive writes to the address in entry 7, which fires an interrupt only on core 7. No cross-core cache traffic, no shootdowns, no lock contention on a shared IRQ handler. This is why NVMe scales linearly with cores while AHCI (single legacy IRQ) collapses past 4 threads.

Rule of thumb: Interrupt affinity only works if you have enough MSI-X vectors. N cores need at least N vectors for a device to scale. Check /proc/interrupts — if a high-throughput device shows one IRQ line with all counts on CPU0, it's stuck in MSI (or legacy) mode and you're leaving throughput on the table.

The catch: the MSI-X table lives in device memory, so updating affinity requires an MMIO write, which serializes with pending interrupts. Rewriting affinity mid-flight can drop the interrupt currently being posted, which is why kernels mask the entry, wait, rewrite, then unmask.

Key Takeaway: MSI-X replaces interrupt wires with per-vector memory writes, letting a single device deliver thousands of independently-targeted interrupts and scale across cores the way legacy IRQs never could.

All newsletters