3. 顶半部(Top Half)机制:顶半部的职责、执行时机、最小化执行时间原则
顶半部(Top Half)是中断处理流程中直接响应硬件中断请求的第一阶段。它运行在中断上下文中,具有最高的抢占优先级,其设计质量直接决定了系统的实时响应能力和稳定性。理解顶半部的职责边界、执行时机以及核心设计原则,是掌握中断处理驱动开发的基础。
3.1 顶半部的核心职责
顶半部的主要任务是以最快的速度确认硬件中断,并完成必须立即处理的操作,然后将剩余的重负载工作推迟到底半部执行。其职责可以归纳为以下四个关键点:
| 职责 | 说明 | 典型操作 |
|---|---|---|
| 中断确认 | 向中断控制器(如GIC、APIC)发送ACK信号,清除中断挂起位,防止同一中断重复触发 | 读取中断状态寄存器、写确认寄存器 |
| 数据保存 | 从硬件FIFO或数据寄存器中读取关键数据,避免被后续中断覆盖 | 读取RX FIFO、保存时间戳、拷贝状态字 |
| 硬件操作 | 执行对时间敏感的硬件控制,如复位错误标志、启动下一次DMA传输 | 写控制寄存器、清除错误位、触发DMA |
| 触发底半部 | 将需要进一步处理的工作调度给底半部机制(Tasklet、Workqueue、Softirq) | 调用 tasklet_schedule()、schedule_work() |
设计要点: 顶半部只做“必须在此刻做”的事,任何可以推迟的操作都应交给底半部。一个常见的判断标准是:如果该操作可以在进程上下文中完成,就不应该在顶半部中执行。
3.2 执行时机与上下文环境
顶半部的执行时机由硬件中断信号和内核中断管理机制共同决定。理解其执行上下文对于编写安全的驱动代码至关重要。
- 触发条件: 当外设产生中断信号,经过中断控制器路由到CPU,CPU在每条指令执行完毕后检查中断标志位,若允许中断则立即响应。
- 执行上下文: 运行在中断上下文(Interrupt Context)中,不可睡眠、不可调度、不可持有互斥锁(mutex)。
- 抢占特性: 在Linux中,顶半部执行时默认禁止本地CPU上的所有中断(或同优先级中断),以保证原子性。
- 嵌套风险: 高优先级中断可以抢占低优先级中断的顶半部,驱动开发者必须考虑重入问题。
常见错误: 在顶半部中使用
kmalloc(GFP_KERNEL) 或 mutex_lock() 会导致内核崩溃,因为这些函数可能触发睡眠操作。顶半部中只能使用 GFP_ATOMIC 标志分配内存。
3.3 最小化执行时间原则
顶半部的执行时间直接决定了系统的中断延迟和实时性上限。业界普遍遵循“微秒级完成”的设计目标。以下是具体的原则和量化指标:
| 原则 | 描述 | 量化目标 |
|---|---|---|
| 只做必要操作 | 仅执行硬件确认、数据保存和底半部调度,不做协议解析、数据拷贝、日志打印 | 代码行数控制在10~30行以内 |
| 避免循环等待 | 不轮询状态寄存器、不等待DMA完成、不使用延迟函数(如udelay超过100μs) | 单次顶半部执行时间 < 50μs |
| 禁止调用阻塞API | 不使用信号量、等待队列、文件系统操作、网络发送等可能阻塞的接口 | 顶半部中0次阻塞调用 |
| 数据拷贝最小化 | 只拷贝必须立即保存的关键数据(如FIFO溢出前的数据),大数据块留给底半部 | 单次拷贝量 < 64字节 |
性能参考: 在ARM Cortex-A72 @1.5GHz平台上,一个设计良好的顶半部(包含中断确认、读取32字节FIFO、调度tasklet)的执行时间通常在5~15μs之间。如果超过100μs,就需要重新审视设计。
3.4 顶半部与底半部的协作流程
下图展示了顶半部与底半部在中断处理中的完整协作流程,以及顶半部在整个中断生命周期中的位置:
3.5 顶半部代码模板与示例
以下是一个典型的网络设备驱动顶半部实现,展示了最小化执行时间原则的具体应用:
/* 网络设备驱动顶半部 - 最小化实现示例 */
static irqreturn_t eth_irq_handler(int irq, void *dev_id)
{
struct net_device *dev = (struct net_device *)dev_id;
struct eth_private *priv = netdev_priv(dev);
u32 status;
/* 1. 读取中断状态寄存器(必须立即读取,防止丢失) */
status = readl(priv->ioaddr + ETH_INT_STATUS);
/* 2. 确认中断:写回ACK位清除中断 */
writel(status & ETH_INT_MASK, priv->ioaddr + ETH_INT_ACK);
/* 3. 保存关键数据:从RX FIFO读取数据长度 */
if (status & ETH_INT_RX_DONE) {
priv->rx_len = readl(priv->ioaddr + ETH_RX_LEN);
/* 注意:此处不拷贝数据,只保存长度 */
}
/* 4. 调度底半部处理实际数据收发 */
if (status & (ETH_INT_RX_DONE | ETH_INT_TX_DONE))
napi_schedule(&priv->napi);
return IRQ_HANDLED;
}
代码分析: 上述顶半部仅执行了4个操作:读取状态、确认中断、保存数据长度、调度NAPI。所有数据拷贝、协议解析、统计更新等耗时操作都在底半部的NAPI轮询函数中完成。整个顶半部执行时间在大多数平台上不超过10μs。
3.6 顶半部设计检查清单
在编写或评审顶半部代码时,可以使用以下清单进行快速自查:
- ☐ 是否在顶半部中调用了可能导致睡眠的函数?(如mutex_lock、wait_event、kmalloc(GFP_KERNEL))
- ☐ 是否在顶半部中进行了大数据拷贝或复杂计算?(应移至底半部)
- ☐ 是否在顶半部中使用了printk或日志输出?(printk在顶半部中可能导致死锁)
- ☐ 是否考虑了中断嵌套和重入问题?(使用per-CPU变量或原子操作保护共享数据)
- ☐ 是否在顶半部中正确确认了中断?(未确认的中断会导致IRQ风暴)
- ☐ 顶半部执行时间是否在50μs以内?(可通过ftrace或内核时间戳测量)
性能陷阱: 即使单个顶半部执行时间很短,如果中断频率极高(如10kHz以上),顶半部的累积开销仍可能耗尽CPU资源。此时应考虑中断合并(Interrupt Coalescing)或改用轮询模式(NAPI)。