PCI IRQ Vector Lookup Guide-Free Linux Device Drivers Training Online

PCI IRQ Vector Lookup Guide

A lecture from EmbeddedPathashala’s free linux kernel development course — turning an allocated interrupt vector index into a real Linux IRQ number with pci_irq_vector().

Reading time: 15 min
Level: Intermediate
Kernel: 6.x mainline

Earlier in this free linux kernel development course you saw how pci_alloc_irq_vectors() reserves a block of legacy, MSI, or MSI-X interrupt vectors for a device. Allocating vectors is only half the job — before you can call request_irq(), you need the actual Linux IRQ number for each vector index, and that’s exactly what pci_irq_vector() gives you. This lecture goes deep on that lookup function, the flag set that controls how vectors are allocated, and wires the whole thing into a complete, working interrupt handler.

What You Will Learn

  • How pci_irq_vector() maps a vector index to a Linux IRQ number
  • The full PCI_IRQ_* flag set and how PCI_IRQ_ALL_TYPES negotiates a mode
  • Why the lookup behaves differently for MSI-X vs MSI vs legacy INTx
  • Wiring pci_alloc_irq_vectors() + pci_irq_vector() + request_irq() into a real handler
  • Testing and observing interrupts on a QEMU-emulated PCI device

Prerequisites

This lecture builds directly on the earlier PCI interrupt distribution lecture in this free linux device drivers course, where pci_alloc_irq_vectors() and the INTx/MSI/MSI-X distinction were introduced. You should be comfortable with basic interrupt handler registration (request_irq()/free_irq()) from the core interrupt-handling lectures elsewhere in this free embedded linux course.

Why a Separate Lookup Step Exists

When you call pci_alloc_irq_vectors(), the PCI core doesn’t hand you Linux IRQ numbers directly — it hands you back a count of how many vectors it managed to allocate. That’s because the three interrupt modes it can pick between represent the vectors completely differently internally:

  • Legacy INTx: a single shared line, already sitting in pci_dev->irq
  • MSI: a contiguous block of message-signaled vectors
  • MSI-X: a table of independently addressable, non-contiguous vector entries

A driver that wants to be agnostic to which mode was ultimately granted needs one stable API to go from “the vector I care about, by index” to “the actual Linux IRQ number I hand to request_irq().” That stable API is pci_irq_vector().

int pci_irq_vector(struct pci_dev *dev, unsigned int nr);

dev is the PCI device, and nr is the zero-based vector index within whatever range pci_alloc_irq_vectors() granted you. The return value is a Linux IRQ number on success, or a negative errno if nr is out of range for the mode currently active on that device.

Vector Index to Linux IRQ Number
pci_alloc_irq_vectors(dev, min, max, flags) → N vectors granted (mode chosen internally) nr = 0 → pci_irq_vector(dev, 0) → Linux IRQ X → request_irq(X, handler0, …) nr = 1 → pci_irq_vector(dev, 1) → Linux IRQ Y → request_irq(Y, handler1, …) nr = 2 → pci_irq_vector(dev, 2) → Linux IRQ Z → request_irq(Z, handler2, …)

How the Lookup Differs by Mode

Internally, the resolution strategy pci_irq_vector() uses depends on which mode ended up active on the device (tracked through dev->msix_enabled and dev->msi_enabled):

Active ModeResolution Strategy
MSI-XWalks the device’s MSI-X entry table and returns the IRQ tied to the nr-th entry, since MSI-X entries are independently addressable and don’t have to be contiguous
MSIAdds nr as a contiguous offset to the device’s base MSI IRQ, since classic MSI vectors are always allocated as one contiguous block
Legacy INTxIgnores nr entirely and always returns pci_dev->irq, since a legacy device has exactly one shared interrupt line

This is the entire reason the function exists rather than a driver just reading pdev->irq everywhere: pdev->irq is only meaningful for the legacy case. Once MSI or MSI-X is active, each vector has its own distinct IRQ number, and only pci_irq_vector() knows how to resolve it correctly for whichever mode actually got negotiated.

The PCI_IRQ_* Flag Set

The mode that ends up active is controlled by the flags argument you pass into pci_alloc_irq_vectors(), defined in include/linux/pci.h:

FlagMeaning
PCI_IRQ_LEGACYOnly try a single shared legacy INTx vector
PCI_IRQ_MSIOnly try classic MSI; sets dev->msi_enabled on success
PCI_IRQ_MSIXOnly try MSI-X; sets dev->msix_enabled on success
PCI_IRQ_ALL_TYPESTry MSI-X first, fall back to MSI, then fall back to legacy INTx
PCI_IRQ_AFFINITYOR this in to auto-spread the allocated vectors across available CPUs

