4. 顶半部与底半部设计哲学:顶半部快速响应原则、底半部延迟处理机制、性能权衡

中断处理程序的设计是驱动响应延迟优化的核心。Linux 内核将中断处理拆分为两个阶段:顶半部(Top Half)和底半部(Bottom Half)。这种设计哲学的核心在于:在中断上下文(原子上下文)中只做最必要、最快速的操作,将耗时、可延迟的工作推迟到更宽松的上下文中执行。

核心原则:顶半部必须“快进快出”,底半部负责“善后收尾”。违反此原则将导致系统中断延迟飙升、实时性恶化,甚至音频卡顿、触控丢帧等用户体验问题。

4.1 顶半部快速响应原则

顶半部是中断请求(IRQ)到来时内核立即执行的代码。它运行在中断上下文中,此时所有本地中断被关闭(或至少当前中断线被屏蔽),不允许睡眠、阻塞或执行复杂的同步操作。

4.1.1 顶半部的职责边界

职责 说明 典型操作
硬件确认 读取硬件状态寄存器,确认中断源 readl(reg) 检查中断挂起位
数据快照 拷贝最小必要数据(如一个字节、一个 FIFO 水位) ioread8() 读取按键扫描码
清除中断标志 向硬件写回确认位,允许后续中断到达 writel(BIT(irq_num), reg_clear)
触发底半部 调度底半部机制(tasklet/workqueue/threaded irq) tasklet_schedule() / schedule_work()
避坑指南:顶半部中绝对禁止调用 kmalloc(GFP_KERNEL)、mutex_lock()、copy_to_user() 等可能引起睡眠的函数。一次顶半部执行时间应控制在 10~50 µs 以内,否则应重新审视设计。

4.1.2 顶半部执行时间与系统延迟的关系

顶半部执行时间直接决定了系统的中断响应延迟。当顶半部占用 CPU 时,其他中断(包括更高优先级的中断)必须等待。下图展示了顶半部过长导致的连锁效应:

顶半部执行时间与中断延迟关系图 顶半部执行时间与中断延迟关系 t 顶半部 底半部处理 正常场景 ~30µs 顶半部过长(占用 CPU) 底半部延迟 延迟恶化场景 ~200µs 新中断等待
关键指标:在 Android 系统中,触控中断的顶半部通常要求在 20 µs 内完成,音频 DMA 中断要求 10 µs 内完成。超过此阈值将直接导致用户体验下降。

4.2 底半部延迟处理机制

底半部负责执行顶半部推迟下来的剩余工作。它运行在软中断上下文或进程上下文中,允许睡眠、持有锁、分配内存等操作。Linux 内核提供了多种底半部机制,各有不同的延迟特性和适用场景。

4.2.1 底半部机制对比

机制 上下文 可睡眠 执行顺序 典型延迟 推荐场景
SoftIRQ 软中断 否 严格顺序 ~10 µs 网络收包、高吞吐数据流
Tasklet 软中断 否 同类型串行 ~50 µs 简单数据处理、状态更新
Workqueue 进程 是 并发(多 worker) ~500 µs ~ 数 ms 复杂 I/O、块设备操作
Threaded IRQ 进程 是 独立内核线程 ~100 µs ~ 数 ms 需要 RT 优先级、复杂驱动

4.2.2 底半部调度流程

下图展示了从硬件中断触发到底半部执行的完整路径:

中断处理完整流程:顶半部到底半部 中断处理完整流程:顶半部 → 底半部 硬件中断触发 顶半部(Top Half) 中断上下文 · 原子操作 · 快速确认 调度底半部 Tasklet 软中断 · 串行执行 Workqueue 进程上下文 · 可睡眠 Threaded IRQ 内核线程 · RT 优先级 用户态驱动 / 应用层响应 选择哪种底半部机制取决于:实时性要求、数据量大小、是否允许睡眠、并发访问模式

4.2.3 代码示例:Tasklet 与 Workqueue 实现对比

以下代码展示了同一功能(按键中断处理)分别使用 Tasklet 和 Workqueue 的实现差异:

/* ========== Tasklet 实现(不可睡眠) ========== */
static void key_tasklet_handler(unsigned long data)
{
    struct key_device *kdev = (struct key_device *)data;
    // 快速读取硬件 FIFO,不睡眠
    u8 scancode = ioread8(kdev->mmio_base + KEY_FIFO);
    input_report_key(kdev->input_dev, scancode, 1);
    input_sync(kdev->input_dev);
}

static DECLARE_TASKLET(key_tasklet, key_tasklet_handler, (unsigned long)&kdev);

