Linux PCI Interrupt Handling Guide-Free Linux Device Drivers Training Online

PREV_LEC  |  NEXT_LEC

Linux PCI Interrupt Handling Guide
INTx, MSI, and MSI-X explained, with a full PCI driver that requests interrupt vectors on a modern kernel
Legacy INTx
MSI
MSI-X

A PCI device that can only be read and written from software is only half a driver — sooner or later it needs to tell the CPU “something happened” without being polled. That’s pci interrupt handling linux in one sentence, and the PCI/PCIe specifications define three different ways to do it: legacy INTx, MSI, and MSI-X. This lecture, continuing EmbeddedPathashala’s free linux kernel development course, builds directly on the BAR-mapped driver from the previous lecture and extends it with real interrupt handling using the modern `pci_alloc_irq_vectors()` API.

What You Will Learn

Legacy INTx interrupts and their limitations Message Signaled Interrupts (MSI) Extended MSI (MSI-X) and per-vector routing pci_alloc_irq_vectors() on modern kernels Writing an interrupt handler for a PCI device Building a complete probe/remove PCI driver

Prerequisites

Completed the previous lecture on PCI address spaces and BARs Familiarity with request_irq() and IRQ handlers Comfortable compiling out-of-tree kernel modules A Linux host or VM with a PCI/PCIe bus (QEMU works fine)

PCI Interrupt Distribution: INTx, MSI, and MSI-X

A PCI Express endpoint can signal the CPU using one of three interrupt mechanisms, and picking the right one matters for both correctness and performance in a pci interrupt handling linux driver:

MechanismHow it SignalsVectors per DeviceTypical Use
Legacy INTxDedicated wire, often shared across devices1 (shared)Old hardware, simple devices
MSIIn-band memory write (“message”) instead of a wireUp to 32General-purpose modern devices
MSI-XMessage-based, each vector has its own address/data pair in a tableUp to 2048Multi-queue NICs, NVMe, high-throughput devices

Legacy INTx interrupts are shared across every device wired to the same physical line, which means the kernel’s IRQ handler has to check “was this interrupt actually for me?” on every invocation — expensive when several devices share a line. MSI and MSI-X remove the shared line entirely: the device signals an interrupt by performing an ordinary memory write to a special address the CPU treats as an interrupt request. Because each vector is its own message, MSI-X in particular lets a device route different event types (RX queue, TX queue, error, link-status) to different CPU cores without any of them contending for a shared line.

From Shared Wire to Per-Vector Messages
INTx
1 shared physical line
MSI
Up to 32 message vectors
MSI-X
Up to 2048 independently addressed vectors

The Modern API: pci_alloc_irq_vectors()

Older kernels forced drivers to handle each interrupt mechanism as a separate code path — call `pci_enable_msi()`, fall back to `request_irq()` with the legacy IRQ number if it failed, and so on. Modern kernels hide all three mechanisms behind one API, so your driver code stays the same whether the hardware ultimately gets MSI-X, falls back to MSI, or falls back further to legacy INTx:

#include <linux/pci.h>
#include <linux/interrupt.h>

#define EP_PCI_MAX_VECTORS 4

static irqreturn_t ep_pci_irq_handler(int irq, void *data)
{
    struct ep_pci_dev *epdev = data;

    /* Clear/acknowledge the device's interrupt status register */
    iowrite32(0x1, epdev->regs + 0x04);

    dev_info(&epdev->pdev->dev, "ep_pci: interrupt on IRQ %d\n", irq);
    return IRQ_HANDLED;
}

static int ep_pci_setup_irqs(struct pci_dev *pdev, struct ep_pci_dev *epdev)
{
    int nvec, i, ret;

    /* Ask for up to 4 vectors, preferring MSI-X, then MSI, then INTx */
    nvec = pci_alloc_irq_vectors(pdev, 1, EP_PCI_MAX_VECTORS,
                                  PCI_IRQ_MSIX | PCI_IRQ_MSI | PCI_IRQ_LEGACY);
    if (nvec < 0)
        return nvec;

    for (i = 0; i < nvec; i++) {
        ret = devm_request_irq(&pdev->dev, pci_irq_vector(pdev, i),
                                ep_pci_irq_handler, 0,
                                "ep_pci_demo", epdev);
        if (ret)
            goto err_free_irqs;
    }

    dev_info(&pdev->dev, "ep_pci: allocated %d interrupt vector(s)\n", nvec);
    return 0;

err_free_irqs:
    pci_free_irq_vectors(pdev);
    return ret;
}

