4. 中断请求(request_irq)流程:源码分析与注册机制

中断处理程序的注册是中断子系统的核心入口。Linux 内核通过 request_irq() 函数将设备驱动的中断处理逻辑与特定的中断号绑定。本章节深入分析 request_irq() 的源码执行路径、中断号的分配策略以及处理函数的注册细节,帮助理解中断负载均衡的底层基础。

4.1 request_irq 函数原型与参数

request_irq() 是驱动开发者最常用的中断注册接口,定义在 include/linux/interrupt.h 中。其函数原型如下:

static inline int __must_check
request_irq(unsigned int irq, irq_handler_t handler,
            unsigned long flags, const char *name, void *dev)
{
    return request_threaded_irq(irq, handler, NULL, flags, name, dev);
}

参数说明:

参数 类型 说明
irq unsigned int 中断号,标识硬件中断源
handler irq_handler_t 中断处理函数指针,类型为 irqreturn_t (*)(int, void *)
flags unsigned long 中断标志位,控制触发方式、共享属性等
name const char * 设备名称,用于 /proc/interrupts 显示
dev void * 设备标识,共享中断时用于区分设备
关键点:request_irq() 实际上是 request_threaded_irq() 的封装,将线程化处理函数设为 NULL,表示使用传统的中断上下文处理方式。若需要线程化中断,可直接调用 request_threaded_irq()。

4.2 request_threaded_irq 核心流程

request_threaded_irq() 是中断注册的真正入口,位于 kernel/irq/manage.c。其执行流程可分为以下关键步骤:

  1. 参数校验与权限检查:验证中断号有效性、处理函数非空、标志位合法性。
  2. 中断描述符获取:通过 irq_to_desc(irq) 获取 struct irq_desc,该结构体是中断管理的核心数据结构。
  3. 中断号分配确认:若中断号尚未分配 struct irq_desc,则调用 irq_alloc_desc_at() 动态分配。
  4. 中断触发类型设置:根据 flags 中的 IRQF_TRIGGER_* 位设置硬件触发方式。
  5. 处理函数注册:将 handler 和 dev_id 封装为 struct irqaction,挂入 irq_desc 的 action 链表。
  6. 中断控制器使能:调用 irq_startup() 或 __irq_set_handler() 激活中断线。
性能关注点:步骤 5 中,若中断号已存在其他处理函数(共享中断),新 irqaction 会被添加到链表尾部。中断触发时,内核会遍历整个链表,逐个调用处理函数直到某个函数返回 IRQ_HANDLED。共享中断过多会显著增加中断延迟。
request_threaded_irq 核心执行流程 入口:request_threaded_irq 参数校验与权限检查 获取 irq_desc 描述符 中断号分配(若未分配) 设置触发类型(flags) 注册 irqaction 到链表 中断控制器使能(irq_startup)

4.3 中断号分配机制

中断号是连接硬件中断源与软件处理函数的桥梁。Linux 内核支持两种中断号分配方式:

分配方式 函数 适用场景 特点
静态分配 irq_alloc_desc_at() 传统 x86 PIC、固定中断号设备 中断号预先定义,不可动态更改
动态分配 irq_alloc_desc_from() PCIe MSI/MSI-X、平台设备 中断号由内核自动分配,避免冲突
层次化分配 irq_domain_alloc_irqs() ARM GIC、RISC-V PLIC 通过 irq_domain 映射硬件中断到 Linux 中断号
负载均衡影响:动态分配的中断号通常与 CPU 亲和性设置相关。在 SMP 系统中,irq_alloc_desc_from() 会优先选择当前 CPU 所在节点(NUMA)的中断描述符,这为后续的 irqbalance 提供了基础。

4.4 irqaction 结构体与处理函数注册

每个中断处理函数被封装为 struct irqaction,其核心字段如下:

struct irqaction {
    irq_handler_t       handler;      // 中断处理函数
    unsigned long       flags;        // 中断标志
    void                *dev_id;      // 设备标识
    struct irqaction    *next;        // 链表指针(共享中断)
    int                 irq;          // 中断号
    irq_handler_t       thread_fn;    // 线程化处理函数
    struct task_struct  *thread;      // 内核线程
    struct irqaction    *secondary;   // 辅助处理
    // ... 其他字段
};

注册过程的核心操作是将 irqaction 插入到 irq_desc 的 action 链表:

