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 设计原则:如何划分上半部与下半部
正确划分上下半部是实时系统优化的关键。以下是一套经过实践检验的设计原则:
- 时间敏感原则:对时间要求极苛刻的操作(如读取硬件 FIFO 防止溢出)必须放在上半部。
- 不可中断原则:需要原子访问的共享数据结构(如 DMA 描述符链表)的更新应在上半部完成。
- 可推迟原则:任何可以稍后处理的工作(如数据解析、统计更新、用户态通知)都应放入下半部。
- 上下文匹配原则:如果处理函数需要睡眠(如等待 I/O、分配大块内存),必须使用工作队列;否则优先使用 tasklet。
- 优先级反转避免原则:下半部不应持有长时间等待的锁,以免阻塞其他高优先级任务。
⚠️ 常见陷阱: 将需要睡眠的
copy_to_user() 或 kmalloc(GFP_KERNEL) 放在 tasklet 中会导致内核崩溃。务必检查函数是否可能在中断上下文中被调用。
4.4 上下半部协作流程(SVG 架构图)
下图展示了 DMA 中断发生后,上半部与下半部之间的典型协作流程:
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 微秒以内,同时将复杂的数据处理任务交给下半部在更宽松的时间窗口内完成。