陷阱二:中断号映射错误——pci_irq_vector() 返回值的校验与常见错误

在 MSI/MSI-X 中断初始化流程中,驱动开发者最常犯的错误之一就是错误地获取或使用中断号。Linux 内核提供了 pci_irq_vector() 函数来将 MSI 向量索引映射为 Linux 中断号(IRQ number),但该函数的返回值校验往往被忽视,导致中断无法触发、触发到错误的处理函数,甚至内核崩溃。

2.1 函数原型与语义

pci_irq_vector() 定义在 <linux/pci.h> 中,其原型如下:

int pci_irq_vector(struct pci_dev *dev, unsigned int nr);
参数/返回值 说明
dev 指向 PCI 设备的 struct pci_dev * 指针
nr MSI/MSI-X 向量的索引,从 0 开始
返回值 成功时返回 Linux IRQ 号(正数);失败时返回 负的错误码(如 -EINVAL)
⚠️ 常见陷阱:许多驱动开发者误以为 pci_irq_vector() 返回 0 表示失败,或者直接使用返回值而不检查负数。实际上,IRQ 0 在 x86 架构上对应系统定时器,但 MSI 中断号永远不会是 0;正确的校验方式是检查返回值 是否小于 0。

2.2 错误案例:未校验返回值

以下代码片段展示了典型的错误用法:

/* ❌ 错误示例:未校验 pci_irq_vector() 返回值 */
static int my_driver_probe(struct pci_dev *pdev,
                           const struct pci_device_id *id)
{
    int irq;
    int ret;

    ret = pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_MSI);
    if (ret < 0)
        return ret;

    irq = pci_irq_vector(pdev, 0);  /* 假设总是成功 */
    ret = request_irq(irq, my_isr, 0, "my_driver", dev);
    if (ret) {
        dev_err(&pdev->dev, "request_irq failed\n");
        return ret;
    }
    /* ... */
}

如果 pci_irq_vector() 因为 nr 越界或设备未正确分配向量而返回 -EINVAL,该负值会被直接传递给 request_irq(),导致 request_irq() 返回 -EINVAL,但错误日志中不会暴露真正的原因——中断号映射失败。

2.3 正确做法:严格校验

正确的做法是在获取 IRQ 号后立即检查返回值是否小于 0:

/* ✅ 正确示例:严格校验 pci_irq_vector() 返回值 */
static int my_driver_probe(struct pci_dev *pdev,
                           const struct pci_device_id *id)
{
    int irq;
    int ret;

    ret = pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_MSI);
    if (ret < 0)
        return ret;

    irq = pci_irq_vector(pdev, 0);
    if (irq < 0) {
        dev_err(&pdev->dev,
                "Failed to map MSI vector 0 to IRQ: %d\n", irq);
        pci_free_irq_vectors(pdev);
        return irq;
    }

    ret = request_irq(irq, my_isr, 0, "my_driver", dev);
    if (ret) {
        dev_err(&pdev->dev, "request_irq failed: %d\n", ret);
        pci_free_irq_vectors(pdev);
        return ret;
    }
    /* ... */
}

2.4 映射错误的原因分析

导致 pci_irq_vector() 返回负值的常见原因包括:

  • 向量索引越界:nr 参数大于等于实际分配的向量数量。例如分配了 2 个向量,却请求索引 2。
  • 设备未启用 MSI/MSI-X:在调用 pci_alloc_irq_vectors() 之前就调用 pci_irq_vector()。
  • 中断控制器映射失败:底层 IRQ 域(irq domain)无法为 MSI 向量分配中断号,通常与 ACPI/Device Tree 中断映射配置错误有关。
  • 热插拔后状态不一致:设备被移除后重新枚举,但驱动未重新分配向量。
pci_irq_vector() 映射错误根因分析 pci_irq_vector() 返回负值 向量索引越界 nr ≥ 已分配向量数 MSI 未启用 未调用 alloc_irq_vectors IRQ 域映射失败 ACPI/DT 配置错误 热插拔状态不一致 设备重枚举未重分配 调试建议 1. 在 request_irq() 之前打印 irq 值:dev_dbg(dev, "IRQ=%d\n", irq); 2. 检查 /proc/interrupts 确认中断号是否注册成功。 3. 使用 tracepoint:echo 1 > /sys/kernel/debug/tracing/events/irq/irq_request/enable 4. 验证 pci_alloc_irq_vectors() 返回值与 nr 参数的关系。

2.5 多向量场景下的索引管理

当使用多个 MSI/MSI-X 向量时,索引管理变得更加复杂。以下表格总结了不同场景下的索引规则:

中断模式 向量数量 有效索引范围 典型错误
MSI 1, 2, 4, 8, 16, 32 0 到 (n-1) 使用索引 n 或负数索引
MSI-X 1 到 2048 0 到 (n-1) 使用未分配的索引(如跳过索引 0)
传统 INTx 1 仅索引 0 使用索引 0 以外的值
💡 最佳实践:建议在驱动中定义一个宏或枚举来管理向量索引,避免硬编码数字。例如:
#define MSI_VEC_TX  0
#define MSI_VEC_RX  1
#define MSI_VEC_ERR 2

irq_tx = pci_irq_vector(pdev, MSI_VEC_TX);
if (irq_tx < 0) { /* 处理错误 */ }

2.6 与 pci_request_irq() 的对比

Linux 内核 4.15 之后引入了 pci_request_irq() 辅助函数,它内部封装了 pci_irq_vector() 和 request_irq(),并自动处理错误校验:

/* 使用 pci_request_irq() 简化代码 */
static int my_driver_probe(struct pci_dev *pdev,
                           const struct pci_device_id *id)
{
    int ret;

    ret = pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_MSI);
    if (ret < 0)
        return ret;

    /* 内部自动校验 pci_irq_vector() 返回值 */
    ret = pci_request_irq(pdev, 0, my_isr, NULL, "my_driver", dev);
    if (ret) {
        dev_err(&pdev->dev, "pci_request_irq failed: %d\n", ret);
        pci_free_irq_vectors(pdev);
        return ret;
    }
    /* ... */
}

对于新驱动,优先推荐使用 pci_request_irq();对于需要精细控制中断亲和性或使用 threaded IRQ 的场景,仍需手动调用 pci_irq_vector() 并严格校验。

2.7 调试检查清单

  • ☐ 调用 pci_irq_vector() 后是否检查返回值 < 0?
  • ☐ 向量索引是否在 pci_alloc_irq_vectors() 返回的数量范围内?
  • ☐ 是否在 pci_free_irq_vectors() 之后还尝试获取 IRQ 号?
  • ☐ 热插拔路径中是否重新分配向量并重新获取 IRQ 号?
  • ☐ 是否在 devm 资源管理框架下正确使用,避免资源泄漏?