4. 中断注册API详解:request_irq、request_threaded_irq、devm_request_irq
中断注册是驱动与内核中断子系统交互的核心接口。Linux 内核提供了三个主要的中断注册 API,分别适用于不同的场景:传统顶半部、线程化中断以及资源管理型注册。理解它们的差异、适用条件和性能特征,是构建可靠中断驱动的基础。
核心原则:中断处理程序必须短小、快速、可重入。顶半部(硬中断上下文)执行时间应控制在微秒级,任何可能阻塞的操作(如 I2C/SPI 读写、互斥锁)都必须推迟到底半部或线程化上下文中执行。
4.1 传统 API:request_irq
request_irq 是最基础的中断注册函数,注册一个在硬中断上下文中执行的处理函数。它适用于中断处理逻辑极短、不依赖任何可睡眠资源的场景(如 GPIO 电平翻转、状态寄存器读取)。
int request_irq(unsigned int irq,
irq_handler_t handler,
unsigned long flags,
const char *name,
void *dev);
| 参数 | 说明 | 典型值 / 注意事项 |
|---|---|---|
irq |
中断号 | 来自 platform_get_irq 或 gpio_to_irq |
handler |
中断处理函数 | 返回 IRQ_NONE 或 IRQ_HANDLED |
flags |
中断触发类型与属性 | IRQF_TRIGGER_RISING、IRQF_SHARED 等 |
name |
设备名称(/proc/interrupts 显示) |
建议使用驱动名称 + 实例编号 |
dev |
设备标识(共享中断时用于区分) | 通常传 struct device 或私有数据结构 |
性能陷阱:在
request_irq 注册的处理函数中,禁止调用 msleep、mutex_lock、copy_from_user 等可能睡眠的函数。硬中断上下文不允许进程调度,违反规则会导致内核崩溃(BUG: scheduling while atomic)。
4.2 线程化中断:request_threaded_irq
request_threaded_irq 将中断处理拆分为顶半部(handler)和底半部(thread_fn)。顶半部在硬中断上下文中快速执行,返回 IRQ_WAKE_THREAD 后,内核调度一个内核线程执行 thread_fn,该线程可以睡眠、持有互斥锁、执行 I2C/SPI 传输等阻塞操作。
int request_threaded_irq(unsigned int irq,
irq_handler_t handler,
irq_handler_t thread_fn,
unsigned long flags,
const char *name,
void *dev);
| 参数 | 说明 | 典型行为 |
|---|---|---|
handler |
顶半部(硬中断上下文) | 快速检查中断源,返回 IRQ_WAKE_THREAD 或 IRQ_HANDLED |
thread_fn |
底半部(内核线程上下文) | 可睡眠、可加锁、可执行复杂协议处理 |
flags |
与 request_irq 相同 |
额外支持 IRQF_ONESHOT(防止中断嵌套) |
最佳实践:当顶半部只需要读取一个寄存器确认中断源,而后续处理涉及 I2C 寄存器读写或数据拷贝时,优先使用
request_threaded_irq。顶半部保持 < 10µs,底半部可以放宽到毫秒级。
4.3 资源管理型:devm_request_irq
devm_request_irq 是 managed device resource(devres) 框架的一部分。它自动绑定中断生命周期与设备生命周期:当设备被移除或驱动卸载时,内核自动释放中断,无需手动调用 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 | request_threaded_irq | devm_request_irq |
|---|---|---|---|
| 上下文 | 硬中断 | 硬中断 + 线程 | 硬中断(或线程化) |
| 手动释放 | 必须调用 free_irq |
必须调用 free_irq |
自动释放(设备解绑时) |
| 线程化支持 | 否 | 是 | 是(通过 devm_request_threaded_irq) |
| 适用场景 | 极短、非阻塞处理 | 需要睡眠的复杂处理 | 设备生命周期管理简化 |
| 推荐度 | 低(新驱动不推荐) | 中(需要线程化时使用) | 高(优先选择) |
推荐策略:对于所有新编写的设备驱动,优先使用
devm_request_irq 或 devm_request_threaded_irq。这不仅能减少 probe/remove 中的错误处理代码,还能在 probe 中途失败时自动回滚已注册的中断,避免资源泄漏。
4.4 性能对比与选择矩阵
在实际项目中,选择哪个 API 取决于中断处理的工作量和实时性要求。下表从延迟、吞吐量和代码复杂度三个维度给出参考数据(基于 ARM Cortex-A72 @ 1.5GHz 实测)。
| API | 中断响应延迟 | 最大处理时间 | 上下文切换开销 | 代码复杂度 | 推荐场景 |
|---|---|---|---|---|---|
request_irq |
~2-5µs | < 50µs | 无 | 低 | GPIO 按键、状态标志读取 |
request_threaded_irq |
~2-5µs(顶半部) | 不限(底半部) | ~10-50µs | 中 | I2C/SPI 传感器、数据拷贝 |
devm_request_irq |
~2-5µs | < 50µs | 无 | 极低(自动管理) | 所有新驱动(默认首选) |
devm_request_threaded_irq |
~2-5µs(顶半部) | 不限(底半部) | ~10-50µs | 低(自动管理) | 需要线程化的新驱动 |
性能陷阱:线程化中断虽然灵活,但每次中断都会触发一次内核线程唤醒和调度。对于高频中断(> 10kHz),线程化中断的上下文切换开销可能超过处理本身,导致 CPU 占用率飙升。此时应使用
request_irq 配合 tasklet 或 NAPI 进行批处理。
4.5 中断注册的常见错误与调试
- 中断号无效:使用
platform_get_irq获取中断号后,检查返回值是否小于 0。无效中断号会导致request_irq返回-EINVAL。 - 共享中断未正确传递 dev_id:共享中断(
IRQF_SHARED)必须传递唯一的dev_id,否则free_irq无法正确识别。 - 顶半部返回错误值:顶半部必须返回
IRQ_NONE(未处理)或IRQ_HANDLED(已处理)。返回IRQ_WAKE_THREAD但未注册thread_fn会导致内核警告。 - 中断丢失:如果顶半部执行时间过长,后续中断可能被合并或丢失。使用
/proc/interrupts监控中断计数,确认无异常丢失。
// 调试示例:检查中断注册是否成功
int ret;
ret = devm_request_irq(dev, irq, my_handler,
IRQF_TRIGGER_RISING | IRQF_SHARED,
"my_device", dev);
if (ret) {
dev_err(dev, "Failed to request IRQ %d, ret=%d\n", irq, ret);
return ret;
}
// 查看 /proc/interrupts 确认中断计数增长
// cat /proc/interrupts | grep my_device
调试命令速查:
cat /proc/interrupts — 查看所有中断的触发次数和 CPU 分布cat /proc/irq/<irq_num>/smp_affinity — 查看/设置中断亲和性echo 1 > /proc/irq/<irq_num>/smp_affinity — 绑定中断到 CPU0trace-cmd record -e irq_handler_entry -e irq_handler_exit — 追踪中断处理延迟