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 顶半部与底半部的协作流程

下图展示了顶半部与底半部在中断处理中的完整协作流程,以及顶半部在整个中断生命周期中的位置:

顶半部与底半部协作流程图 顶半部与底半部协作流程 硬件中断触发 顶半部 (Top Half) 确认中断 · 保存关键数据 · 调度底半部 中断返回 底半部 (Bottom Half) Tasklet / Workqueue / Softirq 进程上下文继续执行 被中断的进程恢复运行 底半部处理完成 关键时间约束 顶半部执行时间 < 50μs 底半部可延迟到空闲时执行

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)。