陷阱二:中断上下文休眠陷阱

在中断服务函数(ISR)里调用可能导致休眠的函数,比如 mutex_lockcopy_from_user,这绝对是海思驱动开发中最常见的“自杀式”操作。我见过不少新手,甚至一些老手,一不留神就踩进去。

为什么会这样?说白了,中断上下文不是普通进程上下文。它不能休眠,也不能被调度出去。你一旦在ISR里调用了可能休眠的函数,内核就会直接崩溃,或者死锁,系统卡死。

问题的根源:中断上下文 vs 进程上下文

我们先搞清楚一个基本概念。Linux内核里,代码执行有两种上下文:

  • 进程上下文:可以休眠、可以被调度、可以访问用户空间数据。
  • 中断上下文:不能休眠、不能被调度、不能访问用户空间。

中断上下文里,CPU正在处理硬件中断。这时候你让它去休眠?那谁来响应下一个中断?系统不就卡死了吗?

我刚开始做海思平台驱动时,就犯过这个错。当时在GPIO中断里调了一个 mutex_lock,结果系统直接打印了一堆“scheduling while atomic”的警告,然后死机。嗯,从那以后我再也不敢在ISR里乱调函数了。

哪些函数不能在ISR里调用?

这里我列一个常见“黑名单”,你写代码时一定要避开:

函数类别 具体函数 为什么危险
锁操作 mutex_lock()mutex_lock_interruptible() mutex可能引起休眠等待
内存分配 kmalloc(GFP_KERNEL) GFP_KERNEL可能触发页面回收,导致休眠
用户空间访问 copy_from_user()copy_to_user() 需要访问用户页表,可能触发缺页异常
信号量操作 down()down_interruptible() 信号量获取失败时会休眠
调度相关 schedule()msleep() 显式触发调度或休眠
警告: 即使你觉得某个函数“应该不会休眠”,也请仔细看内核文档。很多函数在特定条件下会休眠,比如 kmalloc 如果内存不足,就会触发页面回收。

正确的做法:中断底半部机制

那ISR里到底该怎么做?我的建议是:ISR只做最紧急、最轻量的事情。比如读取硬件寄存器、清除中断标志、然后触发底半部处理。

海思平台常用的底半部机制有三种:

  • 软中断(SoftIRQ):内核预定义的,比如网络、定时器。驱动一般不直接用它。
  • Tasklet:基于软中断实现,简单易用。但不能休眠。
  • 工作队列(Workqueue):在进程上下文执行,可以休眠。这是最推荐的方式。

我个人习惯用工作队列。因为它运行在进程上下文,你可以放心地调用 mutex_lockcopy_from_user 这些函数。

代码示例:海思平台GPIO中断的正确写法

我们来看一个实际例子。假设海思芯片上有一个GPIO按键,按下时触发中断,我们需要读取按键状态并上报给用户空间。

错误写法(在ISR里休眠):

static irqreturn_t gpio_irq_handler(int irq, void *dev_id)
{
    struct my_device *dev = (struct my_device *)dev_id;
    int val;

    // 错误!mutex_lock可能休眠
    mutex_lock(&dev->lock);
    val = readl(dev->base + GPIO_DATA);
    mutex_unlock(&dev->lock);

    // 错误!copy_to_user可能触发缺页
    copy_to_user(dev->user_buf, &val, sizeof(val));

    return IRQ_HANDLED;
}

这段代码一跑,系统必崩。我当年在海思Hi3519上试过,内核直接打印“BUG: scheduling while atomic”,然后panic。

正确写法(使用工作队列):

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

    // ISR里只做最轻量的事:调度工作队列
    schedule_work(&dev->work);

    return IRQ_HANDLED;
}

static void gpio_work_handler(struct work_struct *work)
{
    struct my_device *dev = container_of(work, struct my_device, work);
    int val;

    // 现在在进程上下文,可以放心休眠
    mutex_lock(&dev->lock);
    val = readl(dev->base + GPIO_DATA);
    mutex_unlock(&dev->lock);

    // 也可以访问用户空间了
    copy_to_user(dev->user_buf, &val, sizeof(val));
}
提示: 海思平台的中断号可能比较多,记得在 request_irq 时指定 IRQF_TRIGGER_RISINGIRQF_TRIGGER_FALLING,别搞反了。我踩过这个坑,按键按下去触发中断,结果松手才触发。

避坑指南

我曾经在调试海思视频编码模块时,遇到一个诡异的问题:中断偶尔丢失。查了两天才发现,是ISR里调了一个自定义函数,那个函数内部用了 spin_lock_bh,而 spin_lock_bh 会关闭底半部,导致其他中断被延迟处理。

所以,这里给你几个实用建议:

  • ISR里只用spinlock:如果必须加锁,用 spin_lock_irqsave,不要用mutex。
  • 避免复杂逻辑:ISR里不要做循环、不要做内存分配、不要调任何可能阻塞的函数。
  • 使用原子操作:如果只是读写一个寄存器,用 readl_relaxedwritel_relaxed,别加锁。
  • 检查内核配置:海思平台有些内核配置会影响中断行为,比如 CONFIG_PREEMPT_RT。记得确认你的内核版本。
核心原则: 中断上下文里,你只有两个选择——要么立即返回,要么调度底半部。没有第三条路。

总结

中断上下文休眠陷阱,说白了就是“在不能睡觉的地方睡觉”。你想想看,CPU正在处理硬件中断,你让它去等一个锁、或者去访问用户空间,这不是找崩溃吗?

我的经验是:写ISR时,先问自己三个问题——这个函数会休眠吗?这个锁会阻塞吗?这个操作需要进程上下文吗?只要有一个答案是“是”,就把它放到工作队列里。

记住,海思芯片的中断处理路径很短,你越轻量,系统越稳定。别让一个ISR毁了整个系统的实时性。