4. 中断处理函数编写:中断上下文限制、快速响应原则、共享中断处理

好,咱们今天聊聊中断处理函数的编写。这玩意儿,说简单也简单,说难也难。我见过不少新手,甚至有些老手,都在这里栽过跟头。说白了,中断处理函数就是在“刀尖上跳舞”,你得时刻清楚自己身在何处,能做什么,不能做什么。

4.1 中断上下文:一个“不自由”的世界

先说说“中断上下文”这个概念。你想想看,当CPU正在高高兴兴地跑你的应用程序,突然一个中断来了,CPU就得立刻停下手中的活,跳去执行你的中断处理函数。这时候,CPU处于一种特殊的状态——中断上下文。

在这个状态下,有哪些限制呢?我列一下:

  • 不能睡眠:这是最核心的一条。在中断上下文里,你不能调用任何可能导致睡眠的函数。比如 kmalloc(GFP_KERNEL)mutex_lock()copy_from_user() 这些,统统不能用。为什么?因为中断上下文没有进程调度,你一睡,就没人能把你唤醒了。
  • 不能访问用户空间:中断处理函数运行在内核空间,没有进程的页表上下文。你没法直接访问用户空间的地址。我曾经有个同事,在中断里直接解引用用户空间指针,结果系统直接oops了。
  • 不能持有信号量:信号量可能导致睡眠,所以也不行。但自旋锁可以用,不过要小心,持有自旋锁的时间一定要短。
  • 不能做复杂运算:中断处理函数应该像“闪电战”一样,快进快出。浮点运算、大循环、复杂的算法,都不适合在这里做。

核心原则:中断上下文里,你唯一能信任的就是当前CPU的寄存器。其他一切,都要小心再小心。

4.2 快速响应原则:别让CPU等你

中断处理函数的核心要求就一个字:。为什么?因为中断是异步事件,CPU响应中断时,会屏蔽同级和更低级的中断。如果你在中断处理函数里磨磨蹭蹭,其他中断就进不来,系统的实时性就完蛋了。

我个人习惯把中断处理函数分成两部分:

  1. 上半部(Top Half):就是注册的那个中断处理函数。只做最紧急的事,比如:
    • 清除硬件中断标志
    • 从硬件寄存器读取数据
    • 唤醒一个内核线程(下半部)
  2. 下半部(Bottom Half):把耗时的、不紧急的工作放到这里做。比如:
    • 处理读取到的数据
    • 唤醒等待队列中的进程
    • 执行协议栈处理

嗯,这里要注意:上半部里绝对不要做任何可能阻塞的操作。我见过有人在上半部里打印调试信息,结果串口输出太慢,直接把系统卡死了。

我的经验:一个合格的中断处理函数,执行时间应该控制在微秒级别。如果你发现中断处理函数执行时间超过了几十微秒,就该考虑用下半部了。

4.3 共享中断处理:大家一起用

在嵌入式系统里,硬件资源有限,多个设备共享一个中断线的情况很常见。比如GPIO中断,多个引脚可能共用一个中断号。这时候,你的中断处理函数就得学会“认人”。

注册共享中断时,要加上 IRQF_SHARED 标志。内核在调用你的处理函数前,会先检查中断状态寄存器,确认是不是你的设备触发了中断。如果不是,赶紧返回 IRQ_NONE,让内核去调用下一个处理函数。

看个例子:

static irqreturn_t my_shared_handler(int irq, void *dev_id)
{
    struct my_device *dev = (struct my_device *)dev_id;
    u32 status;

    // 读取硬件状态寄存器,确认是不是我的设备
    status = readl(dev->base + STATUS_REG);
    if (!(status & MY_DEVICE_IRQ_PENDING)) {
        // 不是我的中断,赶紧走人
        return IRQ_NONE;
    }

    // 处理中断
    // ... 快速处理 ...

    // 清除中断标志
    writel(MY_DEVICE_IRQ_CLEAR, dev->base + CLEAR_REG);

    return IRQ_HANDLED;
}

// 注册时
ret = request_irq(irq_num, my_shared_handler, IRQF_SHARED,
                  "my_device", my_device);

这里有个坑,我曾经踩过:共享中断里,dev_id 参数不能为NULL。因为内核要靠这个参数来区分不同的设备。如果你传了NULL,卸载驱动时 free_irq() 就找不到对应的处理函数了。

警告:共享中断的处理函数,一定要先检查中断源。如果直接返回 IRQ_HANDLED,会导致其他设备的中断丢失。我曾经因为这个bug,排查了整整两天。

4.4 实战中的避坑指南

最后,分享几个我这些年积累的经验:

  • 中断嵌套要小心:虽然Linux内核支持中断嵌套,但我不建议你用。嵌套中断容易导致栈溢出,而且调试起来非常痛苦。
  • local_irq_save() 保护临界区:如果你在进程上下文和中断上下文共享数据,记得在进程上下文里关中断再访问。否则,数据竞争会让你欲哭无泪。
  • 中断处理函数里别用 printk():除非你是在调试初期,而且知道自己在做什么。printk() 太慢了,会严重影响中断响应时间。
  • 善用 dev_id:把设备结构体指针传给中断处理函数,这样你就能在中断里访问设备寄存器、私有数据了。比用全局变量优雅得多。

我曾经在一个项目中,因为中断处理函数里调用了 msleep(),导致系统在特定条件下死锁。嗯,从那以后,我每次写中断处理函数,都会在心里默念三遍:“不能睡眠,不能睡眠,不能睡眠。”

中断处理函数核心逻辑流程图 硬件中断发生 是否共享中断? 检查中断源 快速处理中断 需要下半部? 触发下半部 返回 IRQ_HANDLED 中断处理结束

好了,中断处理函数的编写,核心就是这些。记住:快、准、稳。快是响应要快,准是中断源要认准,稳是代码要稳定可靠。做到这三点,你的中断处理函数就合格了。

总结一下:中断上下文里,别睡觉,别磨蹭,别认错人。做到这三点,你就是中断处理的高手了。

专注资料整理