4. Linux 内核 PCIe 中断初始化流程:从 pci_read_irq() 到 pci_enable_msi()/pci_enable_msix() 的完整调用链分析
PCIe 设备的中断初始化是内核 PCI 子系统中最核心的流程之一。它决定了设备究竟使用传统 INTx 中断线,还是使用高性能的 MSI/MSI-X 消息中断。本章将沿着 pci_read_irq() → pci_enable_device() → pci_enable_msi()/pci_enable_msix() 这条主线,逐层拆解每个关键函数的职责、调用时机与内部实现细节。
4.1 整体调用链全景图
下面的 SVG 图展示了从设备枚举到中断使能的完整调用链。图中用不同颜色区分了 PCI 核心层、驱动层和中断控制器层,箭头方向代表函数调用关系。
4.2 第一阶段:静态探测——pci_read_irq()
pci_read_irq() 是中断初始化的起点,在设备添加到 PCI 子系统时被调用。它的核心任务是从设备的配置空间或设备树中读取传统 INTx 中断号,并存入 struct pci_dev->irq 字段。
4.2.1 调用链与关键函数
| 函数 | 所在文件 | 职责 |
|---|---|---|
pci_read_irq() | drivers/pci/pci.c | 入口函数,根据平台选择读取方式 |
pci_read_irq_line() | drivers/pci/pci.c | 读取 PCI 配置空间中断线寄存器(INTx) |
of_irq_parse_pci() | drivers/of/of_pci_irq.c | 解析设备树中 PCI 中断映射 |
irq_create_of_mapping() | drivers/of/irq.c | 将设备树中断描述转换为 Linux IRQ 号 |
4.2.2 核心代码路径
// drivers/pci/pci.c
void pci_read_irq(struct pci_dev *dev)
{
dev->irq = 0; // 初始化为无效中断号
// 优先尝试设备树方式(DT/ACPI)
if (pci_dev_of_node(dev))
dev->irq = of_irq_parse_and_map_pci(dev, 0, 0);
// 回退到读取配置空间中断线寄存器
if (!dev->irq)
pci_read_irq_line(dev);
}
void pci_read_irq_line(struct pci_dev *dev)
{
u8 irq_line;
// 读取 PCI 配置空间偏移 0x3C 的中断线寄存器
pci_read_config_byte(dev, PCI_INTERRUPT_LINE, &irq_line);
// 仅当值在有效范围内(1~15 或非 255)才使用
if (irq_line > 0 && irq_line < 16)
dev->irq = irq_line;
else if (irq_line != 255)
dev->irq = irq_line;
// 注意:irq_line=0 表示未连接,irq_line=255 表示未知
}
dev->irq 为 0,说明设备未配置 INTx 中断。此时驱动如果调用 request_irq(dev->irq, ...) 会返回 -EINVAL。排查时可通过 cat /sys/bus/pci/devices/0000:xx:xx.x/irq 查看当前中断号。
4.3 第二阶段:动态使能——pci_enable_device()
pci_enable_device() 是驱动 probe 阶段必须调用的函数。它除了使能设备电源和 I/O/内存空间外,还会触发中断资源的分配。但请注意:它本身不分配 MSI 向量,真正的 MSI/MSI-X 分配发生在后续的 pci_alloc_irq_vectors() 调用中。
4.3.1 调用链展开
pci_enable_device()
└─ pci_enable_device_flags()
└─ do_pci_enable_device()
├─ pci_set_power_state(D0) // 唤醒设备
├─ pcibios_enable_device() // 架构相关使能
│ └─ pci_enable_resources() // 使能 BAR 空间
└─ pci_restore_state() // 恢复保存的配置空间
4.3.2 关键数据结构变化
| 字段 | 使能前 | 使能后 | 说明 |
|---|---|---|---|
dev->enable_cnt | 0 | 1 | 引用计数,防止重复使能 |
dev->is_enabled | false | true | 设备已使能标志 |
dev->current_state | PCI_UNKNOWN | PCI_D0 | 电源状态切换至全速运行 |
dev->irq | 来自 pci_read_irq() | 不变 | INTx 中断号在此阶段不会被修改 |
4.4 第三阶段:MSI/MSI-X 使能——pci_enable_msi()/pci_enable_msix()
现代 PCIe 驱动几乎都会使用 MSI 或 MSI-X 替代传统 INTx。内核提供了两套 API:旧式 pci_enable_msi()/pci_enable_msix() 和新式 pci_alloc_irq_vectors()。推荐使用新式 API,它统一了 MSI 和 MSI-X 的分配逻辑。
4.4.1 新旧 API 对比
| API | 引入版本 | 特点 | 推荐度 |
|---|---|---|---|
pci_enable_msi() | 2.6 | 仅分配 1 个 MSI 向量,不支持 MSI-X | 不推荐(旧驱动兼容) |
pci_enable_msix() | 2.6 | 分配多个 MSI-X 向量,需预先填充 msix_entry 数组 | 不推荐(接口复杂) |
pci_alloc_irq_vectors() | 4.8 | 统一接口,自动选择 MSI-X/MSI/INTx 回退 | 强烈推荐 |
4.4.2 pci_alloc_irq_vectors() 内部流程
// drivers/pci/msi.c
int pci_alloc_irq_vectors(struct pci_dev *dev, unsigned int min_vecs,
unsigned int max_vecs, unsigned int flags)
{
int vecs = -ENOSPC;
// 1. 优先尝试 MSI-X(如果 flags 允许)
if (flags & PCI_IRQ_MSIX) {
vecs = __pci_enable_msix_range(dev, NULL, min_vecs, max_vecs,
NULL, 0);
if (vecs > 0)
return vecs;
}
// 2. 回退尝试 MSI
if (flags & PCI_IRQ_MSI) {
vecs = __pci_enable_msi_range(dev, min_vecs, max_vecs, NULL);
if (vecs > 0)
return vecs;
}
// 3. 最后回退到 INTx(如果 flags 允许)
if (flags & PCI_IRQ_INTX) {
// 检查 dev->irq 是否有效
if (dev->irq > 0)
return 1; // 返回 1 个 INTx 中断
}
return vecs;
}
pci_alloc_irq_vectors() 返回负数,驱动通常直接返回 -ENODEV 导致 probe 失败。排查时先检查 dmesg 中是否有 "MSI/MSI-X not available" 或 "no interrupt" 等错误,然后确认 BIOS/固件是否禁用了 MSI(可通过 pci=no_msi 内核参数强制关闭)。
4.5 第四阶段:中断注册——request_irq()
无论使用 INTx 还是 MSI/MSI-X,最终驱动都需要调用 request_irq() 或 request_threaded_irq() 将中断处理函数注册到内核中断子系统。这里的关键区别在于传入的中断号来源:
- INTx 模式:使用
dev->irq(来自pci_read_irq()) - MSI 模式:使用
dev->irq(自动更新为 MSI 向量号) - MSI-X 模式:使用
pci_irq_vector(dev, index)获取每个向量的 IRQ 号
4.5.1 典型驱动代码模板
static int my_pci_probe(struct pci_dev *dev, const struct pci_device_id *id)
{
int ret, irq;
// 1. 使能设备
ret = pci_enable_device(dev);
if (ret)
return ret;
// 2. 分配 MSI-X 向量(推荐新式 API)
ret = pci_alloc_irq_vectors(dev, 1, 1, PCI_IRQ_MSIX | PCI_IRQ_MSI);
if (ret < 0) {
dev_err(&dev->dev, "Failed to allocate MSI/MSI-X vectors\n");
goto err_disable;
}
// 3. 获取中断号并注册
irq = pci_irq_vector(dev, 0); // 获取第 0 号向量
ret = request_irq(irq, my_interrupt_handler, 0,
"my_driver", dev);
if (ret) {
dev_err(&dev->dev, "Failed to request IRQ %d\n", irq);
goto err_free_vectors;
}
return 0;
err_free_vectors:
pci_free_irq_vectors(dev);
err_disable:
pci_disable_device(dev);
return ret;
}
4.6 关键数据结构与状态转换
整个中断初始化过程中,struct pci_dev 中的中断相关字段会经历多次状态变化。下表总结了每个阶段的关键字段值:
| 阶段 | dev->irq | dev->msi_enabled | dev->msix_enabled | 说明 |
|---|---|---|---|---|
| 设备创建后 | 0 | 0 | 0 | 未初始化 |
pci_read_irq() 后 | INTx 中断号(如 16) | 0 | 0 | 仅 INTx 可用 |
pci_enable_device() 后 | 不变 | 0 | 0 | 设备使能,中断未切换 |
pci_alloc_irq_vectors() 后(MSI) | MSI 向量号(如 32) | 1 | 0 | MSI 激活,dev->irq 被覆盖 |
pci_alloc_irq_vectors() 后(MSI-X) | 不变(仍为 INTx) | 0 | 1 | MSI-X 向量通过 pci_irq_vector() 获取 |
request_irq() 后 | 不变 | 不变 | 不变 | 中断处理函数已注册 |
dev->irq 始终保留原始的 INTx 中断号,不会像 MSI 那样被覆盖。这是因为 MSI-X 支持多个向量,每个向量有独立的 IRQ 号,不能用一个字段表示。驱动必须通过 pci_irq_vector() 获取每个向量的 IRQ 号。
4.7 故障排查要点
基于上述调用链分析,以下是中断初始化阶段最常见的故障模式及其排查方向:
- 中断号为零:检查
pci_read_irq()是否成功读取了 INTx 中断线。常见原因包括设备树缺少中断映射、ACPI 表错误、或配置空间中断线寄存器被清零。 - MSI 分配失败:检查
__pci_enable_msi_range()的返回值。可能原因:BIOS 禁用了 MSI、中断控制器不支持 MSI、或 MSI 向量资源耗尽。 - MSI-X 分配失败:检查
__pci_enable_msix_range()的返回值。常见原因:设备 MSI-X 能力表损坏、BAR 空间映射失败、或msix_entry数组未正确初始化。 - 中断不触发:确认
request_irq()传入的中断号与硬件实际产生的中断号一致。可通过cat /proc/interrupts查看注册的中断号及其触发次数。 - 中断风暴:检查中断处理函数是否在
handle_irq()中正确清除了设备的中断挂起位。对于 MSI/MSI-X,还需要确认是否调用了pci_msi_unmask()或等效操作。
lspci -vvv -s 01:00.0 | grep -i irq— 查看设备中断配置cat /sys/bus/pci/devices/0000:01:00.0/irq— 查看当前中断号cat /proc/interrupts | grep pci— 查看 PCI 设备中断统计echo 1 > /sys/bus/pci/devices/0000:01:00.0/msi_bus— 强制启用 MSI(调试用)
4.8 本章小结
PCIe 中断初始化流程可以概括为三个层次:
- 静态探测层:
pci_read_irq()从固件/设备树获取 INTx 中断号,作为后备方案。 - 动态使能层:
pci_enable_device()使能设备,pci_alloc_irq_vectors()分配 MSI/MSI-X 向量。 - 注册绑定层:
request_irq()将中断处理函数与 IRQ 号绑定,完成最终的中断通路建立。