3. 中断处理基础

在嵌入式Linux音频驱动中,DMA中断是数据搬运的“心跳”。每一次DMA传输完成,硬件都会触发一个中断信号,通知CPU“数据已就绪,请处理”。如果中断处理不当,轻则导致音频卡顿(Xrun),重则使整个系统响应迟缓。本章将深入剖析Linux中断子系统的分层架构、上半部与下半部的拆分哲学,以及中断上下文中的编程禁区,为后续DMA中断优化打下坚实基础。

3.1 Linux中断子系统架构

Linux内核将中断处理抽象为三个层次:硬件中断控制器(GIC/INTC)、通用中断框架(Generic IRQ)以及设备驱动中断处理例程。音频DMA中断从外设触发到驱动响应,需要经过以下路径:

中断传递路径: 外设(DMA控制器)→ 中断控制器(GIC)→ CPU异常向量表 → 内核通用中断处理 → 驱动注册的中断处理函数(上半部)→ 调度下半部(Tasklet/Workqueue)
Linux中断子系统分层架构 DMA控制器(外设) 中断控制器(GIC/INTC) CPU异常向量表 硬件信号 IRQ 通用中断处理(Generic IRQ) 中断描述符(irq_desc) 中断控制器 驱动中断处理函数(上半部) 下半部(Tasklet/Work) 用户态 调度 唤醒 说明:DMA中断从硬件触发到驱动响应,经过GIC分发、内核通用处理,最终到达驱动注册的处理函数

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。两者各有适用场景:

下半部机制对比:Tasklet vs Workqueue 上半部:request_irq() 注册的处理函数 调度 Tasklet(tasklet_schedule) 软中断上下文执行(不可睡眠) 上半部:request_irq() 注册的处理函数 调度 Work(schedule_work) 进程上下文执行(可睡眠、可加锁) 选择依据:如果下半部需要睡眠(如调用 mutex_lock、kmalloc(GFP_KERNEL)),必须使用 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
可重入性 同一中断可能嵌套(高优先级中断抢占) 数据竞争、状态不一致
中断上下文编程禁区 ❌ mutex_lock ❌ copy_to_user ❌ kmalloc(GFP_KERNEL) ❌ schedule() ❌ wait_event() ❌ msleep() ✅ 允许:spin_lock / writel / readl / tasklet_schedule / schedule_work / kfree 核心原则:中断上下文中不能睡眠、不能访问用户空间、不能使用可能导致阻塞的锁

3.5 音频DMA中断处理最佳实践

结合以上分析,在嵌入式Linux音频驱动中,DMA中断处理应遵循以下设计原则:

  1. 上半部最小化: 只做三件事——清除中断标志、读取DMA状态寄存器、调度下半部。所有业务逻辑推迟。
  2. 下半部选择策略:
    • 如果只需要更新内核态环形缓冲区指针 + 唤醒等待队列 → 使用 Tasklet(延迟最低)。
    • 如果需要将数据拷贝到用户空间或获取互斥锁 → 使用 Workqueue(可睡眠)。
    • 对于高性能音频场景(如48kHz/16位/2通道,周期大小256帧),推荐使用Tasklet,因为Workqueue的调度延迟可能导致Xrun。
  3. 锁的选用: 上半部使用 spin_lock_irqsave 保护共享数据;下半部如果使用Tasklet,同样使用spin_lock;如果使用Workqueue,可以使用 mutex_lock。
  4. 测量与验证: 使用 ftrace 或 tracepoint 测量中断处理时间,确保上半部 < 10μs,下半部 < 100μs。
性能指标参考: 在ARM Cortex-A7 @ 1GHz平台上,一个优化良好的音频DMA中断处理流程:上半部约 3-5μs,Tasklet下半部约 20-40μs,Workqueue下半部约 50-100μs(包含调度延迟)。如果上半部超过 15μs,就需要考虑进一步拆分或优化。