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())。中断上下文没有进程调度能力,睡眠会导致内核崩溃。
4.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
}
4.4 三种下半部机制对比总结
| 特性 | Softirq | Tasklet | Workqueue |
|---|---|---|---|
| 执行上下文 | 软中断上下文 | 软中断上下文 | 进程上下文 |
| 是否可睡眠 | 否 | 否 | 是 |
| 并发模型 | 多 CPU 并行 | 单 CPU 串行(同类型) | 多 CPU 并行(不同 work) |
| 创建方式 | 静态编译 | 动态运行时 | 动态运行时 |
| 调度开销 | 极低 | 低 | 中等(线程切换) |
| 典型延迟 | 微秒级 | 微秒级 | 毫秒级 |
| 网络协议栈使用 | 核心收发包路径 | 部分驱动辅助处理 | 配置管理、状态同步 |
选择建议:
- 网络协议栈核心数据路径 → Softirq(如
NET_RX_SOFTIRQ) - 大多数驱动中断下半部 → Tasklet(平衡性能与易用性)
- 需要睡眠或长时间操作 → Workqueue(如硬件初始化、固件加载)
4.5 网络协议栈中的下半部协作实例
以网卡驱动接收数据包为例,展示上半部与下半部的完整协作流程:
- 硬件中断触发:网卡接收到数据包,通过 PCIe 总线触发 IRQ。
- 上半部执行:驱动中断处理函数读取 DMA 完成描述符,将
sk_buff放入 CPU 的backlog队列,调用__raise_softirq_irqoff(NET_RX_SOFTIRQ)。 - Softirq 执行:在
do_softirq()中,net_rx_action()从backlog队列取出sk_buff,调用协议栈注册的netif_receive_skb()。 - 协议栈处理:数据包依次经过链路层、IP 层、TCP/UDP 层,最终放入 socket 接收队列。
- 唤醒用户进程:如果 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 网络协议栈能够在高吞吐场景下保持低延迟,同时确保驱动开发的灵活性和可维护性。