4. 上半部与下半部机制

中断处理是驱动响应硬件事件的核心路径。Linux 内核将中断处理拆分为两个阶段:上半部(Top Half) 和 下半部(Bottom Half)。这种拆分机制直接决定了网络协议栈的吞吐能力和实时性。

4.1 上半部(硬中断)职责

上半部由硬件中断信号直接触发,运行在中断上下文(IRQ Context)中。其核心职责是快速响应,而非完整处理。

核心原则:上半部必须执行得尽可能快,通常只做最必要的硬件操作,如清除中断标志、读取状态寄存器、将数据从硬件 FIFO 拷贝到内存缓冲区,然后调度下半部。
职责 说明 典型操作
硬件确认 向中断控制器(如 APIC、GIC)发送 EOI(End of Interrupt) 写寄存器
状态保存 保存必要的硬件状态或错误标志 读状态寄存器
数据转移 将硬件接收的数据转移到内核缓冲区(如 skb) DMA 完成通知、memcpy
下半部调度 标记需要进一步处理的工作,唤醒下半部机制 raise_softirq()、tasklet_schedule()
避坑指南:在上半部中禁止调用可能导致睡眠的函数(如 kmalloc(GFP_KERNEL)、mutex_lock())。中断上下文没有进程调度能力,睡眠会导致内核崩溃。
上半部(硬中断)处理流程 硬件中断触发 中断控制器分发 上半部处理函数 清除中断标志 读取硬件状态 拷贝数据到内存 调度下半部 (Softirq/Tasklet) 中断返回

4.2 下半部设计动机

如果所有中断处理都在上半部完成,系统将面临两个严重问题:

  1. 中断嵌套风暴:长时间关中断会导致高优先级中断丢失,系统实时性急剧下降。
  2. 处理能力瓶颈:网络协议栈的复杂处理(路由查找、TCP 重组、socket 分发)无法在中断上下文中完成。

下半部的设计动机正是将耗时、可延迟、可睡眠的操作从上半部剥离,在更宽松的上下文中执行。

对比维度 上半部(硬中断) 下半部(Softirq/Tasklet/Workqueue)
执行上下文 中断上下文(不可睡眠) Softirq/Tasklet 仍不可睡眠;Workqueue 可睡眠
中断状态 当前 CPU 关中断(或部分关) 开中断,可被新中断抢占
执行时机 硬件触发后立即执行 上半部结束后、或内核调度点执行
典型耗时 微秒级(< 100μs) 毫秒级甚至更长
允许操作 仅原子操作、寄存器读写 Softirq/Tasklet 有限;Workqueue 可调用所有内核 API

4.3 下半部机制详解

Linux 内核提供了三种下半部机制:Softirq、Tasklet 和 Workqueue。它们在内核中的定位和使用场景各不相同。

4.3.1 Softirq

Softirq 是最高优先级的下半部机制,直接在内核的软中断上下文中执行。它主要用于对性能要求极高的场景,如网络收发包处理(NET_RX_SOFTIRQ、NET_TX_SOFTIRQ)。

  • 静态分配:Softirq 在编译时静态定义,运行时不能动态创建。
  • 可重入性:同一个 Softirq 可以在多个 CPU 上同时执行,必须自行处理并发。
  • 触发方式:通过 raise_softirq() 或 __raise_softirq_irqoff() 触发。
  • 执行点:在 do_softirq() 中执行,通常由中断返回路径 irq_exit() 调用。
// 注册 Softirq 处理函数(通常在驱动初始化时调用)
open_softirq(NET_RX_SOFTIRQ, net_rx_action);

// 在上半部中触发 Softirq
void netif_rx(struct sk_buff *skb) {
    // ... 将 skb 放入 CPU 的 backlog 队列 ...
    __raise_softirq_irqoff(NET_RX_SOFTIRQ);
}

4.3.2 Tasklet

Tasklet 是基于 Softirq 实现的更易用的下半部机制。它解决了 Softirq 的并发复杂性:同一个 Tasklet 在任意时刻只能在一个 CPU 上执行。

  • 动态创建:可以在驱动运行时动态创建和销毁。
  • 串行化:同一类型的 Tasklet 不会并发执行,简化了驱动开发。
  • 底层实现:Tasklet 实际上使用 HI_SOFTIRQ 和 TASKLET_SOFTIRQ 两个 Softirq 实现。
  • 使用场景:适合大多数驱动的中断下半部处理,尤其是网络驱动。
// 定义 Tasklet 处理函数
void my_tasklet_handler(unsigned long data) {
    struct net_device *dev = (struct net_device *)data;
    // 处理网络数据包
}

