2. 中断基础

在Linux网络驱动中,中断是硬件通知软件“有事发生”的核心机制。对于PHY链路检测而言,链路状态变化(Link Up/Down)正是通过中断信号从PHY芯片传递到CPU,进而触发驱动执行链路重协商、状态更新等操作。理解中断子系统是掌握网络驱动协同工作的前提。

2.1 Linux中断子系统整体架构

Linux中断子系统采用分层设计,从上到下依次为:中断处理函数层、中断分发层、硬件中断控制器层。这种分层使得驱动开发者只需关注上层API,而无需关心底层中断控制器的具体实现。

Linux 中断子系统分层架构 中断处理函数层(Interrupt Handler Layer) request_irq() / request_threaded_irq() / free_irq() 驱动开发者直接调用的API接口 中断分发层(Generic IRQ Layer) irq_chip / irq_domain / 中断号映射管理 将硬件中断号映射到Linux IRQ号,管理中断流控 硬件中断控制器层(Hardware IRQ Controller) GIC(ARM) / IO-APIC(x86) / 内部中断控制器 直接与CPU核交互,响应物理中断信号 网络驱动中断处理函数注册在“中断处理函数层”, 通过 irq_domain 映射到具体硬件中断线

2.2 中断号:从硬件到软件的桥梁

中断号是中断子系统中最核心的标识符。在Linux中,中断号分为两个层次:硬件中断号(HW IRQ Number)和Linux IRQ号(Virtual IRQ Number)。驱动开发中,我们通常操作的是Linux IRQ号。

概念 说明 获取方式
硬件中断号 由中断控制器(如GIC)分配的物理中断线编号,与SoC引脚绑定 查阅芯片手册/设备树
Linux IRQ号 Linux内核抽象出的虚拟中断号,驱动通过此号注册处理函数 platform_get_irq() / irq_of_parse_and_map()
irq_domain 负责将硬件中断号映射到Linux IRQ号的中间层 内核自动管理
实战提示:在设备树中,PHY中断通常通过 interrupts = <GIC_SPI 123 IRQ_TYPE_LEVEL_LOW> 描述。驱动中调用 platform_get_irq(pdev, 0) 即可获得Linux IRQ号,无需手动计算硬件中断号。

2.3 中断处理函数注册:request_irq

request_irq() 是Linux驱动中最经典的中断注册函数,用于注册一个顶半部(Top Half)中断处理函数。顶半部在中断上下文中执行,要求快速、不可睡眠。

int request_irq(unsigned int irq,
                irq_handler_t handler,
                unsigned long flags,
                const char *name,
                void *dev_id);
参数 含义 典型值
irq Linux IRQ号 从 platform_get_irq() 获取
handler 中断处理函数指针,返回 irqreturn_t phy_interrupt_handler
flags 中断触发类型与共享标志 IRQF_TRIGGER_LOW | IRQF_SHARED
name 中断名称,用于 /proc/interrupts 显示 "eth0_phy"
dev_id 设备标识,共享中断时用于区分 指向 net_device 或 phy_device
关键点:顶半部执行期间,当前CPU的中断被禁用(取决于 IRQF_DISABLED 标志)。因此顶半部必须极快完成,通常只做:读取中断状态寄存器、清除中断标志、唤醒底半部(Bottom Half)。

2.4 线程化中断:request_threaded_irq

对于PHY链路检测这类需要执行耗时操作(如MDIO读写、状态机切换)的中断场景,顶半部显然不合适。Linux提供了线程化中断(Threaded IRQ)机制,将中断处理分为:

  • 顶半部(handler):在中断上下文中快速执行,返回 IRQ_WAKE_THREAD 唤醒线程。
  • 线程化处理函数(thread_fn):在进程上下文中执行,可以睡眠、可以持有锁、可以调用 phy_read() 等耗时函数。
int request_threaded_irq(unsigned int irq,
                         irq_handler_t handler,
                         irq_handler_t thread_fn,
                         unsigned long flags,
                         const char *name,
                         void *dev_id);
参数 说明
handler 顶半部函数。若为 NULL,内核使用默认的 irq_default_primary_handler(),直接唤醒线程
thread_fn 线程化处理函数。在进程上下文中运行,可睡眠
线程化中断处理流程(PHY链路检测场景) PHY硬件中断 顶半部 handler 读状态寄存器 返回 IRQ_WAKE_THREAD 内核线程(irq/xxx-phy) 进程上下文,可睡眠 thread_fn:PHY链路状态检测 phy_read() → 状态机切换 → netif_carrier_on/off() 线程化中断避免了在中断上下文中执行MDIO读写, 是PHY链路检测中断处理的标准实践

