3. 驱动中断注册接口:request_irq()、request_threaded_irq()、devm_request_irq() 函数原型与参数详解

在 MSI 中断配置流程中,驱动侧的核心任务是将分配到的中断向量(IRQ 号)与具体的中断处理逻辑绑定。Linux 内核提供了三个层次的中断注册接口,分别适用于不同的场景:request_irq() 用于注册标准中断处理函数;request_threaded_irq() 支持将中断处理拆分为快速顶半部和可睡眠的底半部线程;devm_request_irq() 则是资源管理版本,自动在设备移除或驱动卸载时释放中断。

理解这三个函数的原型、参数含义及差异,是正确实现 MSI 中断使能的关键前提。

3.1 核心接口:request_irq()

request_irq() 是最基础的中断注册函数,用于将给定的 IRQ 号与一个中断处理函数(handler)关联。该函数会设置中断描述符、配置触发方式,并将处理函数挂入中断处理链。

函数原型

int request_irq(unsigned int irq,
                irq_handler_t handler,
                unsigned long flags,
                const char *name,
                void *dev);

参数详解

参数 类型 说明
irq unsigned int 要注册的中断号。对于 MSI 中断,该值由 pci_alloc_irq_vectors() 或 pci_irq_vector() 返回。
handler irq_handler_t 中断处理函数指针。原型为 irqreturn_t (*handler)(int irq, void *dev_id)。该函数在中断上下文中执行,必须快速完成,不能睡眠。
flags unsigned long 中断标志位,用于控制触发方式、共享属性等。常用标志见下表。
name const char * 设备名称,用于 /proc/interrupts 中标识中断源。通常设为驱动名称或设备名。
dev void * 传递给中断处理函数的设备标识。对于共享中断,该参数用于区分不同设备;对于 MSI 非共享中断,通常传入设备结构体指针。

常用 flags 标志

标志 值 说明
IRQF_SHARED 0x00000001 允许中断线被多个设备共享。MSI 中断通常不共享,因此一般不需要设置此标志。
IRQF_TRIGGER_NONE 0x00000000 不指定触发方式,由固件或总线决定。MSI 是边沿触发消息,通常不依赖此标志。
IRQF_TRIGGER_RISING 0x00000001 上升沿触发。MSI 消息本质上是边沿触发,但由 PCIe 硬件处理,驱动侧通常不设置此标志。
IRQF_TRIGGER_FALLING 0x00000002 下降沿触发。
IRQF_TRIGGER_HIGH 0x00000004 高电平触发。
IRQF_TRIGGER_LOW 0x00000008 低电平触发。
IRQF_NO_THREAD 0x00000020 禁止将中断线程化。用于必须运行在原子上下文的中断。
IRQF_NO_SUSPEND 0x00000040 系统休眠时不禁用该中断。
MSI 驱动提示: 对于 MSI 中断,flags 通常设置为 0 或 IRQF_NO_THREAD。MSI 中断线是专用的,不需要 IRQF_SHARED;触发方式由 PCIe 消息机制决定,不需要显式指定电平/边沿标志。

返回值

  • 0:注册成功。
  • -EINVAL:参数无效,如 irq 号无效或 handler 为 NULL。
  • -EBUSY:中断号已被占用且未设置 IRQF_SHARED。
  • -ENOMEM:内存分配失败。

3.2 线程化中断:request_threaded_irq()

request_threaded_irq() 是 request_irq() 的扩展版本,允许将中断处理拆分为两个部分:一个快速顶半部(handler)和一个可睡眠的底半部线程(thread_fn)。顶半部在中断上下文中执行,负责处理紧急硬件操作并决定是否需要唤醒底半部;底半部在独立内核线程中运行,可以执行耗时操作、访问睡眠锁、进行 I/O 操作等。

函数原型

int request_threaded_irq(unsigned int irq,
                         irq_handler_t handler,
                         irq_handler_t thread_fn,
                         unsigned long flags,
                         const char *name,
                         void *dev);

参数详解

参数 类型 说明
irq unsigned int 同 request_irq(),MSI 中断号。
handler irq_handler_t 顶半部处理函数。在中断上下文中执行。如果设为 NULL,内核将使用默认的 irq_default_primary_handler(),该函数立即返回 IRQ_WAKE_THREAD,直接唤醒底半部线程。
thread_fn irq_handler_t 底半部线程函数。在进程上下文中执行,可以睡眠。如果 handler 返回 IRQ_WAKE_THREAD,则 thread_fn 被调度执行。
flags unsigned long 同 request_irq()。注意:如果设置了 IRQF_NO_THREAD,则 thread_fn 不会被调用。
name const char * 同 request_irq()。
dev void * 同 request_irq()。
关键区别: request_irq() 本质上是 request_threaded_irq() 的封装,调用 request_irq(irq, handler, flags, name, dev) 等价于调用 request_threaded_irq(irq, handler, NULL, flags, name, dev)。当 thread_fn 为 NULL 时,内核不会创建中断线程。

顶半部与底半部协作流程

request_threaded_irq() 顶半部与底半部协作流程 硬件中断触发 顶半部 handler 执行 中断上下文,快速处理,不能睡眠 返回 IRQ_WAKE_THREAD? 是 底半部 thread_fn 执行 进程上下文,可睡眠,可做 I/O 否 中断处理完成,返回 IRQ_HANDLED handler 为 NULL 时 默认直接唤醒 thread_fn

