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 上半部与下半部协作流程
下图展示一次网络接收中断的完整处理路径:
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 可定位下半部热点函数。