2.5 request_irq vs request_threaded_irq 对比

对比维度 request_irq request_threaded_irq
执行上下文 中断上下文(不可睡眠) 顶半部:中断上下文;线程化函数:进程上下文
可调用函数 仅限原子操作(如 writel、tasklet_schedule) 线程化函数可调用 phy_read、mutex_lock、msleep
适用场景 简单中断确认、唤醒底半部 PHY链路检测、I2C/SPI设备中断、需要等待硬件的操作
中断延迟 低(直接执行) 略高(需调度内核线程)
注册方式 request_irq(irq, handler, flags, name, dev) request_threaded_irq(irq, handler, thread_fn, flags, name, dev)
常见错误:在 request_irq 注册的顶半部中调用 phy_read() 或 printk() 等可能睡眠的函数,会导致内核崩溃(BUG: scheduling while atomic)。PHY链路检测必须使用线程化中断。

2.6 中断注册实战:PHY中断示例

以下代码展示了在PHY驱动中注册线程化中断的典型模式:

/* PHY中断顶半部:快速确认中断源 */
static irqreturn_t phy_interrupt_handler(int irq, void *dev_id)
{
    struct phy_device *phydev = dev_id;

    /* 读取中断状态寄存器,确认中断是否来自本PHY */
    int irq_status = phy_read(phydev, MII_INT_STATUS);
    if (irq_status & PHY_INT_LINK_CHANGE) {
        /* 清除中断标志 */
        phy_write(phydev, MII_INT_STATUS, irq_status);
        return IRQ_WAKE_THREAD;  /* 唤醒线程化处理 */
    }

    return IRQ_NONE;  /* 非本设备中断 */
}

/* PHY中断线程化处理:执行链路状态检测 */
static irqreturn_t phy_link_change_thread(int irq, void *dev_id)
{
    struct phy_device *phydev = dev_id;

    /* 在进程上下文中读取PHY状态寄存器 */
    int bmsr = phy_read(phydev, MII_BMSR);
    int lpa  = phy_read(phydev, MII_LPA);

    /* 更新链路状态 */
    if (bmsr & BMSR_LSTATUS) {
        netif_carrier_on(phydev->attached_dev);
        phydev->link = 1;
    } else {
        netif_carrier_off(phydev->attached_dev);
        phydev->link = 0;
    }

    /* 触发上层状态机 */
    phy_state_machine(phydev);

    return IRQ_HANDLED;
}

/* 中断注册(在PHY驱动probe函数中调用) */
int phy_request_irq(struct phy_device *phydev)
{
    int ret;

    ret = request_threaded_irq(phydev->irq,
                               phy_interrupt_handler,      /* 顶半部 */
                               phy_link_change_thread,     /* 线程化函数 */
                               IRQF_TRIGGER_LOW | IRQF_SHARED,
                               "phy_link_irq",
                               phydev);
    if (ret)
        dev_err(&phydev->mdio.dev, "Failed to request PHY IRQ\n");

    return ret;
}
最佳实践:在PHY驱动中,建议始终使用 request_threaded_irq 而非 request_irq + tasklet。线程化中断天然支持进程上下文操作,且与内核的 phy_state_machine() 配合更自然,避免了tasklet中无法持有mutex的问题。

2.7 中断释放与注意事项

在驱动卸载或PHY设备移除时,必须释放已注册的中断:

void phy_free_irq(struct phy_device *phydev)
{
    if (phydev->irq >= 0)
        free_irq(phydev->irq, phydev);
}

中断注册与释放的注意事项:

  • 中断号有效性检查:注册前确认 phydev->irq 不为 0 或负数。
  • 共享中断的dev_id:使用 IRQF_SHARED 标志时,dev_id 必须唯一且非NULL,用于 free_irq() 时区分。
  • 中断触发类型匹配:PHY芯片的中断触发类型(电平/边沿)必须与设备树或平台数据一致,否则可能导致中断丢失或风暴。
  • 禁止在顶半部调用phy_read:MDIO总线操作可能睡眠,必须放在线程化函数中。
总结:中断子系统是网络驱动与PHY硬件交互的“第一响应人”。request_threaded_irq 是PHY链路检测场景的标准选择,它将快速中断确认与耗时链路检测分离,既保证了中断响应速度,又避免了原子上下文违规。下一节将深入PHY中断的具体配置与链路状态机协同。