Notice there is no branching on “is this MSI or MSI-X.” `pci_alloc_irq_vectors()` negotiates the best available mechanism with the PCI core and hands your driver plain Linux IRQ numbers either way — `pci_irq_vector(pdev, i)` translates vector index `i` into the correct IRQ number regardless of which mechanism was actually granted underneath.

Putting It Together: A Complete ep_pci_demo Driver

Combining BAR mapping from the previous lecture with the interrupt setup above gives a complete, original driver:

#include <linux/module.h>
#include <linux/pci.h>
#include <linux/interrupt.h>
#include <linux/io.h>

struct ep_pci_dev {
    struct pci_dev *pdev;
    void __iomem *regs;
};

static irqreturn_t ep_pci_irq_handler(int irq, void *data)
{
    struct ep_pci_dev *epdev = data;
    iowrite32(0x1, epdev->regs + 0x04);
    dev_info(&epdev->pdev->dev, "ep_pci: irq %d fired\n", irq);
    return IRQ_HANDLED;
}

static int ep_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
    struct ep_pci_dev *epdev;
    int ret, nvec;

    epdev = devm_kzalloc(&pdev->dev, sizeof(*epdev), GFP_KERNEL);
    if (!epdev)
        return -ENOMEM;

    epdev->pdev = pdev;
    pci_set_drvdata(pdev, epdev);

    ret = pci_enable_device(pdev);
    if (ret)
        return ret;

    ret = pci_request_regions(pdev, "ep_pci_demo");
    if (ret)
        goto err_disable;

    epdev->regs = pci_iomap(pdev, 0, pci_resource_len(pdev, 0));
    if (!epdev->regs) {
        ret = -ENOMEM;
        goto err_release;
    }

    pci_set_master(pdev);

    nvec = pci_alloc_irq_vectors(pdev, 1, 1,
                                  PCI_IRQ_MSIX | PCI_IRQ_MSI | PCI_IRQ_LEGACY);
    if (nvec < 0) {
        ret = nvec;
        goto err_unmap;
    }

    ret = devm_request_irq(&pdev->dev, pci_irq_vector(pdev, 0),
                            ep_pci_irq_handler, 0, "ep_pci_demo", epdev);
    if (ret)
        goto err_free_irq;

    dev_info(&pdev->dev, "ep_pci_demo: probed successfully\n");
    return 0;

err_free_irq:
    pci_free_irq_vectors(pdev);
err_unmap:
    pci_iounmap(pdev, epdev->regs);
err_release:
    pci_release_regions(pdev);
err_disable:
    pci_disable_device(pdev);
    return ret;
}

static void ep_pci_remove(struct pci_dev *pdev)
{
    struct ep_pci_dev *epdev = pci_get_drvdata(pdev);

    pci_free_irq_vectors(pdev);
    pci_iounmap(pdev, epdev->regs);
    pci_release_regions(pdev);
    pci_disable_device(pdev);
}

static const struct pci_device_id ep_pci_ids[] = {
    { PCI_DEVICE(0x1af4, 0x1000) }, /* example: virtio-net under QEMU */
    { 0, }
};
MODULE_DEVICE_TABLE(pci, ep_pci_ids);

static struct pci_driver ep_pci_driver = {
    .name     = "ep_pci_demo",
    .id_table = ep_pci_ids,
    .probe    = ep_pci_probe,
    .remove   = ep_pci_remove,
};

module_pci_driver(ep_pci_driver);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("EmbeddedPathashala PCI interrupt demo driver");

Build and Test Walkthrough

Build it like any other out-of-tree module and load it against a matching device — a QEMU guest with a `virtio-net-pci` device is the easiest way to test this without physical hardware:

make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
sudo insmod ep_pci_demo.ko
dmesg | tail -n 5

Expected output after loading against a matching device:

[  102.441823] ep_pci_demo: probed successfully
[  102.441905] ep_pci: allocated 1 interrupt vector(s)

Confirm the kernel bound your driver instead of a stock one, and verify which interrupt mechanism was actually granted:

lspci -k -s 00:03.0
cat /proc/interrupts | grep ep_pci_demo

sudo rmmod ep_pci_demo

