陷阱二:中断上下文休眠陷阱
在中断服务函数(ISR)里调用可能导致休眠的函数,比如 mutex_lock、copy_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_lock、copy_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_RISING 或 IRQF_TRIGGER_FALLING,别搞反了。我踩过这个坑,按键按下去触发中断,结果松手才触发。
避坑指南
我曾经在调试海思视频编码模块时,遇到一个诡异的问题:中断偶尔丢失。查了两天才发现,是ISR里调了一个自定义函数,那个函数内部用了 spin_lock_bh,而 spin_lock_bh 会关闭底半部,导致其他中断被延迟处理。
所以,这里给你几个实用建议:
- ISR里只用spinlock:如果必须加锁,用
spin_lock_irqsave,不要用mutex。 - 避免复杂逻辑:ISR里不要做循环、不要做内存分配、不要调任何可能阻塞的函数。
- 使用原子操作:如果只是读写一个寄存器,用
readl_relaxed或writel_relaxed,别加锁。 - 检查内核配置:海思平台有些内核配置会影响中断行为,比如
CONFIG_PREEMPT_RT。记得确认你的内核版本。
总结
中断上下文休眠陷阱,说白了就是“在不能睡觉的地方睡觉”。你想想看,CPU正在处理硬件中断,你让它去等一个锁、或者去访问用户空间,这不是找崩溃吗?
我的经验是:写ISR时,先问自己三个问题——这个函数会休眠吗?这个锁会阻塞吗?这个操作需要进程上下文吗?只要有一个答案是“是”,就把它放到工作队列里。
记住,海思芯片的中断处理路径很短,你越轻量,系统越稳定。别让一个ISR毁了整个系统的实时性。