中断注册: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 共享中断时区分
我曾经踩过的坑: 共享中断时,dev参数传NULL。结果多个设备共享同一个中断号,中断来了根本不知道是谁触发的。系统日志里全是「unexpected IRQ」的警告。

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. 中断注册的完整流程

咱们用一张图把整个流程串起来:

中断注册与释放完整流程 获取中断号 request_irq/devm_request_irq 注册成功 中断处理函数 驱动卸载 free_irq/devm_free_irq 关键说明 中断号来自设备树 或 platform_get_irq devm版本自动释放 手动版本需配对调用 处理函数要求 不能睡眠 执行时间 < 100us 返回 IRQ_HANDLED

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

看到这个输出,说明中断注册成功了。如果数字一直不涨,嗯,那说明中断根本没触发。这时候就要检查硬件连接和设备树配置了。

我的调试习惯: 先 cat /proc/interrupts 看中断号对不对。然后在中断处理函数里加个计数器,用 atomic_inc 记录触发次数。最后在驱动卸载时打印出来。这样能快速定位问题。

8. 常见错误与避坑指南

错误 现象 解决方案
中断号错误 request_irq 返回 -EINVAL 检查设备树和 platform_get_irq
中断处理函数睡眠 BUG: scheduling while atomic 检查所有调用的函数
共享中断 dev_id 传 NULL 中断无法区分来源 传设备私有数据结构指针
忘记清除中断标志 中断风暴,CPU 100% 处理函数第一件事就是清中断
中断释放顺序错误 访问已释放的内存 先 free_irq,再释放资源
我曾经最惨的一次: 在中断处理函数里调了 printk 来调试。结果中断频率太高,printk 把系统锁死了。从那以后,我调试中断只用 trace_printk 或者直接写内存。