2. 中断共享原理
在PCIe总线中,传统INTx中断(INTA#、INTB#、INTC#、INTD#)是为兼容PCI/PCI-X设备而保留的中断机制。与MSI/MSI-X的点对点中断不同,INTx采用电平触发、线或连接的共享方式,这意味着多个设备可以共用同一条中断线。理解INTx的共享原理、电气特性及驱动兼容性要求,是正确配置中断处理程序的基础。
核心概念: INTx中断共享的本质是多个PCIe功能通过开漏输出连接到同一根中断请求线(IRQ)。任何设备都可以将中断线拉低,中断控制器检测到电平变化后触发中断处理程序,再由软件遍历所有共享设备的中断服务例程(ISR)。
2.1 INTx中断共享的电气模型
INTx中断线在电气上采用开漏(Open-Drain)或开集电极(Open-Collector)输出,配合上拉电阻实现“线与”逻辑。当设备需要触发中断时,将输出管脚拉低(低电平有效);当设备无中断请求时,输出管脚处于高阻态,由上拉电阻将中断线维持在高电平。
电气特性要点:
- 低电平有效:INTx信号在PCIe总线上是低电平触发,设备通过拉低中断线来请求服务。
- 线与逻辑:多个设备的开漏输出直接连接,只要有一个设备拉低,整条线就是低电平。
- 上拉电阻:中断线必须通过上拉电阻连接到Vcc,确保无中断请求时线为高电平。
- 电平触发 vs 边沿触发:INTx是电平触发,中断控制器持续检测电平状态,而非仅检测跳变沿。
2.2 INTx中断共享的软件处理流程
当多个设备共享同一IRQ时,内核的中断处理程序无法直接确定是哪个设备触发了中断。因此,必须采用轮询(Polling)方式:中断处理程序依次调用所有注册在该IRQ上的设备中断服务例程(ISR),直到某个ISR返回IRQ_HANDLED,或者所有ISR都返回IRQ_NONE。
关键约束:共享中断的驱动必须满足以下条件,否则会导致系统死锁或中断丢失:
- ISR必须能够区分中断是否属于自己的设备(通过读取设备的中断状态寄存器)。
- 如果ISR检测到中断不属于自己,必须立即返回
IRQ_NONE,不能执行任何操作。 - ISR必须清除设备的中断挂起位,否则中断线将保持低电平,导致无限循环触发。
- 驱动注册中断时必须设置
IRQF_SHARED标志,否则内核会拒绝共享IRQ。
2.3 INTx中断路由与PCIe配置空间
PCIe设备通过配置空间中的中断引脚寄存器(Interrupt Pin Register)和中断线寄存器(Interrupt Line Register)来管理INTx中断路由。中断引脚寄存器是只读的,指示设备使用哪个INTx引脚;中断线寄存器由BIOS/固件写入,指定设备连接到哪个系统IRQ。
| 配置空间寄存器 | 偏移地址 | 位宽 | 描述 |
|---|---|---|---|
| Interrupt Pin | 0x3D | 8位 | 只读。值1=INTA#,2=INTB#,3=INTC#,4=INTD#,0=设备不使用INTx |
| Interrupt Line | 0x3C | 8位 | 读写。由固件写入系统IRQ号,驱动读取后用于request_irq() |
| Status Register | 0x06 | 16位 | 位3(Interrupt Status)反映当前INTx信号状态,只读 |
| Command Register | 0x04 | 16位 | 位10(Interrupt Disable)可屏蔽INTx中断,写1禁用 |
驱动读取中断信息的标准代码片段:
// 读取中断引脚和中断线
u8 pin, line;
pci_read_config_byte(pdev, PCI_INTERRUPT_PIN, &pin);
pci_read_config_byte(pdev, PCI_INTERRUPT_LINE, &line);
dev_info(&pdev->dev, "INTx pin=%d, IRQ line=%d\n", pin, line);
// 注册共享中断处理程序
if (pin != 0) {
err = request_irq(pdev->irq, my_interrupt_handler,
IRQF_SHARED, "my_driver", pdev);
if (err) {
dev_err(&pdev->dev, "Failed to register shared IRQ\n");
return err;
}
}
2.4 驱动兼容性要求与最佳实践
编写支持INTx共享中断的驱动时,必须遵循以下兼容性规则,否则可能导致中断风暴、系统挂起或设备无法正常工作。
| 要求类别 | 具体规则 | 违反后果 |
|---|---|---|
| 注册标志 | 必须使用IRQF_SHARED标志 |
内核拒绝注册,返回-EBUSY |
| 设备检测 | ISR必须读取设备状态寄存器确认中断来源 | 误处理其他设备的中断,导致数据损坏 |
| 返回值 | 确认是自己的中断返回IRQ_HANDLED,否则返回IRQ_NONE |
内核认为中断未被处理,可能禁用IRQ |
| 中断清除 | 必须在ISR中清除设备的中断挂起位 | 中断线持续低电平,无限触发中断 |
| 设备ID | request_irq()的dev_id参数必须唯一标识设备 |
无法在free_irq()时正确释放 |
| 中断屏蔽 | 支持在ISR中临时屏蔽本设备中断(若硬件支持) | 高频率中断可能导致其他设备饥饿 |
调试技巧: 在
/proc/interrupts中可以查看每个IRQ上注册的设备数量以及中断触发次数。如果某个共享IRQ的中断计数异常增长,说明可能有设备未正确清除中断挂起位,或者ISR返回了错误的返回值。
# 查看中断统计
cat /proc/interrupts | grep -E "CPU|17:"
CPU0 CPU1
17: 12345 67890 IO-APIC 17-fasteoi eth0, my_pci_dev, usb_hcd
2.5 INTx与MSI/MSI-X的关键对比
理解INTx与MSI/MSI-X的差异,有助于在驱动设计时做出正确的选择。下表从多个维度对比两种中断机制:
| 对比维度 | INTx(共享中断) | MSI/MSI-X |
|---|---|---|
| 触发方式 | 电平触发(低电平有效) | 边沿触发(写内存写事务) |
| 共享能力 | 支持,多个设备共用一根IRQ线 | 不支持共享,每个中断向量独立 |
| 中断向量数 | 最多4个(INTA#~INTD#) | MSI最多32个,MSI-X最多2048个 |
| 软件开销 | 高,需要轮询所有共享ISR | 低,直接定位到目标ISR |
| 电气要求 | 需要上拉电阻,开漏输出 | 无需额外电气元件,基于内存写 |
| 中断丢失风险 | 低(电平触发,持续有效) | 高(边沿触发,可能丢失) |
| 驱动注册标志 | IRQF_SHARED |
无需IRQF_SHARED |
| 适用场景 | 传统设备、低吞吐量设备 | 高性能设备、多队列设备 |
重要提示: 在PCIe原生设计中,INTx中断主要用于兼容旧式操作系统和引导阶段。对于现代Linux内核,如果设备支持MSI/MSI-X,内核会优先尝试启用MSI/MSI-X。驱动开发者应在探测函数中优先调用
pci_alloc_irq_vectors(),仅在MSI/MSI-X不可用时回退到INTx。
2.6 实战:编写兼容INTx共享的ISR模板
以下是一个完整的INTx共享中断处理程序模板,展示了设备检测、中断清除和返回值处理的正确模式:
static irqreturn_t my_pci_isr(int irq, void *dev_id)
{
struct my_pci_dev *mydev = (struct my_pci_dev *)dev_id;
u32 status;
// 步骤1:读取设备中断状态寄存器
status = readl(mydev->ioaddr + MYDEV_INT_STATUS);
// 步骤2:检查是否属于本设备的中断
if (!(status & MYDEV_INT_PENDING)) {
// 不是本设备的中断,立即返回IRQ_NONE
return IRQ_NONE;
}
// 步骤3:清除中断挂起位(写1清除)
writel(MYDEV_INT_PENDING, mydev->ioaddr + MYDEV_INT_CLEAR);
// 步骤4:处理中断事件(如DMA完成、错误等)
if (status & MYDEV_INT_DMA_DONE) {
// 处理DMA完成
dma_complete_handler(mydev);
}
if (status & MYDEV_INT_ERROR) {
// 处理错误
dev_err(&mydev->pdev->dev, "Device error detected\n");
}
// 步骤5:返回IRQ_HANDLED表示已处理
return IRQ_HANDLED;
}
// 注册中断时传递设备私有结构体作为dev_id
static int my_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
struct my_pci_dev *mydev;
int ret;
// ... 分配和初始化mydev ...
// 注册共享中断
ret = request_irq(pdev->irq, my_pci_isr,
IRQF_SHARED, "my_pci_driver", mydev);
if (ret) {
dev_err(&pdev->dev, "Cannot register shared ISR\n");
return ret;
}
return 0;
}
static void my_pci_remove(struct pci_dev *pdev)
{
struct my_pci_dev *mydev = pci_get_drvdata(pdev);
// 释放中断时必须使用相同的dev_id
free_irq(pdev->irq, mydev);
// ... 清理其他资源 ...
}
关键检查清单:
- ☐ ISR中是否读取了设备状态寄存器来确认中断来源?
- ☐ 是否在ISR中清除了中断挂起位?
- ☐ 非本设备中断是否返回
IRQ_NONE? - ☐ 注册时是否使用了
IRQF_SHARED标志? - ☐
dev_id参数是否指向唯一的设备私有数据结构? - ☐ 是否在
remove()函数中正确调用了free_irq()?