Common Mistakes and Troubleshooting

  • Not checking the vector count returned by `pci_alloc_irq_vectors()` — it can legitimately return fewer vectors than requested; the driver must adapt rather than assume the maximum was granted.
  • Forgetting to acknowledge the interrupt in the device’s status register — if the handler doesn’t clear the source, the device may keep re-asserting and flood the CPU.
  • Mixing `request_irq()` with `pci_alloc_irq_vectors()` incorrectly — always translate vector index to IRQ number with `pci_irq_vector()`, never assume `pdev->irq` still holds the right value once MSI/MSI-X is enabled.
  • Leaking IRQ vectors on probe failure — every `pci_alloc_irq_vectors()` needs a matching `pci_free_irq_vectors()` in the unwind path, exactly as shown in the error labels above.
  • Assuming MSI-X is always available — cheaper or virtualized devices may only support MSI or even legacy INTx; always pass all three flags and let the kernel negotiate.

Best Practices, Performance, and Security

  • Prefer MSI-X over MSI over legacy INTx whenever the device supports it — fewer shared lines means lower interrupt latency and better multi-core scaling.
  • Use `devm_*` managed variants (`devm_request_irq`, `devm_kzalloc`) so cleanup happens automatically on driver detach, reducing the chance of resource leaks.
  • When handling MSI-X on multi-queue hardware, spread vectors across CPUs with `irq_set_affinity_hint()` rather than leaving every vector pinned to CPU0.
  • Keep interrupt handlers short — acknowledge the device and hand real work off to a threaded IRQ handler or workqueue rather than doing heavy processing in hard-IRQ context.
  • Never trust interrupt data blindly from a device that could be under attacker control (e.g. a malicious PCIe peripheral) — validate any values read from device registers before acting on them.

Summary and Key Takeaways

PCI Express gives drivers three ways to raise an interrupt — legacy INTx on a shared wire, MSI as an in-band message, and MSI-X as a per-vector message table. Modern kernels collapse all three behind `pci_alloc_irq_vectors()`, which negotiates the best available mechanism and returns plain Linux IRQ numbers your handler code never has to branch on. Combined with the BAR mapping from the previous lecture, this completes the toolkit for a correct, modern linux pci device driver — the same toolkit used by real NIC, NVMe, and GPU drivers in the mainline kernel.

Frequently Asked Questions

Should a new PCI driver use MSI or MSI-X?

Prefer MSI-X when the hardware supports it — it allows more independent interrupt vectors, each with its own message address, avoiding shared-line contention. Use pci_alloc_irq_vectors() with the PCI_IRQ_MSIX | PCI_IRQ_MSI | PCI_IRQ_LEGACY flags so the kernel picks the best available option automatically.

Why is legacy INTx interrupt handling slower than MSI?

INTx uses a physical interrupt line that is often shared across multiple PCI devices. Every handler on that shared line has to run and check whether the interrupt belongs to it, adding latency. MSI and MSI-X replace the wire with a unique memory write per vector, removing the sharing problem entirely.

What happens if pci_alloc_irq_vectors() returns fewer vectors than requested?

Your driver must handle that gracefully — check the returned count and only register handlers for the vectors actually granted, rather than assuming the requested maximum was allocated.

How do I know which interrupt mechanism my device actually got?

Check /proc/interrupts for the driver’s IRQ entries, or inspect the device’s capability list with lspci -vvv, which shows whether MSI or MSI-X capability is enabled.

Why does pci_irq_vector() exist instead of using pdev->irq directly?

Once MSI or MSI-X is enabled, a device can have multiple IRQ numbers, one per vector. pci_irq_vector(pdev, i) correctly returns the IRQ number for vector index i regardless of which mechanism was granted, whereas pdev->irq only reflects the legacy INTx line.

Do I need real PCI hardware to practice this lecture?

No. QEMU can expose virtual PCI/PCIe devices such as virtio-net-pci to a guest VM, which is enough to practice probe(), BAR mapping, and interrupt handling exactly as shown in the ep_pci_demo driver.

Is this course part of a free Linux kernel development curriculum?

Yes — this lecture is part of EmbeddedPathashala’s free linux kernel development course, which also covers character drivers, platform drivers, DMA, clocks, and other Linux kernel subsystems end to end.

Continue Your Free Linux Kernel Development Course

Practice the ep_pci_demo driver on QEMU and move on to the next lecture in this free linux device drivers course.

Next Lecture Course Index

PREV_LEC  |  NEXT_LEC

Leave a Reply

Your email address will not be published. Required fields are marked *