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 时,其他中断(包括更高优先级的中断)必须等待。下图展示了顶半部过长导致的连锁效应:
关键指标:在 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 底半部调度流程
下图展示了从硬件中断触发到底半部执行的完整路径:
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 权衡三角模型
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 性能权衡决策流程
- 测量顶半部执行时间:使用
ftrace或trace_printk测量顶半部耗时。如果超过 50 µs,必须将更多工作移到底半部。 - 评估数据实时性要求:如果数据必须在 100 µs 内到达用户态(如触控),选择 Threaded IRQ 并设置 RT 优先级;如果允许 1 ms 延迟,Workqueue 即可。
- 分析数据量:单次中断只有几个字节(如按键),Tasklet 足够;如果是批量数据(如摄像头帧),考虑 NAPI 或 Workqueue。
- 检查并发访问:如果底半部需要访问共享资源,优先选择 Workqueue(可睡眠、可持有 mutex),避免 Tasklet 中的自旋锁竞争。
- 压力测试:在系统满载(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()。这会导致死锁或严重的优先级反转。底半部应该独立完成工作,顶半部只负责“点火”。