中断机制是 Linux 内核响应硬件事件的核心基础设施,而软中断(softirq)则是中断处理中"延迟执行"的关键机制。网卡收到数据包、定时器到期、块 I/O 完成等事件,都依赖中断和软_irq_来高效处理。本文将从中断门描述符表的硬件机制出发,深入解析顶半部/底半部、软中断调度、tasklet、workqueue、 threaded IRQ 等机制的原理与代码实现,并结合 NAPI 网络收包、BLOCK 软中断、TIMER 软中断等实际场景进行工程分析。

一、中断的硬件基础与内核入口

x86 架构通过 IDT(Interrupt Descriptor Table)将中断向量映射到处理函数。Linux 内核在初始化阶段填充 IDT 条目,每个条目包含段选择子、偏移地址和权限级别(DPL)。硬件中断的入口路径为:

  • CPU 响应中断:压栈 EFLAGS/CS/EIP,关中断(IF=0),跳转至 IDT 中的处理地址
  • 汇编入口:entry_INT80_32 / idtentry 宏保存寄存器上下文到 pt_regs
  • 分发:do_IRQ() → handle_irq_event() 遍历 IRQ 上的 action 链表

ARM64 架构类似但使用不同的向量表结构:VBAR_EL1 寄存器指向 16 个异常向量入口,同步异常、IRQ、FIQ、SError 各有两张(来自 EL0 和 EL1)。

二、顶半部与底半部:为什么需要拆分

硬件中断处理必须尽可能快。如果在关中断的状态下完成全部工作,会导致中断延迟急剧增加、系统响应性下降。因此 Linux 将中断处理拆分为:

  • 顶半部(Top Half):关中断执行,做最紧要的应答(读取状态、ACK 中断),然后标记底半部待执行
  • 底半部(Bottom Half):开中断或进程上下文中执行耗时的数据处理

Linux 内核提供了四种底半部机制,按抽象层次从低到高排列:软中断(softirq)、tasklet、工作队列(workqueue)、线程化中断(threaded IRQ)。它们之间的核心区别在于执行上下文、是否允许睡眠、以及是否可并行。

三、软中断(Softirq):最底层的延迟机制

3.1 软中断的类型与优先级

软中断是静态编译时注册的(不能在运行时动态添加),定义在 include/linux/interrupt.h 中的枚举 softirq_nr。当前内核定义的软中断类型及其优先级顺序为:

  1. HI_SOFTIRQ — 高优先级 tasklet(softirq_action)
  2. TIMER_SOFTIRQ — 定时器软中断(已不再直接使用,由 hrtimer 替代)
  3. NET_TX_SOFTIRQ — 网络发送
  4. NET_RX_SOFTIRQ — 网络接收(NAPI 轮询入口)
  5. BLOCK_SOFTIRQ — 块设备 I/O 完成
  6. IRQ_POLL_SOFTIRQ — 中断轮询
  7. TASKLET_SOFTIRQ — 普通 tasklet
  8. SCHED_SOFTIRQ — 调度器负载均衡
  9. HRTIMER_SOFTIRQ — 高精度定时器
  10. RCU_SOFTIRQ — RCU 回调

3.2 softirq 的执行模型

软中断的核心数据是每个 CPU 的 softirq_ctrl 结构中的待处理位掩码 __softirq_pending。触发软中断调用 raise_softirq() 设置对应位标记,然后在以下时机执行:

  • 中断返回路径:irq_exit() → invoke_softirq()(硬中断上下文,每个 CPU 执行最多 MAX_SOFTIRQ_RESTART=10 次循环)
  • 软中断守护进程:每个 CPU 的 ksoftirqd 内核线程,在软中断被高频触发时接管执行
  • 显式检查点:local_bh_enable()、_cond_resched() 等开底半部时检查

关键限制:软中断不可睡眠,同一软中断类型可在不同 CPU 上并行执行(无需像 tasklet 那样做串行化),但软中断不可被自己抢占。

3.3 网络接收软中断与 NAPI

现代高速网卡驱动使用 NAPI(New API)混合中断+轮询模式。当网卡收到数据包触发硬件中断时,驱动顶半部关闭网卡中断并调用 napi_schedule()。在软中断上下文中,NAPI 轮询函数 poll() 从环形缓冲区(Ring Buffer)中批量收包:

  1. 驱动注册 _struct napi_struct,包含 poll 函数指针和 weight(预算,默认 64)
  2. 触发软中断后,net_rx_action() 遍历 per-CPU 的 poll_list 并调用 poll()
  3. 每次轮询最多处理 budget(默认 300)个 NAPI 结构,以防止软中断饿死进程
  4. 如果未完成,设置下次轮询时间并返回, Hardware interrupt 暂时关闭以保持低延迟

XDP(eXpress Data Path)则更激进:在驱动收包后但在 skb 分配之前,通过 BPF 程序直接处理甚至回写数据包,实现单核 20Mpps+ 的转发性能。

四、Tasklet:基于软中断的串行化延迟机制

Tasklet 是基于软中断的动态底半部机制。它封装了 TASKLET_SOFTIRQ 和 HI_SOFTIRQ 两个软中断向量,提供以下特性:

  • 动态注册:通过 tasklet_init() 在运行时创建(区别于软中断的静态编译)
  • 串行化保证:同一个 tasklet 不会在多个 CPU 上并发执行( TASKLET_STATE_RUN 标志位保护)
  • 普通优先级:使用 TASKLET_SOFTIRQ(大多数驱动使用此类型)
  • 高优先级:使用 HI_SOFTIRQ(用于低延迟需求的驱动)