3.3 资源管理版本:devm_request_irq()

devm_request_irq() 是 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);

参数详解

参数 类型 说明
dev struct device * 请求中断的设备指针。对于 PCIe 设备,通常传入 &pdev->dev。该参数用于关联设备生命周期。
irq unsigned int 同 request_irq()。
handler irq_handler_t 同 request_irq()。
flags unsigned long 同 request_irq()。
name const char * 同 request_irq()。
dev_id void * 同 request_irq() 的 dev 参数。
注意: devm_request_irq() 没有对应的 devm_request_threaded_irq() 版本。如果需要线程化中断且使用资源管理,需要手动调用 devm_add_action() 或 devm_request_irq() 配合 request_threaded_irq() 手动管理。

3.4 三个接口的对比与选择

特性 request_irq() request_threaded_irq() devm_request_irq()
底半部线程支持 否(仅顶半部) 是(顶半部 + 线程底半部) 否(仅顶半部)
自动资源管理 否 否 是
适用场景 快速中断,处理简单 需要睡眠操作的中断 简化 probe/remove 路径
需要手动 free_irq() 是 是 否(自动释放)
MSI 驱动推荐度 中等 高(复杂设备) 高(简化代码)

3.5 典型 MSI 驱动注册示例

以下代码展示了在 PCIe MSI 驱动中,使用 devm_request_irq() 注册中断的典型模式:

static irqreturn_t my_msi_handler(int irq, void *dev_id)
{
    struct my_device *mydev = dev_id;

    /* 读取 MSI 状态寄存器,确认中断来源 */
    u32 status = readl(mydev->regs + MSI_STATUS_REG);

    if (!(status & MY_DEVICE_INT_PENDING))
        return IRQ_NONE;

    /* 清除中断挂起位 */
    writel(status, mydev->regs + MSI_STATUS_REG);

    /* 处理数据(快速操作,不睡眠) */
    mydev->packet_count++;

    return IRQ_HANDLED;
}

static int my_pci_probe(struct pci_dev *pdev,
                        const struct pci_device_id *ent)
{
    struct my_device *mydev;
    int ret, irq;

    /* 分配设备私有结构体 */
    mydev = devm_kzalloc(&pdev->dev, sizeof(*mydev), GFP_KERNEL);
    if (!mydev)
        return -ENOMEM;

    pci_set_drvdata(pdev, mydev);

    /* 启用 MSI 并分配中断向量 */
    ret = pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_MSI);
    if (ret < 0)
        return ret;

    /* 获取第一个 MSI 中断号 */
    irq = pci_irq_vector(pdev, 0);

    /* 注册中断处理函数(资源管理版本) */
    ret = devm_request_irq(&pdev->dev, irq,
                           my_msi_handler,
                           0,           /* flags: MSI 不需要特殊标志 */
                           "my_msi_dev",
                           mydev);      /* dev_id 传入设备结构体 */
    if (ret) {
        dev_err(&pdev->dev, "Failed to request MSI IRQ %d\n", irq);
        return ret;
    }

    /* 启用设备 DMA 和 MSI 消息生成 */
    writel(MY_DEVICE_ENABLE, mydev->regs + MSI_CTRL_REG);

    return 0;
}

/* 注意:remove 函数中不需要调用 free_irq(),
 * devm_request_irq() 会在设备移除时自动释放 */
static void my_pci_remove(struct pci_dev *pdev)
{
    struct my_device *mydev = pci_get_drvdata(pdev);

    /* 禁用设备中断生成 */
    writel(0, mydev->regs + MSI_CTRL_REG);

    /* 释放 MSI 向量 */
    pci_free_irq_vectors(pdev);

    /* devm 资源自动释放,无需手动 free_irq */
}

3.6 中断注册流程总览

MSI 驱动中断注册完整流程 1. 分配 MSI 向量 pci_alloc_irq_vectors() 2. 获取 IRQ 号 pci_irq_vector() 3. 选择注册接口 request_irq / threaded / devm request_irq() 简单快速中断 request_threaded_irq() 需要睡眠操作 devm_request_irq() 自动资源管理 4. 注册成功,使能设备中断 写设备寄存器启用 MSI 消息生成 5. 中断处理函数被调用

3.7 常见错误与调试

常见错误 1:中断号无效
在调用 request_irq() 之前,必须确保 pci_alloc_irq_vectors() 成功返回。如果分配失败,pci_irq_vector() 返回的 IRQ 号可能为 0 或负数,导致 request_irq() 返回 -EINVAL。
常见错误 2:顶半部中执行睡眠操作
request_irq() 和 devm_request_irq() 注册的 handler 运行在中断上下文,不能调用任何可能睡眠的函数(如 kmalloc(GFP_KERNEL)、mutex_lock()、copy_to_user())。如果需要这些操作,必须使用 request_threaded_irq() 将耗时操作放入 thread_fn。
调试技巧: 注册中断后,可以通过 cat /proc/interrupts 查看中断注册情况。MSI 中断会显示为 PCI-MSI 类型,并显示对应的设备名称和触发次数。如果中断未触发,检查设备 MSI 使能寄存器是否已正确配置。