4. 中断处理函数编写:上半部(硬中断)与下半部(tasklet/softirq)分工

网络设备中断处理是驱动性能的核心瓶颈。Linux 内核将中断处理拆分为 上半部(硬中断) 和 下半部(软中断/tasklet),以平衡实时性与吞吐量。本章深入剖析分工原则、代码实现与性能权衡。

核心原则:上半部必须极短(微秒级),仅完成最紧急的硬件操作;下半部处理耗时逻辑,并可被更高优先级中断抢占。

4.1 上半部(硬中断处理程序)

上半部在关闭本地 CPU 中断(或特定中断线)的上下文中执行,必须快速返回。典型职责包括:

  • 确认中断:读取中断状态寄存器,清除中断标志位。
  • 保存关键数据:从硬件 FIFO 读取数据到内存缓冲区。
  • 调度下半部:标记软中断或唤醒 tasklet/kthread。
  • 返回 IRQ_HANDLED:通知内核中断已被处理。
// 典型上半部处理函数示例
static irqreturn_t netdev_irq_handler(int irq, void *dev_id)
{
    struct net_device *dev = (struct net_device *)dev_id;
    struct my_priv *priv = netdev_priv(dev);

    // 1. 读取中断状态寄存器
    u32 status = readl(priv->ioaddr + INT_STATUS);

    // 2. 清除中断标志(写1清除)
    writel(status, priv->ioaddr + INT_STATUS);

    // 3. 检查是否为接收中断
    if (status & RX_INT_MASK) {
        // 保存接收状态,供下半部使用
        priv->rx_status = status;
        // 调度 NAPI 软中断(下半部)
        napi_schedule(&priv->napi);
    }

    // 4. 检查是否为发送完成中断
    if (status & TX_INT_MASK) {
        // 唤醒发送完成处理(下半部)
        tasklet_schedule(&priv->tx_tasklet);
    }

    return IRQ_HANDLED;
}
避坑指南:上半部中禁止调用 printk、kmalloc(GFP_KERNEL)、mutex_lock 等可能睡眠的函数。使用 dev_dbg 或 netdev_dbg 时需确保其实现不引入阻塞。

4.2 下半部机制对比:Softirq vs Tasklet

Linux 提供多种下半部机制,网络驱动最常用的是 Softirq(NAPI 核心)和 Tasklet(基于 Softirq 的封装)。

特性 Softirq Tasklet
执行上下文 软中断上下文(可被硬中断抢占) 软中断上下文(基于 TASKLET_SOFTIRQ)
并发性 同一类型可在多 CPU 上并行执行 同一 tasklet 在同一时刻只在一个 CPU 上执行
注册方式 open_softirq() + raise_softirq() tasklet_init() + tasklet_schedule()
典型用途 NAPI 轮询、网络协议栈处理 发送完成清理、错误处理、延迟工作
性能开销 较低(直接调用函数) 中等(需调度到 TASKLET_SOFTIRQ)
性能建议:对于高吞吐网络驱动,优先使用 NAPI(基于 Softirq)处理接收路径;发送完成等低频事件可使用 Tasklet 简化代码。

4.3 下半部实现:Tasklet 示例

Tasklet 适用于不需要严格并行、代码逻辑相对简单的场景。以下展示发送完成处理的典型实现:

// 发送完成 tasklet 处理函数
static void tx_complete_tasklet(unsigned long data)
{
    struct net_device *dev = (struct net_device *)data;
    struct my_priv *priv = netdev_priv(dev);
    struct netdev_queue *txq = netdev_get_tx_queue(dev, 0);

    // 1. 读取发送完成描述符
    while (priv->tx_complete_count > 0) {
        // 释放 DMA 缓冲区
        dma_unmap_single(&dev->dev, priv->tx_desc[priv->tx_clean_idx].dma,
                         priv->tx_desc[priv->tx_clean_idx].len, DMA_TO_DEVICE);
        // 释放 skb
        dev_kfree_skb(priv->tx_skb[priv->tx_clean_idx]);
        priv->tx_clean_idx = (priv->tx_clean_idx + 1) % TX_RING_SIZE;
        priv->tx_complete_count--;
    }

    // 2. 唤醒发送队列(如果之前被停止)
    netif_tx_wake_queue(txq);
}

// 初始化 tasklet
tasklet_init(&priv->tx_tasklet, tx_complete_tasklet, (unsigned long)dev);

4.4 上半部与下半部协作流程

下图展示一次网络接收中断的完整处理路径:

中断处理上半部与下半部协作流程 中断处理:上半部 → 下半部协作流程 硬件中断触发 上半部(硬中断) 读状态 → 清标志 → 保存数据 调度下半部 NAPI Softirq 轮询收包 → 协议栈递交 Tasklet 发送完成 → 释放 skb/DMA 中断处理完成 上半部关闭中断执行,下半部在开中断环境下运行,可被新硬中断抢占

4.5 分工性能权衡

合理划分上下半部直接影响网络吞吐和 CPU 利用率。以下为常见权衡点:

  • 上半部过长:增加中断延迟,可能导致高优先级中断丢失(如时钟中断)。
  • 下半部过重:软中断执行时间过长会阻塞用户态进程调度,降低交互体验。
  • 过度拆分:频繁的上下半部切换增加上下文切换开销,降低 cache 命中率。
调优经验:对于 10Gbps 及以上网卡,建议将数据拷贝、协议解析全部放入 NAPI 轮询(Softirq),上半部仅做状态读取和 NAPI 调度。实测表明,上半部执行时间应控制在 5μs 以内。

4.6 代码组织与调试技巧

组件 文件位置(示例) 关键函数
上半部 drivers/net/ethernet/mydev/mydev_main.c mydev_irq_handler()
NAPI 轮询 drivers/net/ethernet/mydev/mydev_napi.c mydev_poll()
Tasklet drivers/net/ethernet/mydev/mydev_tx.c mydev_tx_complete()
初始化/注册 drivers/net/ethernet/mydev/mydev_probe.c request_irq()、netif_napi_add()、tasklet_init()
调试命令:使用 cat /proc/interrupts 查看硬中断分布;cat /proc/softirqs 查看软中断执行次数;perf top -G 可定位下半部热点函数。