2. Linux MSI中断框架:pci_alloc_irq_vectors()与pci_request_irq()的正确使用
在PCIe MSI中断调试中,驱动开发者最常踩入的陷阱并非硬件配置错误,而是对Linux内核MSI中断分配与注册API的误用。pci_alloc_irq_vectors()与pci_request_irq()是MSI/MSI-X中断初始化的核心接口,但它们的参数语义、返回值处理、以及调用顺序存在大量容易被忽略的细节。本节将拆解这两个API的正确用法,并指出常见错误。
核心原则:先分配中断向量(
pci_alloc_irq_vectors),再注册中断处理函数(pci_request_irq)。分配阶段决定中断类型(MSI/MSI-X/INTx)和数量,注册阶段绑定具体处理逻辑。
2.1 pci_alloc_irq_vectors():中断向量的分配与协商
该函数替代了旧版pci_enable_msi()/pci_enable_msix(),提供统一的分配接口。其原型如下:
int pci_alloc_irq_vectors(struct pci_dev *dev,
unsigned int min_vecs,
unsigned int max_vecs,
unsigned int flags);
参数含义与陷阱点:
| 参数 | 含义 | 常见错误 |
|---|---|---|
min_vecs |
驱动必须获得的最少中断向量数 | 设为0导致分配成功但实际无中断可用;设为大于硬件支持数导致分配失败 |
max_vecs |
驱动期望的最大中断向量数 | 超过dev->msi_max_segments或dev->msix_nr_entries导致内核截断 |
flags |
中断类型偏好:PCI_IRQ_MSIX、PCI_IRQ_MSI、PCI_IRQ_INTX |
未指定PCI_IRQ_MSI导致回退到INTx;或同时指定PCI_IRQ_MSIX | PCI_IRQ_MSI但未检查实际分配类型 |
陷阱1:返回值是实际分配的中断向量数,而非错误码。必须检查返回值是否 >=
min_vecs,而不是判断返回值是否小于0。当分配失败时,返回负数(如-ENOMEM),但部分驱动错误地将返回值与min_vecs比较时忽略了负数情况。
分配成功后,驱动通过pci_irq_vector()获取每个向量的Linux IRQ号:
int irq = pci_irq_vector(dev, index); // index 从0到nr_vecs-1
下面展示MSI中断向量分配与类型协商的完整流程:
2.2 pci_request_irq():中断处理函数的注册
分配中断向量后,驱动必须为每个向量注册中断处理函数。pci_request_irq()是推荐的注册接口,它封装了request_irq()并自动关联PCI设备上下文:
int pci_request_irq(struct pci_dev *dev, unsigned int nr,
irq_handler_t handler, irq_handler_t thread_fn,
void *dev_id, const char *fmt, ...);
| 参数 | 说明 | 陷阱 |
|---|---|---|
nr |
中断向量索引(从0开始),非IRQ号 | 误传pci_irq_vector()返回的IRQ号,导致注册到错误的中断线 |
handler |
中断顶半部处理函数 | 在MSI共享中断场景中未检查中断状态,导致其他设备中断被误处理 |
thread_fn |
中断底半部处理函数(可为NULL) | 在需要底半部处理时传NULL,导致中断上下文执行耗时操作 |
dev_id |
传递给处理函数的设备标识 | 传NULL导致free_irq()时无法正确匹配,或处理函数无法获取设备上下文 |
最佳实践:使用
pci_request_irq()而非直接调用request_irq(),因为前者会自动设置IRQF_SHARED标志(当使用INTx时),并确保dev_id与PCI设备关联,便于后续pci_free_irq()正确释放。
2.3 完整初始化序列与常见错误模式
正确的MSI中断初始化序列应遵循以下步骤:
- 分配向量:调用
pci_alloc_irq_vectors(),指定min_vecs和max_vecs。 - 检查返回值:确认返回值 >=
min_vecs,且 > 0。 - 获取IRQ号:对每个索引调用
pci_irq_vector()。 - 注册处理函数:对每个索引调用
pci_request_irq()。 - 启用设备中断:调用
pci_set_master()(如果需要DMA)并写设备寄存器使能MSI。
下面展示一个典型的错误模式对比:
| 错误模式 | 错误代码示例 | 后果 |
|---|---|---|
| 未检查返回值 | |
使用负数作为向量数,导致数组越界或空指针解引用 |
| IRQ号与索引混淆 | |
将IRQ号(如32)当作索引传入,注册到错误的向量 |
| 未释放旧向量 | |
内核警告“vector already allocated”,分配失败 |
| 忽略MSI-X单向量 | |
分配失败,驱动未提供回退逻辑 |
陷阱2:在
pci_alloc_irq_vectors()成功后,必须调用pci_free_irq_vectors()来释放向量,不能直接调用pci_disable_msi()或pci_disable_msix()。混合使用新旧API会导致引用计数混乱,内核触发“WARNING: at drivers/pci/msi.c”错误。
2.4 中断释放与错误回滚
在驱动卸载或初始化失败时,必须按注册的逆序释放资源:
// 错误回滚示例
err_irq:
for (i = 0; i < nr_vecs; i++)
pci_free_irq(dev, i, dev);
err_vectors:
pci_free_irq_vectors(dev);
return ret;
释放顺序必须严格遵循:先pci_free_irq()释放每个向量的处理函数,再pci_free_irq_vectors()释放中断向量。颠倒顺序会导致内核访问已释放的MSI描述符,触发use-after-free。
2.5 调试检查清单
当MSI中断无法触发时,按以下清单排查:
- 检查
/proc/interrupts:确认MSI中断向量已分配,且中断计数在触发时递增。 - 检查
dmesg:搜索“MSI”、“irq”、“vector”相关内核消息,确认无分配失败警告。 - 验证
pci_alloc_irq_vectors返回值:打印实际分配数,确认 >=min_vecs。 - 确认设备寄存器使能:读取PCI配置空间的MSI Capability寄存器,检查MSI Enable位是否置位。
- 检查中断处理函数注册:使用
cat /proc/irq/<irq>/smp_affinity确认中断亲和性设置正确。
快速验证:在驱动初始化成功后,执行
cat /sys/kernel/debug/irq/irqs/<irq>查看中断描述符的详细信息,包括action、handler和dev_id,确认注册正确。