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 时,内核不会创建中断线程。
顶半部与底半部协作流程
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 中断注册流程总览
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 使能寄存器是否已正确配置。