3. 网络设备中断注册:request_irq()、devm_request_irq()、中断标志

中断注册是网络驱动与内核中断子系统交互的入口。驱动必须为设备分配的中断线(IRQ)注册一个处理函数,内核才能在硬件产生中断时正确调用该函数。Linux 内核提供了两套核心注册 API:request_irq() 和 devm_request_irq(),以及一组用于描述中断行为的中断标志(IRQF_*)。

3.1 核心 API:request_irq()

request_irq() 是中断注册的基础函数,定义在 include/linux/interrupt.h 中。驱动在探测(probe)阶段调用该函数,将 IRQ 号、处理函数、中断标志、设备名称和设备上下文绑定在一起。

int request_irq(unsigned int irq,
                irq_handler_t handler,
                unsigned long flags,
                const char *name,
                void *dev);
参数 类型 说明
irq unsigned int 要注册的中断号,通常由 pci_alloc_irq_vectors() 或平台设备资源获取
handler irq_handler_t 中断处理函数指针,原型为 irqreturn_t (*handler)(int irq, void *dev_id)
flags unsigned long 中断标志位掩码,控制中断触发方式、共享属性、线程化等行为
name const char * 设备名称,显示在 /proc/interrupts 中,用于调试和统计
dev void * 传递给处理函数的设备标识,共享中断时用于区分设备;通常传入网络设备的 struct net_device * 或 struct adapter *
关键约束: 共享中断(IRQF_SHARED)场景下,dev 参数必须唯一且非 NULL,内核在调用处理函数前会遍历所有注册该 IRQ 的处理程序,只有 dev 匹配的设备才会被调用。驱动必须检查中断状态寄存器,确认中断是否来自本设备。

3.2 托管版本:devm_request_irq()

devm_request_irq() 是设备资源管理(devres)框架下的中断注册函数。它自动将中断的生命周期绑定到设备生命周期,驱动卸载或 probe 失败时,内核自动释放中断,无需手动调用 free_irq()。

int devm_request_irq(struct device *dev,
                     unsigned int irq,
                     irq_handler_t handler,
                     unsigned long flags,
                     const char *name,
                     void *dev_id);
对比维度 request_irq() devm_request_irq()
资源释放方式 手动调用 free_irq() 自动释放,设备脱离时内核回收
适用场景 需要精细控制中断生命周期,或中断与设备生命周期不同步 标准网络驱动 probe/remove 流程,推荐使用
错误路径处理 probe 失败时需手动清理已注册的中断 probe 失败时自动回滚,减少内存泄漏风险
代码简洁性 需要维护中断释放逻辑 更简洁,减少样板代码
最佳实践: 现代网络驱动优先使用 devm_request_irq()。它消除了 probe 失败路径中遗漏 free_irq() 的风险,且与 devres 框架的其他资源(如 DMA 映射、寄存器映射)统一管理。

3.3 中断标志(IRQF_*)详解

中断标志通过 flags 参数传递给注册函数,多个标志使用按位或(|)组合。以下是网络驱动中最常用的标志:

标志 值 含义 网络驱动典型用途
IRQF_SHARED 0x00000080 允许共享中断线,多个设备共用同一 IRQ PCI 传统中断(INTx)场景,多个设备可能共享同一物理中断线
IRQF_TRIGGER_NONE 0x00000000 不指定触发方式,由固件或 BIOS 配置 平台设备或旧式 PCI 设备
IRQF_TRIGGER_RISING 0x00000001 上升沿触发 部分 SoC 内部网络控制器
IRQF_TRIGGER_FALLING 0x00000002 下降沿触发 部分 SoC 内部网络控制器
IRQF_TRIGGER_HIGH 0x00000004 高电平触发 PCIe MSI/MSI-X 中断通常使用电平触发
IRQF_TRIGGER_LOW 0x00000008 低电平触发 部分嵌入式网络设备
IRQF_ONESHOT 0x00002000 中断处理期间屏蔽该中断线,处理完成后自动重新使能 线程化中断处理(request_threaded_irq)必须使用
IRQF_NO_SUSPEND 0x00004000 系统休眠时不关闭该中断 网络唤醒(WoL)功能需要保持中断活跃
共享中断陷阱: 使用 IRQF_SHARED 时,处理函数必须首先读取设备中断状态寄存器,确认中断是否属于本设备。如果中断不属于本设备,必须立即返回 IRQ_NONE,否则会导致内核误判为"伪中断"(spurious interrupt),严重时内核会禁用该中断线。

3.4 中断注册流程与标志组合

