引言

在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 的执行入口有两条主路径:

  1. 硬中断退出时:在 irq_exit() 中调用 invoke_softirq()
  2. 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:

维度softirqtasklet
注册方式编译期静态(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);

处理流程:

  1. hard_handler:在硬中断上下文快速判断是否本设备中断,清除中断源(必须非阻塞)
  2. 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_BH flag(Bottom Half),workqueue 回到 BH 上下文(解决睡眠限制的轻量替代)

十、总结与实践指南

Linux内核下半部机制并非相互替代关系,而是分层协作的完整体系:

  • 硬件中断触发 → 关中断、ack、计时
  • softirq → 快速分发(网络收发、块层完成、定时器、RCU)
  • tasklet → 动态设备驱动异步处理
  • workqueue → 可睡眠的长时间工作(磁盘 I/O、文件操作、协议栈处理)
  • kworker/ksoftirqd → 回落线程,避免中断上下文过长阻塞系统

生产环境中,最优策略是:以 softirq 快速 ACK 硬件 → 以 tasklet 快速分发 → 以 workqueue 执行耗时工作 → 以 kworker 线程兜底过载。理解每一层的约束和代价,才能在不浪费 CPU 的前提下实现低延迟和高吞吐的统一。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部