static irqreturn_t key_top_half(int irq, void *dev_id)
{
    struct key_device *kdev = dev_id;
    // 顶半部:确认中断 + 调度 tasklet
    writel(KEY_IRQ_CLEAR, kdev->mmio_base + KEY_IRQ_STATUS);
    tasklet_schedule(&kdev->tasklet);
    return IRQ_HANDLED;
}

/* ========== Workqueue 实现(可睡眠) ========== */
static void key_work_handler(struct work_struct *work)
{
    struct key_device *kdev = container_of(work, struct key_device, work);
    // 可以调用 kmalloc、mutex_lock 等睡眠函数
    u8 *buffer = kmalloc(MAX_KEY_BUF, GFP_KERNEL);
    if (buffer) {
        // 复杂数据处理...
        kfree(buffer);
    }
}

static irqreturn_t key_top_half_work(int irq, void *dev_id)
{
    struct key_device *kdev = dev_id;
    writel(KEY_IRQ_CLEAR, kdev->mmio_base + KEY_IRQ_STATUS);
    schedule_work(&kdev->work);  // 调度 workqueue
    return IRQ_HANDLED;
}

4.3 性能权衡:延迟、吞吐量与 CPU 开销

选择顶半部/底半部分割策略时,需要在中断响应延迟、数据吞吐量和CPU 开销之间进行权衡。没有一种方案适合所有场景。

4.3.1 权衡三角模型

性能权衡三角模型 性能权衡三角模型 低延迟 顶半部极简 · 高优先级底半部 高吞吐 批量处理 · 合并中断 低 CPU 开销 减少上下文切换 · 避免频繁调度 设计目标区域 实际驱动设计只能落在三角形内部,无法同时达到三个顶点

4.3.2 不同场景的权衡策略

场景 首要目标 推荐策略 底半部选择
触控屏 低延迟(触摸跟手性) 顶半部仅读取坐标,立即调度高优先级 threaded irq Threaded IRQ (RT priority 50)
音频 DMA 低延迟 + 稳定周期 顶半部填充环形缓冲区,使用 SoftIRQ 处理 SoftIRQ (HI_SOFTIRQ)
WiFi 网络 高吞吐 NAPI 合并中断,批量收包,减少顶半部触发频率 NAPI + SoftIRQ
传感器数据 低 CPU 开销 顶半部标记数据就绪,workqueue 中批量处理 Workqueue (unbound wq)
按键/GPIO 平衡 顶半部快速确认,tasklet 中上报输入事件 Tasklet

4.3.3 性能权衡决策流程

  1. 测量顶半部执行时间:使用 ftrace 或 trace_printk 测量顶半部耗时。如果超过 50 µs,必须将更多工作移到底半部。
  2. 评估数据实时性要求:如果数据必须在 100 µs 内到达用户态(如触控),选择 Threaded IRQ 并设置 RT 优先级;如果允许 1 ms 延迟,Workqueue 即可。
  3. 分析数据量:单次中断只有几个字节(如按键),Tasklet 足够;如果是批量数据(如摄像头帧),考虑 NAPI 或 Workqueue。
  4. 检查并发访问:如果底半部需要访问共享资源,优先选择 Workqueue(可睡眠、可持有 mutex),避免 Tasklet 中的自旋锁竞争。
  5. 压力测试:在系统满载(CPU 100%、内存压力)下测试中断延迟抖动,使用 cyclictest 或 trace-cmd 验证最坏情况延迟。
实战建议:在 Android BSP 开发中,优先使用 Threaded IRQ 作为默认底半部机制。它提供了清晰的线程上下文、可配置的 RT 优先级,并且与内核的 RT 补丁(PREEMPT_RT)兼容性最好。仅在性能敏感路径(如音频 DMA)才考虑 SoftIRQ 或 Tasklet。

4.3.4 常见性能陷阱

  • 顶半部中调用 printk:printk 在中断上下文中可能导致控制台锁竞争,严重增加延迟。使用 trace_printk 替代。
  • 底半部中过度使用自旋锁:Tasklet 中长时间持有自旋锁会阻塞其他 Tasklet 执行。考虑使用 Workqueue + mutex 替代。
  • 中断共享导致顶半部膨胀:多个设备共享同一 IRQ 线时,顶半部需要遍历所有设备确认中断源。使用 devm_request_irq() 并确保每个设备的确认操作足够快。
  • 底半部调度过于频繁:高频率中断(如 1 kHz 触控扫描)导致底半部被反复调度,上下文切换开销剧增。使用 NAPI 或合并中断(coalescing)降低频率。
性能反模式:不要在顶半部中调用 schedule_work() 后立即在底半部中调用 flush_work() 或 cancel_work_sync()。这会导致死锁或严重的优先级反转。底半部应该独立完成工作,顶半部只负责“点火”。