网络驱动在 probe 函数中注册中断的典型流程如下:

  1. 获取 IRQ 资源: 通过 pci_alloc_irq_vectors() 分配 MSI/MSI-X 向量,或从 platform_get_irq() 获取传统中断号。
  2. 确定中断标志: MSI/MSI-X 场景使用 IRQF_TRIGGER_HIGH;传统 INTx 场景使用 IRQF_SHARED | IRQF_TRIGGER_HIGH。
  3. 注册中断: 调用 devm_request_irq() 或 request_irq(),传入 IRQ 号、处理函数、标志、设备名称和设备上下文。
  4. 启用中断: 在设备寄存器中使能中断位,开始接收硬件中断。
// 典型网络驱动中断注册示例(使用 devm_request_irq)
static int netdev_probe(struct platform_device *pdev)
{
    struct net_device *ndev;
    struct adapter *adapter;
    int irq, ret;

    // ... 分配 net_device、初始化硬件 ...

    irq = platform_get_irq(pdev, 0);
    if (irq < 0)
        return irq;

    // 使用 IRQF_TRIGGER_HIGH | IRQF_SHARED 注册中断
    ret = devm_request_irq(&pdev->dev, irq,
                           netdev_interrupt,
                           IRQF_TRIGGER_HIGH | IRQF_SHARED,
                           ndev->name, ndev);
    if (ret) {
        dev_err(&pdev->dev, "failed to request irq %d\n", irq);
        return ret;
    }

    // 使能设备中断寄存器
    writel(INT_ENABLE, adapter->ioaddr + INT_MASK_REG);

    return 0;
}

3.5 中断注册架构图

下图展示了从驱动 probe 到中断子系统注册的完整数据流与标志决策路径:

网络设备中断注册架构图 驱动 probe 入口 获取 IRQ 资源 pci_alloc_irq_vectors / platform_get_irq 确定中断标志 MSI / INTx / 共享决策 注册中断处理函数 devm_request_irq / request_irq 使能设备中断 写设备中断使能寄存器 中断类型判断 MSI / MSI-X 传统 INTx IRQF_TRIGGER_HIGH IRQF_SHARED | IRQF_TRIGGER_HIGH 每个向量独立注册 共享中断线注册 说明: MSI/MSI-X 每个中断向量独占 IRQ,无需 IRQF_SHARED;传统 INTx 必须设置 IRQF_SHARED,处理函数需检查设备中断状态。 devm_request_irq 自动管理生命周期,推荐用于所有新驱动。

3.6 中断处理函数签名与返回值

中断处理函数必须遵循内核定义的签名:

irqreturn_t (*handler)(int irq, void *dev_id);
返回值 含义 使用场景
IRQ_NONE (0) 中断不是本设备产生的 共享中断中,读取状态寄存器后发现中断不属于本设备
IRQ_HANDLED (1) 中断已被本设备处理 正常处理完成,或 NAPI 调度后返回
IRQ_WAKE_THREAD (2) 唤醒线程化中断处理函数 使用 request_threaded_irq() 时,主处理函数返回此值
性能提示: 网络驱动的中断处理函数应尽可能轻量。典型做法是:在中断处理函数中读取状态寄存器、调度 NAPI(napi_schedule())、清除中断状态位,然后返回 IRQ_HANDLED。实际的数据包接收和协议栈处理在 NAPI 轮询上下文中完成。

3.7 中断注册的常见错误与调试

  • IRQ 号无效: 注册前未正确获取 IRQ 资源,导致传入 0 或负数。使用 platform_get_irq() 时需检查返回值。
  • 共享中断未检查设备状态: 处理函数未读取中断状态寄存器就直接返回 IRQ_HANDLED,导致内核统计异常。
  • 中断标志不匹配: 设备实际使用电平触发但注册时指定边沿触发,导致中断丢失或重复触发。
  • dev 参数重复: 同一 IRQ 上注册多个处理函数时 dev 参数相同,内核无法区分设备。

调试中断注册问题时,/proc/interrupts 是最直接的诊断工具:

# cat /proc/interrupts
           CPU0       CPU1       CPU2       CPU3
 16:        123        456        789        012  IR-IO-APIC   16-fasteoi   eth0
 17:         34         56         78         90  IR-IO-APIC   17-fasteoi   eth1

每一行显示 IRQ 号、各 CPU 上的中断计数、中断控制器信息、触发方式以及注册时传入的设备名称。如果某个设备的中断计数不增长,说明中断注册或硬件配置存在问题。