引言
在Linux内核的中断处理体系中,上半部(Top Half)快速响应硬件并调度下半部(Bottom Half)完成耗时工作。下半部机制是内核实现高效、低延迟I/O的核心架构,经过二十余年的演进,已发展出softirq、tasklet、workqueue、timer、threaded irq等多种形态。本文深入剖析softirq与Workqueue两大核心机制的源码实现、设计权衡和生产调优。
一、中断上下文的分类与约束
在Linux内核中,处理器至少存在两种执行上下文:
- 进程上下文(Process Context):通过系统调用或缺页异常进入内核,持有进程凭证,允许睡眠,可访问用户空间。
- 中断上下文(Interrupt Context):硬件中断或软中断触发进入内核,不隶属于任何进程,不允许睡眠(无进程地址空间,无法被调度器选择)。
内核通过以下宏判断当前上下文:
#include <linux/preempt.h>
in_interrupt() // 硬中断 + 软中断 + NMI + 异常
in_irq() // 仅硬中断(含NMI)
in_softirq() // softirq handler 上下文(含 BH disable)
in_atomic() // 不可睡眠:中断、持有 spinlock、BH disabled
理解这些约束是下半部选型的根本依据——能否睡眠、能否被抢占、延迟要求决定了方案取舍。
二、softirq:内核下半部的基石
2.1 softirq 类型与优先级
softirq是内核中编译期静态注册的延迟处理机制,定义在include/linux/interrupt.h中,每种类型对应一个用途:
enum {
HI_SOFTIRQ = 0, // 高优先级 tasklet(最早执行)
TIMER_SOFTIRQ, // 定时器回调(定时器中断下半部)
NET_TX_SOFTIRQ, // 网卡发送(网络栈发送路径)
NET_RX_SOFTIRQ, // 网卡接收(NAPI 轮询触发区块)
BLOCK_SOFTIRQ, // 块层完成通知(end_that_request_last)
IRQ_POLL_SOFTIRQ, // 块设备中断轮询
TASKLET_SOFTIRQ, // 普通tasklet(动态分配)
SCHED_SOFTIRQ, // SMP 负载均衡(触发 IPI)
HRTIMER_SOFTIRQ, // 高精度定时器(hrtimer)
RCU_SOFTIRQ, // RCU 回调(rcu_core)
NR_SOFTIRQS // 总数 = 10
};
softirq 数组通过 __softirq_pending 位图管理,同类型 softirq 可在不同 CPU 上并行执行(SMP 友好),但 同类型 softirq 在同一 CPU 上串行(执行前会关闭BH)。
2.2 softirq 触发与执行路径
softirq 的执行入口有两条主路径:
- 硬中断退出时:在
irq_exit()中调用invoke_softirq() - ksoftirqd 内核线程:softirq 被高频触发时(如网络洪峰),避免单CPU长期关中断,唤醒
ksoftirqd/N在进程上下文处理
执行路径源码追踪(6.6内核):
// kernel/softirqd.c → invoke_softirq()
static void __invoke_softirq(void)
{
// 1. 记录当前 pending 位图快照
// 2. 清空中断使能(保证快照一致性)
// 3. 遍历 NR_SOFTIRQS,执行对应 handler
// 4. 若 pending 中仍有位被置位:
// - 重试次数 < MAX_SOFTIRQ_RESTART(10):继续循环
- 超过阈值:wakeup_softirqd() 唤醒内核线程处理
}
关键约束:一次 __do_softirq 最多处理 MAX_SOFTIRQ_RESTART 批 softirq,防止网络洪峰时软中断完全占据 CPU。如果 ksoftirqd 被唤醒,softirq 就在进程上下文执行,此时 BH 仍不可抢占。
2.3 网络栈 NAPI 与 softirq
网络子系统是 softirq 最密集的使用者,NET_RX_SOFTIRQ 由 NAPI(New API)驱动:
// 网卡中断 handler 中(硬中断上下文)
napi_schedule(&my_adapter->napi); // __napi_schedule → raise_softirq_irqoff(NET_RX_SOFTIRQ)
// 触发 NET_RX_SOFTIRQ 后(softirq 上下文)
// net_rx_action() → napi_poll() → driver.poll() → 处理 ring buffer 直到 done 或 budget 耗尽
NAPI 采用中断+轮询混合模式:
- 首个包到达 → 触发硬中断 → 关闭网卡 RX 中断 → 调度 softirq
- softirq 中调用驱动 poll() 批量收包,直到收完或 net_budget(64 包) 耗尽
- 若预算耗尽,重新开启 RX 中断等待下次触发
网络调优参数:
net.core.netdev_budget = 300 # NAPI poll() 单次预算(默认300,可调至600)
net.core.netdev_budget_usecs = 2000 # poll() 最大时间(μs)
net.core.dev_weight = 64 # 每帧 NAPI budget(兼容老驱动)
三、tasklet:softirq 上的动态封装
3.1 tasklet 实现原理
tasklet 是 HI_SOFTIRQ 和 TASKLET_SOFTIRQ 的封装,使用方式比 softirq 动态化:
// 声明与初始化
void my_tasklet_func(unsigned long data);
DECLARE_TASKLET(my_tasklet, my_tasklet_func, (unsigned long)priv);
// 触发
tasklet_schedule(&my_tasklet); // 将 tasklet 挂入 CPU 的 tasklet_vec 链表
// 触发 HI_SOFTIRQ 或 TASKLET_SOFTIRQ
源码实现关键点(kernel/softirq.c):
struct tasklet_struct {
struct tasklet_struct *next;
unsigned long state; // TASKLET_STATE_SCHED | TASKLET_STATE_RUN
atomic_t count; // 禁用计数(>0 时不会执行)
void (*func)(unsigned long); // handler
unsigned long data;
};
3.2 为什么要引入 tasklet
对比直接使用 softirq:
| 维度 | softirq | tasklet |
|---|---|---|
| 注册方式 | 编译期静态(OPEN_SOFTIRQ) | 运行时动态(DECLARE_TASKLET) |
| 并发 | 同类型可多CPU并行 | 同tasklet同一时间仅一个CPU执行 |
| 适用场景 | 高频、硬要求(网络、块层、RCU) | 设备驱动、低频异步处理 |
| 编程复杂度 | 高(需考虑reentrancy、SMP) | 低(内置防并发保护) |
Linux 内核小组的明确建议:优先使用 tasklet,除非对延迟和吞吐量有极端要求(此时直接实现 softirq handler)。
四、Workqueue:可以睡眠的下半部
4.1 workqueue 的设计哲学
softirq/tasklet 永远运行在中断上下文,不能睡眠、不能访问用户空间、不能调用可能触发调度的函数。当需要:
- 执行长时间 I/O 等待
- 分配大内存(GFP_KERNEL 可能睡眠)
- 获取互斥锁(mutex 会睡眠)
- 执行互斥量密集型的复杂工作
workqueue 成为唯一选择。它的本质是把工作委托给专门的内核工作线程,在进程上下文执行。
4.2 workqueue 顶层架构
// 用户接口(进程上下文 → 内存上下文)
schedule_work(&my_work); // 提交到系统级全局 workqueue (system_wq)
schedule_delayed_work(&dw, HZ); // 延迟提交
// 创建自定义 workqueue
struct workqueue_struct *wq = alloc_workqueue("my_wq", WQ_UNBOUND | WQ_HIGHPRI, 0);
queue_work(wq, &my_work);
// 内部执行(kworker 线程)
// kworker/N:X → worker_thread() → process_one_work() → work->func()
workqueue 演进历经三代:
- 原线程池模型(2.6):全局 keventd_wq,线程池管理
- cmwq(Concurrency-Management Workqueue,3.17):统一线程池管理,动态扩容
- cmwq v2(5.x+):NUMA感知、WQ_POWER_EFFICIENT、strict_unbound
4.3 workqueue flags 与选型
| Flag | 含义 | 推荐场景 |
|---|---|---|
| WQ_UNBOUND | 不限绑CPU,线程池统一调度 | 计算密集,需跨NUMA负载均衡 |
| WQ_HIGHPRI | 使用高优先级 kworker(nice -20) | 实时性要求高的工作 |
| WQ_MEM_RECLAIM | 标记可能触发内存回收的工作 | 执行路径中可能调用alloc_page |
| WQ_FREEZABLE | 系统休眠/唤醒时冻结 | 涉及进程状态同步的工作 |
| WQ_SYSFS | 暴露到/sys/devices/virtual/workqueue/ | 需要运行时观察/调优 |
4.4 高性能 queuing:ordered workqueue 与 multi-worker
// 串行 workqueue(FIFO 顺序保证,同一时间仅一个 worker 执行)
alloc_ordered_workqueue("my_ordered", 0);
// 并行 workqueue + max_active 控制
alloc_workqueue("my_wq", WQ_UNBOUND, max_active = 0);
// max_active = 0 → 默认 256 × num_possible_cpus()(UNBOUND)
// max_active 越小 → 消费端反压越强,内存越省,但吞吐越低
五、软中断与下半部选型决策树
综合延迟、并发、睡眠需求三个维度,选型逻辑如下:
1. 能否睡眠(mutex / copy_to_user / alloc(GFP_KERNEL))?
├─ 是 → 必须用 Workqueue(或 threaded irq + workqueue)
└─ 否 ↓
2. 需要处理的数据量是否巨大(如网卡ring buffer)?
├─ 是 → 直接使用 softirq(如 NAPI 在 NET_RX_SOFTIRQ 中)
└─ 否 ↓
3. 是否需要运行时动态创建/销毁?
├─ 是 → Tasklet(或在 threaded irq 中使用)
└─ 否 → 直接使用 softirq handler(高性能驱动通用选择)
是否需要 CPU 间负载均衡?
├─ 是 → WQ_UNBOUND workqueue(或 SOFTIRQ 在多线程中执行)
├─ 否 → 绑CPU workqueue(cache亲和性更好)
六、threaded IRQ:中断线程化利器
Linux 3.0+ 引入了中断线程化(IRQF_THREAD),将硬中断的 handler 全部移到线程上下文:
// 注册 threaded IRQ
request_threaded_irq(irq, hard_handler, thread_fn, IRQF_ONESHOT, "my_irq", dev);
处理流程:
- hard_handler:在硬中断上下文快速判断是否本设备中断,清除中断源(必须非阻塞)
- thread_fn:在 kirqd 线程上下文执行,可以睡眠、可以调用 workqueue
实时内核(PREEMPT_RT)将所有硬中断强制线程化,threaded IRQ 是其主要迁移路径。
七、内核定时器与 hrtimer
7.1 timer wheel 与级联机制
旧版 Linux 使用jiffies + timer wheel管理定时器,最小粒度取决于 CONFIG_HZ(通常 1ms):
unsigned long expires = jiffies + msecs_to_jiffies(10);
add_timer(&my_timer); // timer 到期后 → TIMER_SOFTIRQ 触发 → run_timer_softirq()
时间轮分5级(8bit × 5 = 40bit 覆盖),每级粒度指数递增:
- Level 0: 8 buckets × 1 tick(256 粒度覆盖)
- Level 1: 64 buckets × 256 ticks
- ... Level 4: 覆盖 2^40 ticks
槽位不足时级联(cascade)到低级,调优时注意避免 slot 过多带来的级联开销。
7.2 hrtimer:高精度定时器
hrtimer(High Resolution Timer)提供纳秒级精度,实现原理:
- 红黑树组织定时器节点,按过期时间排序
- 硬件时钟源(TSC/HPET/ARM Timer)编程下次唤醒
- 中断触发 → HRTIMER_SOFTIRQ →
run_hrtimers()遍历到期节点
// hrtimer 初始化
hrtimer_init(&hr, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
hr.function = my_hrtimer_cb;
hrtimer_start(&hr, ms_to_ktime(5), HRTIMER_MODE_REL);
// 回调返回值
HRTIMER_NORESTART // 不重启
HRTIMER_RESTART // 重新设置 expires 并再次加入红黑树(周期唤醒)
八、生产环境调优与诊断
8.1 softirq 系统监控
# 查看每个 CPU 的 softirq 统计(累计)
cat /proc/softirqs
# 输出示例(6 CPU 系统)
CPU0 CPU1 CPU2 CPU3 CPU4 CPU5
HI: 1245 1023 890 452 120 78
TIMER: 234567 345678 123456 234567 345678 123456
NET_TX: 12345 67890 23456 34567 45678 56789
NET_RX: 123456789 234567890 345678901 456789012 567890123 678901234
BLOCK: 567890 678901 789012 890123 901234 0123456
RCU: 345678901 456789012 567890123 678901234 789012345 890123456
8.2 ksoftirqd CPU 占用过高诊断
ksoftirqd/CPU 占用超过 80% 意味着软中断过载,常见原因和解决方法:
| 症状 | 原因 | 解决方法 |
|---|---|---|
| 单 CPU NET_RX 激增 | 网卡多队列RSS未正确配置 | 配置 smp_affinity 分散到更多 CPU |
| 所有 CPU NET_RX 高涨 | DDoS / 小包洪峰 | 启用 RPS/XPS、RFS;调大 netdev_budget |
| BLOCK softirq 暴涨 | NVMe 完成队列深度过大 | 限制 nr_requests,io_poll_delay 调整 |
| TIMER softirq 过高 | 应用高频 timer(如超时轮询) | 迁移到 hrtimer(更轻量),或合并定时器 |
8.3 中断亲和性与 RPS/RFS
# 将中断 irq 42 绑定到 CPU 2
echo 2 > /proc/irq/42/smp_affinity_list
# 配置 RPS(Receive Packet Steering)——软件层将包分流到其他CPU
echo f > /sys/class/net/eth0/queues/rx-0/rps_cpus # 将 RX queue 0 分发到 CPU 0-3
# RFS(Receive Flow Steering)——按流分发到应用所在CPU
echo 32768 > /proc/sys/net/core/rps_sock_flow_entries
echo 4096 > /sys/class/net/eth0/queues/rx-0/rps_flow_cnt
8.4 workqueue 调优参数
/sys/devices/virtual/workqueue/
├── system_wq/ # 全局系统 workqueue
├── system_highpri_wq/ # 高优先级
├── system_long_wq/ # 长耗时工作
├── system_unbound_wq/ # 非绑定
└── system_power_efficient_wq/ # 节能模式
可调参数(每个 workqueue 目录下):
nice # worker 线程优先级
max_active # 最大并行工作数
cpumask # worker 绑定的 CPU mask
power_efficient # 是否优先节能(空闲时降频)
九、内核最新演进
- NAPI:Linux 6.12 引入
gro_count批量GRO(Generic Receive Offload)聚合点数,降低每帧 overhead - mpath(Multipath):6.6 引入
multipath_tcp+ softirq 配合优化 - timer_strict_reclaim:6.1 新增
CONFIG_HZ_PERIODIC改进 tickless 内核 timer 精度 - force_irqthreads:
threadirqs内核参数,将所有中断强制线程化(PREEMPT_RT 场景) - WQ_BH:6.7 新增
WQ_BHflag(Bottom Half),workqueue 回到 BH 上下文(解决睡眠限制的轻量替代)
十、总结与实践指南
Linux内核下半部机制并非相互替代关系,而是分层协作的完整体系:
- 硬件中断触发 → 关中断、ack、计时
- softirq → 快速分发(网络收发、块层完成、定时器、RCU)
- tasklet → 动态设备驱动异步处理
- workqueue → 可睡眠的长时间工作(磁盘 I/O、文件操作、协议栈处理)
- kworker/ksoftirqd → 回落线程,避免中断上下文过长阻塞系统
生产环境中,最优策略是:以 softirq 快速 ACK 硬件 → 以 tasklet 快速分发 → 以 workqueue 执行耗时工作 → 以 kworker 线程兜底过载。理解每一层的约束和代价,才能在不浪费 CPU 的前提下实现低延迟和高吞吐的统一。

发表评论 取消回复