PCI_IRQ_ALL_TYPES is what most modern drivers use in practice — it lets the PCI core negotiate the best interrupt mode the platform and device both support, and returns immediately at the first mode that succeeds. Your driver code then stays identical regardless of which mode won, because it always goes through pci_irq_vector() rather than assuming a particular mode.

Building a Complete Interrupt-Driven Driver

Here’s an original demo driver, ep_pci_irqvec_demo, written for this lecture. It allocates up to four vectors with PCI_IRQ_ALL_TYPES, resolves each with pci_irq_vector(), and registers a handler for every one of them — a pattern typical of multi-queue network and storage drivers. It targets QEMU’s edu educational PCI device (vendor 0x1234, device 0x11e8), the same device used in this course’s earlier PCI interrupt lecture.

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

#define DRV_NAME     "ep_pci_irqvec_demo"
#define EP_MAX_VECS  4

struct ep_irqvec_dev {
        struct pci_dev *pdev;
        int nvecs;
};

static irqreturn_t ep_irqvec_handler(int irq, void *data)
{
        struct ep_irqvec_dev *edev = data;

        dev_info(&edev->pdev->dev, "interrupt fired on Linux IRQ %d\n", irq);
        return IRQ_HANDLED;
}

static int ep_irqvec_probe(struct pci_dev *pdev,
                            const struct pci_device_id *id)
{
        struct ep_irqvec_dev *edev;
        int nvecs, i, irq, err;

        err = pcim_enable_device(pdev);
        if (err)
                return err;

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

        edev->pdev = pdev;

        nvecs = pci_alloc_irq_vectors(pdev, 1, EP_MAX_VECS,
                                       PCI_IRQ_ALL_TYPES);
        if (nvecs < 0)
                return nvecs;

        edev->nvecs = nvecs;

        if (pdev->msix_enabled)
                dev_info(&pdev->dev, "using MSI-X, %d vector(s)\n", nvecs);
        else if (pdev->msi_enabled)
                dev_info(&pdev->dev, "using MSI, %d vector(s)\n", nvecs);
        else
                dev_info(&pdev->dev, "using legacy INTx\n");

        for (i = 0; i < nvecs; i++) {
                irq = pci_irq_vector(pdev, i);
                if (irq < 0) {
                        pci_free_irq_vectors(pdev);
                        return irq;
                }

                err = devm_request_irq(&pdev->dev, irq, ep_irqvec_handler,
                                        0, DRV_NAME, edev);
                if (err) {
                        pci_free_irq_vectors(pdev);
                        return err;
                }

                dev_info(&pdev->dev, "vector %d -> Linux IRQ %d registered\n",
                         i, irq);
        }

        pci_set_drvdata(pdev, edev);
        return 0;
}

static void ep_irqvec_remove(struct pci_dev *pdev)
{
        pci_free_irq_vectors(pdev);
        dev_info(&pdev->dev, "ep_pci_irqvec_demo removed\n");
}

static const struct pci_device_id ep_irqvec_ids[] = {
        { PCI_DEVICE(0x1234, 0x11e8) },
        { }
};
MODULE_DEVICE_TABLE(pci, ep_irqvec_ids);