// 声明并初始化 Tasklet
DECLARE_TASKLET(my_tasklet, my_tasklet_handler, (unsigned long)dev);

// 在上半部中调度 Tasklet
void my_interrupt_handler(int irq, void *dev_id) {
    // ... 上半部处理 ...
    tasklet_schedule(&my_tasklet);  // 调度下半部
}

4.3.3 Workqueue

Workqueue 是唯一允许睡眠的下半部机制。它在内核线程的进程上下文中执行,可以调用所有可能阻塞的内核 API。

  • 进程上下文:可以持有互斥锁、执行文件系统操作、分配大块内存。
  • 线程化:每个 Workqueue 对应一个或多个内核线程(kworker)。
  • 延迟执行:支持指定延迟时间后执行(schedule_delayed_work())。
  • 使用场景:需要睡眠的复杂操作,如协议栈状态机、硬件配置、日志写入。
// 定义工作处理函数
void my_work_handler(struct work_struct *work) {
    struct my_priv *priv = container_of(work, struct my_priv, my_work);
    // 可以睡眠的操作
    mutex_lock(&priv->lock);
    // ... 复杂处理 ...
    mutex_unlock(&priv->lock);
}

// 初始化工作项
INIT_WORK(&priv->my_work, my_work_handler);

// 在上半部中调度工作
void my_interrupt_handler(int irq, void *dev_id) {
    // ... 上半部处理 ...
    schedule_work(&priv->my_work);  // 调度到系统默认 workqueue
}
下半部机制架构与选择路径 上半部(硬中断) Softirq Tasklet Workqueue 特性 • 最高性能 • 可重入,需处理并发 • 静态分配 特性 • 易用性好 • 同一类型串行执行 • 动态创建 特性 • 可睡眠 • 进程上下文 • 支持延迟执行 网络协议栈核心 通用驱动下半部 需要睡眠的复杂操作

4.4 三种下半部机制对比总结

特性 Softirq Tasklet Workqueue
执行上下文 软中断上下文 软中断上下文 进程上下文
是否可睡眠 否 否 是
并发模型 多 CPU 并行 单 CPU 串行(同类型) 多 CPU 并行(不同 work)
创建方式 静态编译 动态运行时 动态运行时
调度开销 极低 低 中等(线程切换)
典型延迟 微秒级 微秒级 毫秒级
网络协议栈使用 核心收发包路径 部分驱动辅助处理 配置管理、状态同步
选择建议:
  • 网络协议栈核心数据路径 → Softirq(如 NET_RX_SOFTIRQ)
  • 大多数驱动中断下半部 → Tasklet(平衡性能与易用性)
  • 需要睡眠或长时间操作 → Workqueue(如硬件初始化、固件加载)

4.5 网络协议栈中的下半部协作实例

以网卡驱动接收数据包为例,展示上半部与下半部的完整协作流程:

  1. 硬件中断触发:网卡接收到数据包,通过 PCIe 总线触发 IRQ。
  2. 上半部执行:驱动中断处理函数读取 DMA 完成描述符,将 sk_buff 放入 CPU 的 backlog 队列,调用 __raise_softirq_irqoff(NET_RX_SOFTIRQ)。
  3. Softirq 执行:在 do_softirq() 中,net_rx_action() 从 backlog 队列取出 sk_buff,调用协议栈注册的 netif_receive_skb()。
  4. 协议栈处理:数据包依次经过链路层、IP 层、TCP/UDP 层,最终放入 socket 接收队列。
  5. 唤醒用户进程:如果 socket 上有进程在 recv() 阻塞,内核唤醒该进程。
// 网络驱动中断处理函数(上半部)
static irqreturn_t my_netdev_interrupt(int irq, void *dev_id) {
    struct net_device *dev = dev_id;
    struct my_priv *priv = netdev_priv(dev);

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

    // 2. 清除中断标志(必须在上半部完成)
    writel(status, priv->ioaddr + INT_STATUS);

    // 3. 如果收到数据包,将 skb 加入队列并触发 Softirq
    if (status & RX_INT) {
        napi_schedule(&priv->napi);  // NAPI 机制内部调用 __raise_softirq_irqoff
    }

    return IRQ_HANDLED;
}

// Softirq 处理函数(下半部)
static void net_rx_action(struct softirq_action *h) {
    struct napi_struct *napi;
    // 遍历 NAPI 列表,批量处理数据包
    list_for_each_entry(napi, &softnet_data->poll_list, poll_list) {
        napi->poll(napi, budget);  // 调用驱动的 poll 函数
    }
}

通过这种分层协作,Linux 网络协议栈能够在高吞吐场景下保持低延迟,同时确保驱动开发的灵活性和可维护性。