中断注册:Linux内核中断申请与释放
中断注册,说白了就是告诉内核:「这个硬件信号来了,请帮我调用这个函数」。听起来简单,但里面坑不少。我刚开始做驱动时,就因为在中断注册上马虎,导致系统莫名其妙死机,查了整整两天。
今天咱们就把 request_irq 和 devm_request_irq 这两个函数彻底讲透。你想想看,中断注册要是没搞对,后面的DMA优化全是白搭。
1. 中断注册的核心API
Linux内核提供了两套中断注册接口。一套是传统的 request_irq,另一套是带设备资源管理的 devm_request_irq。我个人习惯在新驱动里用后者,省心。
1.1 request_irq 函数原型
int request_irq(unsigned int irq,
irq_handler_t handler,
unsigned long flags,
const char *name,
void *dev);
参数说明:
| 参数 | 含义 | 注意事项 |
|---|---|---|
| irq | 中断号 | 来自设备树或平台数据 |
| handler | 中断处理函数 | 不能是阻塞函数 |
| flags | 中断标志 | 触发方式、共享等 |
| name | 设备名称 | /proc/interrupts显示 |
| dev | 设备ID | 共享中断时区分 |
1.2 devm_request_irq 的优势
devm_request_irq 是资源管理版。它会在设备驱动卸载时自动释放中断。说白了,你忘了调 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);
区别就一个:多了一个 struct device *dev 参数。内核通过这个设备指针管理中断生命周期。
devm_request_irq。除非你明确需要手动控制中断释放时机,比如热插拔场景。
2. 中断标志 flags 详解
flags 参数决定了中断的行为。我见过太多人随便传个0,结果中断触发方式不对,系统负载飙升。
| 标志 | 含义 | 适用场景 |
|---|---|---|
| IRQF_TRIGGER_RISING | 上升沿触发 | GPIO按键、边缘敏感设备 |
| IRQF_TRIGGER_FALLING | 下降沿触发 | 同上,极性相反 |
| IRQF_TRIGGER_HIGH | 高电平触发 | 电平敏感设备 |
| IRQF_TRIGGER_LOW | 低电平触发 | 同上,极性相反 |
| IRQF_SHARED | 共享中断 | 多个设备共用中断线 |
| IRQF_ONESHOT | 一次性触发 | 中断线程化时使用 |
关键点: 汽车电子中,大部分传感器中断使用电平触发。因为边沿触发容易受噪声干扰,产生误触发。我在做车载摄像头驱动时,就因为这个吃了大亏。
3. 中断处理函数编写规范
中断处理函数分两种:上半部(硬中断)和下半部(软中断/工作队列)。这里先讲上半部的硬性要求。
static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
struct my_device *dev = (struct my_device *)dev_id;
/* 读取中断状态寄存器 */
u32 status = readl(dev->base + IRQ_STATUS);
/* 清除中断标志 */
writel(status, dev->base + IRQ_CLEAR);
/* 处理数据... 但要快! */
return IRQ_HANDLED;
}
嗯,这里要注意几点:
- 不能睡眠 — 不能调
msleep、wait_event、mutex_lock - 不能调
printk— 至少不能频繁调,会死锁 - 执行时间要短 — 一般不超过100微秒
- 返回值要正确 — 处理了返回
IRQ_HANDLED,否则返回IRQ_NONE
kmalloc(GFP_KERNEL)。GFP_KERNEL 可能触发内存回收,导致睡眠。系统直接报「BUG: scheduling while atomic」。从那以后,我所有中断里只用 GFP_ATOMIC。
4. 中断释放:free_irq 的正确姿势
有申请就有释放。释放中断用 free_irq:
void free_irq(unsigned int irq, void *dev_id);
释放时要注意:
- 确保中断处理函数已经退出 — 调
free_irq会等待正在执行的中断处理完成 - 共享中断必须传正确的 dev_id — 否则释放失败
- 释放顺序 — 先释放中断,再释放设备内存
devm_free_irq(如果用 devm 版本),或者手动调 free_irq。然后才释放 DMA 缓冲区和寄存器映射。
5. 中断注册的完整流程
咱们用一张图把整个流程串起来:
6. 实战:CAN控制器中断注册
咱们以汽车上最常见的CAN控制器为例,写一个完整的中断注册代码:
static int can_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct can_priv *priv;
int irq, ret;
/* 分配设备私有数据 */
priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
/* 获取中断号 */
irq = platform_get_irq(pdev, 0);
if (irq < 0)
return irq;
/* 映射寄存器地址 */
priv->base = devm_platform_ioremap_resource(pdev, 0);
if (IS_ERR(priv->base))
return PTR_ERR(priv->base);
/* 注册中断 - 使用devm版本 */
ret = devm_request_irq(dev, irq, can_irq_handler,
IRQF_TRIGGER_HIGH | IRQF_ONESHOT,
"my_can", priv);
if (ret) {
dev_err(dev, "中断注册失败: %d\n", ret);
return ret;
}
platform_set_drvdata(pdev, priv);
return 0;
}
static irqreturn_t can_irq_handler(int irq, void *dev_id)
{
struct can_priv *priv = dev_id;
u32 status;
/* 读取中断状态 */
status = readl(priv->base + CAN_IRQ_REG);
/* 清除中断 */
writel(status, priv->base + CAN_IRQ_CLR);
/* 处理接收/发送完成中断 */
if (status & CAN_IRQ_RX)
can_rx_handler(priv);
if (status & CAN_IRQ_TX)
can_tx_handler(priv);
return IRQ_HANDLED;
}
注意: 这里用了 IRQF_ONESHOT 标志。为什么?因为CAN中断处理需要线程化,而 IRQF_ONESHOT 能保证中断线在处理完成前不会被再次触发。我在做车载网关项目时,没加这个标志,结果中断风暴直接把CPU干到100%。
7. 调试技巧:查看中断注册状态
中断注册完了,怎么确认它生效了?Linux提供了几个调试接口:
- /proc/interrupts — 查看所有中断的触发次数和注册信息
- /proc/irq/{irq}/ — 每个中断的详细配置目录
- cat /proc/interrupts | grep my_can — 快速过滤你的设备
输出示例:
CPU0 CPU1
53: 1234 5678 GIC-0 53 my_can
看到这个输出,说明中断注册成功了。如果数字一直不涨,嗯,那说明中断根本没触发。这时候就要检查硬件连接和设备树配置了。
atomic_inc 记录触发次数。最后在驱动卸载时打印出来。这样能快速定位问题。
8. 常见错误与避坑指南
| 错误 | 现象 | 解决方案 |
|---|---|---|
| 中断号错误 | request_irq 返回 -EINVAL | 检查设备树和 platform_get_irq |
| 中断处理函数睡眠 | BUG: scheduling while atomic | 检查所有调用的函数 |
| 共享中断 dev_id 传 NULL | 中断无法区分来源 | 传设备私有数据结构指针 |
| 忘记清除中断标志 | 中断风暴,CPU 100% | 处理函数第一件事就是清中断 |
| 中断释放顺序错误 | 访问已释放的内存 | 先 free_irq,再释放资源 |
printk 来调试。结果中断频率太高,printk 把系统锁死了。从那以后,我调试中断只用 trace_printk 或者直接写内存。