3. 中断处理基础
在嵌入式Linux音频驱动中,DMA中断是数据搬运的“心跳”。每一次DMA传输完成,硬件都会触发一个中断信号,通知CPU“数据已就绪,请处理”。如果中断处理不当,轻则导致音频卡顿(Xrun),重则使整个系统响应迟缓。本章将深入剖析Linux中断子系统的分层架构、上半部与下半部的拆分哲学,以及中断上下文中的编程禁区,为后续DMA中断优化打下坚实基础。
3.1 Linux中断子系统架构
Linux内核将中断处理抽象为三个层次:硬件中断控制器(GIC/INTC)、通用中断框架(Generic IRQ)以及设备驱动中断处理例程。音频DMA中断从外设触发到驱动响应,需要经过以下路径:
中断传递路径: 外设(DMA控制器)→ 中断控制器(GIC)→ CPU异常向量表 → 内核通用中断处理 → 驱动注册的中断处理函数(上半部)→ 调度下半部(Tasklet/Workqueue)
3.2 上半部(Top Half)与下半部(Bottom Half)
中断处理必须“快进快出”。在中断上下文中,CPU默认关闭本地中断(或至少关闭当前IRQ线),如果处理时间过长,会导致其他中断丢失。Linux内核将中断处理拆分为两部分:
| 特性 | 上半部(Top Half) | 下半部(Bottom Half) |
|---|---|---|
| 执行上下文 | 中断上下文(不可睡眠) | 软中断/进程上下文(可睡眠) |
| 中断状态 | 当前IRQ线被屏蔽(或全部中断被屏蔽) | 所有中断已开启 |
| 执行时机 | 硬件中断触发后立即执行 | 上半部返回后,由内核调度执行 |
| 典型任务 | 清除中断标志、拷贝硬件寄存器、记录状态 | 数据拷贝、协议解析、唤醒等待队列 |
| 耗时要求 | 微秒级(必须极短) | 毫秒级(可稍长) |
| 音频DMA场景 | 更新DMA描述符指针、清除中断挂起位 | 拷贝音频数据到环形缓冲区、更新应用程序指针 |
音频驱动最佳实践: 在DMA中断上半部中,只做“最小必要操作”——更新DMA传输描述符、清除中断标志、记录传输状态。将音频数据搬运、指针更新、唤醒等待队列等耗时操作全部推迟到下半部执行。典型的上半部执行时间应控制在10微秒以内。
3.3 下半部机制:Tasklet vs Workqueue
Linux内核提供多种下半部实现机制。在音频驱动中,最常用的是Tasklet和Workqueue。两者各有适用场景:
3.3.1 Tasklet(软中断上下文)
Tasklet基于软中断实现,执行在软中断上下文中。其特点是:
- 不可睡眠: 不能调用任何可能导致睡眠的函数(如
mutex_lock、copy_to_user、kmalloc(GFP_KERNEL))。 - 同一Tasklet串行执行: 同一个Tasklet不会在多个CPU上同时运行,但不同Tasklet可以并行。
- 执行时机: 上半部返回后,在
do_softirq()中立即执行,延迟极低。 - 适用场景: 音频DMA中断中,仅需搬运少量数据、更新指针、无需睡眠的场景。
/* 音频驱动中典型的Tasklet使用示例 */
static void audio_dma_tasklet_handler(unsigned long data)
{
struct audio_dev *dev = (struct audio_dev *)data;
unsigned long flags;
/* 注意:此处不能调用 copy_to_user 或 mutex_lock */
spin_lock_irqsave(&dev->lock, flags);
/* 更新环形缓冲区写指针 */
dev->hw_ptr += dev->period_bytes;
if (dev->hw_ptr >= dev->buffer_size)
dev->hw_ptr -= dev->buffer_size;
spin_unlock_irqrestore(&dev->lock, flags);
/* 唤醒等待数据的用户态进程 */
wake_up(&dev->wait);
}
/* 上半部注册 */
static irqreturn_t audio_dma_irq_handler(int irq, void *dev_id)
{
struct audio_dev *dev = (struct audio_dev *)dev_id;
/* 清除DMA中断标志(硬件相关) */
writel(0x1, dev->regs + DMA_INT_CLR);
/* 调度Tasklet */
tasklet_schedule(&dev->dma_tasklet);
return IRQ_HANDLED;
}
3.3.2 Workqueue(进程上下文)
Workqueue将下半部任务交给内核工作线程执行,运行在进程上下文中:
- 可睡眠: 可以调用
mutex_lock、copy_to_user、kmalloc(GFP_KERNEL)等阻塞函数。 - 并行执行: 多个Work可以在不同CPU上并行运行(默认workqueue池)。
- 执行时机: 由内核工作线程调度,延迟相对Tasklet稍高(但仍在可接受范围)。
- 适用场景: 需要将音频数据拷贝到用户空间、需要获取互斥锁、需要分配内存的场景。
/* 音频驱动中典型的Workqueue使用示例 */
static void audio_dma_work_handler(struct work_struct *work)
{
struct audio_dev *dev = container_of(work, struct audio_dev, dma_work);
/* 进程上下文:可以调用 copy_to_user */
if (copy_to_user(dev->user_buffer, dev->dma_area, dev->period_bytes)) {
dev_err(dev->dev, "Failed to copy audio data to user\n");
return;
}
/* 可以获取互斥锁 */
mutex_lock(&dev->mutex);
dev->appl_ptr += dev->period_bytes;
if (dev->appl_ptr >= dev->buffer_size)
dev->appl_ptr -= dev->buffer_size;
mutex_unlock(&dev->mutex);
/* 通知应用程序数据就绪 */
wake_up(&dev->poll_wait);
}
/* 上半部注册 */
static irqreturn_t audio_dma_irq_handler(int irq, void *dev_id)
{
struct audio_dev *dev = (struct audio_dev *)dev_id;
/* 清除中断标志 */
writel(0x1, dev->regs + DMA_INT_CLR);
/* 调度Workqueue */
schedule_work(&dev->dma_work);
return IRQ_HANDLED;
}
关键警告: 在中断上半部中,绝对不要调用
copy_to_user、kmalloc(GFP_KERNEL)、mutex_lock 等可能导致睡眠的函数。这些函数只能在Workqueue(进程上下文)中使用。违反此规则会导致内核崩溃(Kernel Panic)或死锁。
3.4 中断上下文限制
中断上下文是Linux内核中最严格的执行环境之一。理解其限制,是编写稳定音频驱动的必要条件:
| 限制类别 | 具体规则 | 违反后果 |
|---|---|---|
| 睡眠禁止 | 不能调用任何可能睡眠的函数(schedule()、wait_event()、mutex_lock()) |
内核崩溃(Kernel Panic) |
| 内存分配 | 只能使用 GFP_ATOMIC 标志分配内存 |
睡眠导致死锁或崩溃 |
| 用户空间访问 | 禁止 copy_to_user / copy_from_user |
页面错误导致崩溃 |
| 锁的使用 | 只能使用 spin_lock / spin_lock_irqsave,不能使用 mutex_lock |
睡眠导致死锁 |
| 执行时间 | 必须极短(建议 < 10μs) | 中断延迟增加,音频Xrun |
| 可重入性 | 同一中断可能嵌套(高优先级中断抢占) | 数据竞争、状态不一致 |
3.5 音频DMA中断处理最佳实践
结合以上分析,在嵌入式Linux音频驱动中,DMA中断处理应遵循以下设计原则:
- 上半部最小化: 只做三件事——清除中断标志、读取DMA状态寄存器、调度下半部。所有业务逻辑推迟。
- 下半部选择策略:
- 如果只需要更新内核态环形缓冲区指针 + 唤醒等待队列 → 使用 Tasklet(延迟最低)。
- 如果需要将数据拷贝到用户空间或获取互斥锁 → 使用 Workqueue(可睡眠)。
- 对于高性能音频场景(如48kHz/16位/2通道,周期大小256帧),推荐使用Tasklet,因为Workqueue的调度延迟可能导致Xrun。
- 锁的选用: 上半部使用
spin_lock_irqsave保护共享数据;下半部如果使用Tasklet,同样使用spin_lock;如果使用Workqueue,可以使用mutex_lock。 - 测量与验证: 使用
ftrace或tracepoint测量中断处理时间,确保上半部 < 10μs,下半部 < 100μs。
性能指标参考: 在ARM Cortex-A7 @ 1GHz平台上,一个优化良好的音频DMA中断处理流程:上半部约 3-5μs,Tasklet下半部约 20-40μs,Workqueue下半部约 50-100μs(包含调度延迟)。如果上半部超过 15μs,就需要考虑进一步拆分或优化。