static struct pci_driver ep_pci_irqvec_driver = {
        .name     = DRV_NAME,
        .id_table = ep_irqvec_ids,
        .probe    = ep_irqvec_probe,
        .remove   = ep_irqvec_remove,
};
module_pci_driver(ep_pci_irqvec_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("EmbeddedPathashala");
MODULE_DESCRIPTION("Original demo: PCI IRQ vector lookup with pci_irq_vector()");

Two things worth calling out in this driver: it uses devm_request_irq() instead of the raw request_irq()/free_irq() pair, which ties the IRQ’s lifetime to the device and removes the need to manually free it in the remove callback; and it always calls pci_free_irq_vectors() in remove, regardless of which mode was active, because the same call correctly tears down legacy, MSI, or MSI-X allocations.

Running It on QEMU

qemu-system-x86_64 \
    -kernel bzImage \
    -initrd rootfs.cpio.gz \
    -device edu \
    -append "console=ttyS0" \
    -nographic
make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
insmod ep_pci_irqvec_demo.ko
dmesg | tail

Expected output on a QEMU host that supports MSI-X passthrough:

ep_pci_irqvec_demo 0000:00:04.0: using MSI-X, 4 vector(s)
ep_pci_irqvec_demo 0000:00:04.0: vector 0 -> Linux IRQ 27 registered
ep_pci_irqvec_demo 0000:00:04.0: vector 1 -> Linux IRQ 28 registered
ep_pci_irqvec_demo 0000:00:04.0: vector 2 -> Linux IRQ 29 registered
ep_pci_irqvec_demo 0000:00:04.0: vector 3 -> Linux IRQ 30 registered

You can confirm the negotiated mode and IRQ mapping from user space too:

cat /proc/interrupts | grep ep_pci_irqvec_demo
lspci -vv -s 00:04.0 | grep -i -A3 "MSI-X\|MSI:"

Unload with:

rmmod ep_pci_irqvec_demo

Real-World Use Cases

This exact pattern — allocate a block of vectors with PCI_IRQ_ALL_TYPES, then loop over them resolving each with pci_irq_vector() — is precisely how multi-queue NIC and NVMe drivers in mainline set up one interrupt per RX/TX queue or per submission queue. Combining it with PCI_IRQ_AFFINITY lets the kernel spread those per-queue interrupts across CPUs automatically, which is a major factor in multi-core network and storage throughput scaling.

Common Mistakes

Reading pdev->irq after enabling MSI-X

pdev->irq only holds a meaningful value for legacy INTx mode. Once MSI or MSI-X is active, always resolve each vector through pci_irq_vector() instead.

Requesting more vectors than min_vecs guarantees

pci_alloc_irq_vectors() can legally return fewer than max_vecs. Always use the actual returned count, not the requested maximum, when looping to register handlers.

Forgetting pci_free_irq_vectors() on error paths

If request_irq() fails partway through a multi-vector loop, the allocated vector block must still be freed with pci_free_irq_vectors() before returning an error from probe().

Best Practices

  • Always resolve IRQ numbers through pci_irq_vector(), never pdev->irq directly
  • Prefer PCI_IRQ_ALL_TYPES for maximum hardware/platform compatibility
  • Use PCI_IRQ_AFFINITY on multi-queue devices for better CPU scaling
  • Use devm_request_irq() to avoid manual cleanup bugs
  • Always pair pci_alloc_irq_vectors() with pci_free_irq_vectors() on every exit path

Summary and Key Takeaways

pci_irq_vector() is the missing link between an allocated block of interrupt vectors and the concrete Linux IRQ numbers request_irq() needs. It resolves differently depending on whether MSI-X, MSI, or legacy INTx ended up active, which is exactly why relying on it — rather than pdev->irq — keeps a driver correct across every platform and hardware combination it might run on. Combined with pci_alloc_irq_vectors() and the PCI_IRQ_* flag set, it forms the backbone of interrupt handling for every serious PCI and PCIe driver in the mainline kernel. That wraps up the interrupt-handling arc of the PCI subsystem series in this free linux kernel development course.

Frequently Asked Questions

What does pci_irq_vector() return on failure?

A negative errno, typically -EINVAL, if the requested vector index nr is out of range for the interrupt mode currently active on the device.

Is pdev->irq still usable at all on modern kernels?

Yes, but only when legacy INTx mode is what actually got negotiated. Once MSI or MSI-X is active, pdev->irq no longer reflects the per-vector IRQ numbers, so pci_irq_vector() must be used instead.

What is the difference between MSI and MSI-X vector resolution?

MSI vectors are always allocated as one contiguous block, so resolution is a simple base-plus-offset calculation. MSI-X vectors sit in an independently addressable table, so resolution walks that table to find the matching entry.

Why use PCI_IRQ_ALL_TYPES instead of picking MSI-X directly?

PCI_IRQ_ALL_TYPES lets the kernel automatically fall back through MSI-X, then MSI, then legacy INTx, so the same driver keeps working correctly on platforms or devices that don’t support the newer modes.

What does PCI_IRQ_AFFINITY actually change?

It asks the kernel to automatically distribute the allocated interrupt vectors across the system’s available CPUs rather than leaving them all pinned to a single core, improving multi-core scaling for multi-queue devices.

Do I need pci_free_irq_vectors() if I used devm_request_irq()?

Yes. devm_request_irq() only manages the individual IRQ handler’s lifetime, not the underlying vector block allocated by pci_alloc_irq_vectors(), which must still be released explicitly.

Is this part of a complete free linux device drivers course?

Yes, this lecture continues the PCI subsystem series inside EmbeddedPathashala’s free linux device drivers course, following directly from the earlier interrupt distribution and I/O port access lectures.

Continue the Free Linux Kernel Development Course

Explore more PCI, driver model, and subsystem lectures in this free embedded linux course.

Next Lecture Browse All Lectures

Leave a Reply

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