// kernel/irq/manage.c 中 setup_irq() 的核心逻辑
static int
setup_irq(unsigned int irq, struct irqaction *new)
{
    struct irq_desc *desc = irq_to_desc(irq);
    struct irqaction *old, **old_ptr;
    unsigned long flags;

    raw_spin_lock_irqsave(&desc->lock, flags);
    old_ptr = &desc->action;
    old = *old_ptr;
    if (old) {
        // 共享中断:遍历到链表尾部
        while (old->next)
            old = old->next;
        old_ptr = &old->next;
    }
    // 插入新 irqaction
    *old_ptr = new;
    raw_spin_unlock_irqrestore(&desc->lock, flags);
    return 0;
}
性能分析:共享中断链表的遍历发生在中断上下文中。当 irqaction 数量超过 3-4 个时,遍历开销会显著增加中断响应延迟。在负载均衡设计中,应尽量避免非必要的共享中断,或使用 MSI-X 为每个设备分配独立中断号。
irq_desc 与 irqaction 链表结构 struct irq_desc irq = 16 action irqaction 1 handler = func_a next irqaction 2 handler = func_b next irqaction 3 handler = func_c NULL 中断触发时,内核从 action 链表头开始遍历 依次调用 handler,直到返回 IRQ_HANDLED

4.5 中断标志位 flags 详解

flags 参数控制中断的行为特性,常用的标志位如下:

标志位 值 说明 负载均衡影响
IRQF_SHARED 0x00000001 允许共享中断线 增加链表遍历开销,降低确定性
IRQF_TRIGGER_RISING 0x00000002 上升沿触发 边缘触发易丢失,不适合负载均衡
IRQF_TRIGGER_FALLING 0x00000004 下降沿触发 同上
IRQF_TRIGGER_HIGH 0x00000008 高电平触发 电平触发可共享,适合多设备
IRQF_TRIGGER_LOW 0x00000010 低电平触发 同上
IRQF_PERCPU 0x00000020 中断仅在本 CPU 处理 不参与全局负载均衡
IRQF_NOBALANCING 0x00000080 禁止 irqbalance 迁移 固定 CPU 亲和性,绕过均衡器
IRQF_NO_THREAD 0x00010000 禁止线程化处理 保持硬中断上下文,降低延迟
最佳实践:对于高性能设备(如网卡、NVMe SSD),建议使用 IRQF_NOBALANCING 结合手动设置 CPU 亲和性,避免 irqbalance 在运行时动态迁移中断导致缓存抖动。对于普通设备,保留默认标志位让 irqbalance 自动优化。

4.6 中断处理函数的执行上下文

注册后的中断处理函数在以下上下文中执行:

  • 硬中断上下文(Hard IRQ Context):handler 直接执行,禁止睡眠,禁止持有锁(除非使用 spin_lock)。执行时间应极短(通常 < 100μs)。
  • 软中断上下文(SoftIRQ Context):硬中断返回后触发,用于处理耗时较长的下半部(如网络协议栈)。
  • 线程化中断上下文(Threaded IRQ Context):通过 request_threaded_irq() 的 thread_fn 参数注册,运行在内核线程中,允许睡眠,适合复杂设备驱动。
// 示例:注册线程化中断
int dev_probe(struct platform_device *pdev)
{
    int irq = platform_get_irq(pdev, 0);
    return request_threaded_irq(irq,
                                hard_irq_handler,   // 硬中断:快速处理,返回 IRQ_WAKE_THREAD
                                thread_irq_handler, // 线程化:处理耗时操作
                                IRQF_TRIGGER_HIGH | IRQF_ONESHOT,
                                "my_device", dev);
}
延迟分析:线程化中断将处理函数从硬中断上下文迁移到内核线程,虽然增加了调度延迟(通常 10-100μs),但避免了硬中断长时间占用 CPU,提高了系统整体的实时性和可预测性。在负载均衡场景中,线程化中断可以像普通进程一样被调度器迁移到不同 CPU。

4.7 中断注册的返回值与错误处理

request_irq() 的返回值需要驱动开发者仔细检查:

返回值 含义 常见原因
0 注册成功 -
-EINVAL 参数无效 中断号超出范围、handler 为 NULL、flags 非法
-EBUSY 中断号已被占用且不允许共享 未设置 IRQF_SHARED 但中断线已被注册
-ENOMEM 内存分配失败 无法分配 irqaction 或 irq_desc
调试建议:当 request_irq() 返回 -EBUSY 时,可通过 cat /proc/interrupts 查看当前中断号的使用情况,确认是否与其他设备冲突。对于 PCIe 设备,可使用 lspci -vvv 查看 MSI/MSI-X 中断分配状态。

通过以上分析,可以看到 request_irq() 不仅仅是简单的函数注册,它涉及中断描述符管理、链表操作、硬件触发配置等多个子系统。理解这些底层细节,是后续进行中断负载均衡优化和调度延迟分析的基础。