Tasklet 适用于不需要睡眠、但需要不同模块独立的延迟处理场景,比如网卡驱动的定时器、块设备的完成通知等。需要注意的是,如果两个不同的 tasklet 访问同一资源,仍需要额外串行化。

五、工作队列(Workqueue):可睡眠的延迟机制

如果延迟处理的逻辑需要使用睡眠操作(如 mutex_lock()、copy_from_user()、alloc-GFP_KERNEL),软中断和 tasklet 就不能满足。工作队列是唯一允许睡眠的底半部机制。

5.1 工作队列的核心数据结构

  • struct work_struct:表示一个待执行的工作项,通过 INIT_WORK() 初始化
  • struct workqueue_struct:工作队列,最流行的使用方式是全局共享的 system_wq、system_highpri_wq、system_long_wq 等预定义队列
  • struct worker:执行工作的内核线程,每个 worker 属于一个 worker_pool
  • struct worker_pool:worker 的集合,每个 CPU 有两个 pool(普通优先级、高优先级)

5.2 CMWQ(Concurrency-Managed Workqueue)

从 Linux 3.18 开始,工作队列引入了 CMWQ 架构。核心思想是:

  • worker 的动态调度:当工作密集时自动创建新 worker,空闲时销毁
  • 并发限制:每个 CPU 同一个工作池的活跃 worker 数量受 max_active 限制(默认普通优先级 256)
  • NUMA 亲和:worker 优先在处理中断的本地 NUMA 节点上执行,减少跨 NUMA 内存访问

5.3 与 tasklet 的选择

对绝大多数设备驱动推荐优先使用工作队列。只有满足以下全部条件的场景才使用 tasklet:

  • 不允许睡眠(无 mutex、无 GFP_KERNEL 分配)
  • 执行时间极短(<1ms)
  • 对延迟极其敏感(需要中断返回后立即执行)

如果不符合以上条件,使用工作队列更安全且可扩展。

六、线程化中断(Threaded IRQ)

Linux 3.1 引入了线程化中断,将传统中断处理的大部分工作直接放入内核线程,避免在中断上下文做大量工作导致的延迟。触发方式是通过 request_threaded_irq() 注册:

  • primary handler:硬中断上下文运行,可以是简单返回 IRQ_WAKE_THREAD 来唤醒线程
  • thread_fn:在专用 IRQ 线程中运行,允许睡眠

典型例子:I2C/SPI 设备驱动需要与硬件进行多步交互(mutex+sleep),适合使用线程化中断。IRQF_ONESHOT 标志会永久禁用中断直到线程处理完成,防止初级处理中的中断反复触发。

七、RCU 软中断与回调执行

RCU(Read-Copy-Update)是 Linux 核心数据结构中广泛使用的无锁同步机制。当 synchronize_rcu() 或 rcu_barrier() 等待宽限期结束后,需要执行回调(如 kfree_rcu() 延迟释放)。这些回调通常在 RCU_SOFTIRQ 中执行,通过 rcu_core() 在每个 CPU 上处理待执行的 RCU 回调。

内核线程 rcuc/N 帮助在极端情况下处理 RCU 回调,防止软中断占用过多 CPU 时间。rcuog/rcuop 线程则处理 RCU 回调中的 offload 操作,以应对网络栈中过重的 RCU 回调负载。

八、性能调优与监控

8.1 软中断 CPU 占用分析

  • cat /proc/softirqs:查看每个 CPU 上各类软中断的累计触发次数
  • top 或 htop:观察 ksoftirqd/%CPU 使用率
  • perf top -e irq:softirq_enter,irq:softirq_exit:火焰图定位软中断热点

8.2 RPS/RFS 中断负载均衡

当单网卡 MSI-X 较少、所有中断绑定到少数 CPU 时,可通过 RPS(Receive Packet Steering)将数据包哈希到多个 CPU 的 NET_RX_SOFTIRQ,实现收包负载均衡。RFS(Receive Flow Steering)则将同一流的数据包发送到应用所在的 CPU,提高缓存局部性。

8.3 自适应中断合并(Adaptive Coalescing)

多数现代网卡驱动支持自适应中断合并,在流量低时减少合并以提升响应速度,在高流量时增加合并以减少中断频率。ethtool -c eth0 查看并配置合并参数。

九、总结

Linux 内核的中断/软中断体系是一个精心设计的延迟处理层次结构,从不可睡眠、不可抢占的软中断,到串行化的 tasklet,再到可睡眠的工作队列和可抢占的线程化中断,每种机制都有其不可替代的场景。理解它们的关键是:

  • 上下文边界:必须区分"不可睡眠"和"可睡眠"的代码
  • 串行化成本:tasklet 保证同类型不并发,适合轻量场景
  • 可扩展性:高吞吐场景优先用 workqueue + per-CPU 数据结构
  • 实时延迟:网络等对延迟敏感的领域需要 NAPI 轮询 + RPS 分发

随着 XDP/eBPF 和 io_uring 的快速发展,未来的内核编程中越来越多的软中断处理逻辑将被推入用户态执行,但理解这些底层机制仍是高性能系统开发的必备基础。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部
0.424723s