4. 上半部与下半部机制:上半部快速处理、下半部延迟处理、设计原则

在 DMA 中断处理场景中,中断服务程序(ISR)的执行时间直接决定了系统的实时响应能力。Linux 内核通过将中断处理拆分为 上半部(Top Half) 和 下半部(Bottom Half),实现了“快速响应、延迟处理”的核心设计哲学。

4.1 上半部:快速响应,最小化关中断时间

上半部是传统的中断服务程序(ISR),它在中断上下文(Interrupt Context)中执行,具有以下关键特征:

  • 原子性要求:执行期间当前 CPU 的中断被屏蔽(或至少同级别中断被屏蔽),因此必须极快完成。
  • 不可睡眠:中断上下文不允许调用可能导致睡眠的函数(如 kmalloc(GFP_KERNEL)、mutex_lock())。
  • 典型职责:清除硬件中断标志、保存关键寄存器状态、将数据从 DMA 缓冲区拷贝到内核缓冲区、唤醒等待队列。
⚡ 核心原则: 上半部只做“必须立即做且不做不行”的事,其余所有处理都推迟到下半部。

4.2 下半部:延迟处理,灵活调度

下半部负责执行上半部推迟的、耗时较长或可被中断打断的任务。Linux 内核提供了多种下半部机制,在 DMA 中断处理中,tasklet 和 工作队列(Workqueue) 是最常用的两种。

特性 Tasklet 工作队列(Workqueue)
执行上下文 软中断上下文(Softirq Context) 进程上下文(Process Context)
是否可睡眠 ❌ 不可睡眠 ✅ 可睡眠
调度开销 低(直接由软中断触发) 较高(需要唤醒内核线程)
并发性 同一 tasklet 在多个 CPU 上串行执行 可并行执行(多线程)
适用场景 轻量级、快速、不可阻塞的处理 重量级、可能阻塞、需要持有锁或访问文件系统
💡 选择建议: 对于 DMA 传输完成后的数据校验、协议头部解析等短操作,使用 tasklet;对于需要将数据写入磁盘、网络协议栈处理或调用可能睡眠的 API 的操作,使用工作队列。

4.3 设计原则:如何划分上半部与下半部

正确划分上下半部是实时系统优化的关键。以下是一套经过实践检验的设计原则:

  1. 时间敏感原则:对时间要求极苛刻的操作(如读取硬件 FIFO 防止溢出)必须放在上半部。
  2. 不可中断原则:需要原子访问的共享数据结构(如 DMA 描述符链表)的更新应在上半部完成。
  3. 可推迟原则:任何可以稍后处理的工作(如数据解析、统计更新、用户态通知)都应放入下半部。
  4. 上下文匹配原则:如果处理函数需要睡眠(如等待 I/O、分配大块内存),必须使用工作队列;否则优先使用 tasklet。
  5. 优先级反转避免原则:下半部不应持有长时间等待的锁,以免阻塞其他高优先级任务。
⚠️ 常见陷阱: 将需要睡眠的 copy_to_user() 或 kmalloc(GFP_KERNEL) 放在 tasklet 中会导致内核崩溃。务必检查函数是否可能在中断上下文中被调用。

4.4 上下半部协作流程(SVG 架构图)

下图展示了 DMA 中断发生后,上半部与下半部之间的典型协作流程:

DMA 中断上下半部协作流程 DMA 中断上下半部协作流程 DMA 控制器 硬件中断信号 上半部 ISR(中断上下文) 清除中断标志 · 保存状态 · 调度下半部 调度 tasklet / 工作队列 选择下半部机制 Tasklet 软中断上下文 · 不可睡眠 工作队列 进程上下文 · 可睡眠 数据校验 · 协议解析 磁盘写入 · 网络协议栈 上半部 ISR 执行时间应控制在微秒级,下半部可根据负载动态调整优先级

4.5 代码示例:DMA 中断上下半部实现

以下代码展示了在 DMA 驱动中如何注册上半部 ISR 并调度 tasklet 作为下半部:

/* 定义 tasklet 处理函数(下半部) */
static void dma_tasklet_handler(unsigned long data)
{
    struct dma_device *dev = (struct dma_device *)data;
    
    /* 此时处于软中断上下文,不可睡眠 */
    /* 处理 DMA 传输完成的数据 */
    process_dma_buffer(dev->dma_buf, dev->buf_len);
    
    /* 更新统计信息 */
    atomic_inc(&dev->complete_count);
    
    /* 唤醒等待数据的用户态进程 */
    wake_up_interruptible(&dev->read_wait);
}

/* 上半部 ISR */
static irqreturn_t dma_isr(int irq, void *dev_id)
{
    struct dma_device *dev = (struct dma_device *)dev_id;
    
    /* 1. 清除硬件中断标志(必须立即做) */
    writel(DMA_INT_CLEAR, dev->regs + DMA_INT_STATUS);
    
    /* 2. 保存关键状态(必须在上半部做) */
    dev->last_status = readl(dev->regs + DMA_STATUS);
    
    /* 3. 调度 tasklet 处理数据(推迟到下半部) */
    tasklet_schedule(&dev->dma_tasklet);
    
    return IRQ_HANDLED;
}

/* 驱动初始化时注册 ISR 和 tasklet */
static int dma_probe(struct platform_device *pdev)
{
    struct dma_device *dev;
    
    /* ... 分配设备结构体、映射寄存器等 ... */
    
    /* 初始化 tasklet */
    tasklet_init(&dev->dma_tasklet, dma_tasklet_handler, (unsigned long)dev);
    
    /* 注册中断处理函数 */
    ret = request_irq(dev->irq, dma_isr, IRQF_TRIGGER_RISING,
                      "dma_device", dev);
    if (ret) {
        dev_err(&pdev->dev, "Failed to request IRQ\n");
        return ret;
    }
    
    return 0;
}
🔑 关键点: 上半部 ISR 中只做了三件事:清除中断标志、保存状态、调度 tasklet。所有数据处理逻辑都放在 dma_tasklet_handler 中,确保中断关闭时间最小化。

4.6 实时性考量:上下半部对延迟的影响

在实时系统中,上下半部的划分直接影响中断延迟和任务调度延迟:

  • 上半部过长:导致高优先级中断被屏蔽时间增加,可能丢失后续 DMA 数据或导致硬件 FIFO 溢出。
  • 下半部优先级:Tasklet 在软中断中执行,优先级高于普通进程但低于硬件中断;工作队列作为内核线程,受调度器管理,优先级可动态调整。
  • 抢占模型:在 PREEMPT_RT 内核中,tasklet 被转换为可抢占的内核线程,进一步降低了延迟抖动。
延迟类型 上半部影响 下半部影响
中断响应延迟 直接决定(关中断时间) 间接影响(下半部持有锁可能阻塞其他中断)
任务调度延迟 无直接影响 工作队列线程优先级影响用户态任务响应
DMA 数据吞吐量 上半部处理速度影响缓冲区释放 下半部处理速度影响数据消费速率

通过合理划分上下半部,DMA 驱动可以在保证数据不丢失的前提下,将中断处理延迟控制在 10 微秒以内,同时将复杂的数据处理任务交给下半部在更宽松的时间窗口内完成。