为什么中断要拆两半:快响应与重活不可兼得
网卡收到包,触发硬中断(hardirq)通知 CPU。硬中断的执行环境极其苛刻:关抢占(当前核上其他中断被屏蔽)、不能睡眠、必须尽快返回。把整个收包处理(协议栈解析、socket 查找、数据拷贝)都塞进硬中断,意味着高流量时中断屏蔽时间过长——时钟中断被延迟、系统失去响应。
Linux 的方案是把中断处理拆成两半(第 11 篇讲过信号与中断的通盘框架):
- 上半部(hardirq):只做最紧急的事——应答硬件、把数据从网卡 DMA 环形缓冲摘下来挂到收包队列(本系列第 14 篇讲过这条 DMA 路径),然后触发软中断标记立即返回。微秒级。
- 下半部(softirq):真正的重活——协议栈处理、socket 分发。它在「中断返回后的时机」或「ksoftirqd 内核线程」里执行,可以被更高优先级工作抢占。
这个设计的中文名字就叫「中断下半部」,它带来的直接后果就是本篇的主角现象:top 里 si(softirq)列占满某个 CPU——不是故障,是架构的可见面。
softirq 的执行时机与 budget:为什么单核会被打满
softirq 不绑定线程,它是一组「待执行的下半部类型」(NET_RX/NET_TX/TIMER/TASKLET…),在三个时机被检查执行:硬中断返回时(irq_exit)、系统调用返回时、以及 ksoftirqd 内核线程被唤醒时。
关键的防饿死设计是 budget:每次执行点最多处理一定量的软中断工作(net_rx_action 的默认 budget 为 300 个包/次,受 net.core.netdev_budget 与 netdev_budget_usecs 控制)——超了就把剩余工作交给 ksoftirqd 线程,自己返回。这保证了软中断不会无限期霸占 CPU,但代价是:包速率超过单核 budget 时,ksoftirqd 持续 runnable,该核的 si 长期 80%–100%。
为什么通常只打满一个核?因为默认配置下,一块网卡的收包中断固定绑在一个 CPU 上(BIOS/内核自动分配的 IRQ 亲和性),该核的中断→软中断→ksoftirqd 全链条都压在同一个核上。包速率再涨,这个核就是瓶颈——其他核闲着,吞吐却上不去。这是「机器不忙但网速上不去」的经典病理。
NAPI:从「每包一中断」到「关中断轮询」
每包触发一次硬中断在高流量下本身就是开销(中断风暴)。NAPI(new API)的思路是混合模式:
- 第一个包到达,触发硬中断,NAPI 调度开始;
- 内核关闭该网卡的硬中断,切换为轮询模式——
poll回调批量从环形缓冲收包(每次最多 budget 个); - 缓冲区空了(流量退潮),重新打开硬中断回到事件模式。
低流量时省电省延迟(事件驱动),高流量时避免中断风暴(批量轮询)——这是第 5 篇 epoll「事件驱动」思想的物理层版本:用轮询替代高频事件的条件是「事件密度高到事件通知本身成为开销」。/proc/interrupts 里看网卡中断计数不再随流量线性暴涨,就是 NAPI 在工作的迹象。
多队列分流:RSS、RPS 与 irqbalance 的取舍
解决「单核瓶颈」的正解是让收包工作分散到多核,三层机制:
RSS(receive side scaling):现代网卡硬件能力——网卡内置多队列,按包头哈希(四元组)把包分发到不同队列,每个队列一个独立的硬中断向量,可绑定到不同 CPU。这是最优解:分流发生在硬件,缓存局部性最好。ethtool -L eth0 combined 8 开多队列,/proc/interrupts 找到队列 IRQ 后写 /proc/irq/N/smp_affinity 绑核(或 irqbalance 服务自动分配)。
RPS(receive packet steering):软件版的 RSS——单队列网卡由内核把包「转发」到其他 CPU 的 backlog 处理。有 IPI(核间中断)开销,效果不及硬件分流,但聊胜于单核硬扛。RFS(flow steering)是其增强版,让包跟随应用所在核,缓存友好度更高。
irqbalance:自动平衡中断的守护进程。优点省心,缺点两条——它可能把同一网卡的队列打散到不理想的核(与 NUMA 拓扑冲突,第 19 篇),且周期性迁移本身造成缓存抖动。高性能服务器的通行做法:关掉 irqbalance,手动把网卡队列的 IRQ 按 NUMA 节点绑定(网卡在 node 0,队列中断绑 node 0 的核)。
# 诊断 si 的标准动作
top -1 # 按核看 si 分布:确认是否单核打满
cat /proc/interrupts | grep eth0 # 各队列中断计数与绑定核
ethtool -l eth0 # 当前队列数
ethtool -x eth0 # RSS 哈希配置
mpstat -P ALL 1 # %soft 按核趋势
# 处理
ethtool -L eth0 combined 8 # 开 8 队列
echo <mask> > /proc/irq/<n>/smp_affinity # 按 NUMA 绑核
sysctl -w net.core.netdev_budget=600 # 加大单次预算(治标)
si 异常的完整诊断流程
高 si 告警时按这个顺序走:
- 定位类型:
/proc/softirqs看 NET_RX/NET_TX/TIMER 哪个在涨——绝大多数是 NET_RX。 - 定位核与队列:
top -1找满载核 →/proc/interrupts确认该核绑的网卡队列。 - 判断是流量问题还是配置问题:总包速率(
ip -s link)没超网卡能力却单核打满 → 队列数不足或 IRQ 绑定失败;总速率逼近线速 → 加机器或上内核旁路。 - 检查 budget:
netdev_budget与netdev_budget_usecs是否被限(默认 300 包/2000μs,高速场景可加大,但加大意味着该核更忙——治标)。 - 终极手段:DPDK/XDP(第 9 篇 eBPF 的 XDP 模式可在驱动层丢包/转发,绕开协议栈),把每核处理能力提高一个量级。
一个容易误判的点:si 高 ≠ 异常。满载网卡的正确形态就是「某个(些)核的 si 接近 100%」——判断标准是该核是否成为吞吐瓶颈(包速率不再随负载上涨)与业务延迟是否受影响。盲目「消灭 si」只会消灭吞吐。
一个 si 告警的完整复盘:从告警到参数
把本篇的诊断流程走一遍真实案例。告警:网关机器 si 单核 95%,流量未达峰值但新连接成功率下降。第一步定位类型:/proc/softirqs 里 NET_RX 增速异常,确认是收包软中断。第二步定位核与队列:top -1 显示 CPU3 满载,/proc/interrupts 里 eth0 的唯一队列 IRQ 绑在 CPU3——这块万兆卡只开了一个队列。第三步判断根因:ethtool -l 显示 combined 1(固件默认单队列),总包速率 2.4Mpps 远低于线速——配置问题而非容量问题。第四步修复:ethtool -L eth0 combined 8 开满队列;停 irqbalance;8 个队列 IRQ 按 NUMA(网卡在 node0)绑到 CPU0-7 与 CPU16-23(node0 的核);确认 RPS 未启用(硬件分流足够)。第五步验证:si 均匀分布到 8 个核(各 25% 左右),新连接成功率恢复,吞吐上限从 2.4Mpps 抬到 18Mpps(逼近该卡小包线速)。遗留问题记录:netdev_budget 默认 300 未动(治标项,当前不需要);XDP 未引入(普通网关协议栈够用)。这个案例的元教训:si 告警的第一反应应该是「查配置」而不是「加机器」——单队列默认值、irqbalance 的拓扑失明、NUMA 失配,三件配置层的事故占 si 故障的大多数,参数调优(budget)与架构升级(XDP)排在最后。
与本系列其他章节的勾连
收包的完整旅程在第 14 篇(DMA → 环形缓冲 → NAPI → 协议栈 → epoll 返回)已画成全景图,本篇聚焦其中「下半部怎么跑、跑在哪个核」的调度与拓扑细节。多队列 RSS 与第 19 篇 NUMA 对齐是同一主题(数据局部性)在网络路径上的应用;XDP 是第 9 篇 eBPF 的数据面杀手应用;budget 限制与第 20 篇 OOM 的水位线是同一设计模式(防饿死的预算制)。下一章进入包本身:sk_buff 的结构与一个包在协议栈里的真实成本。
中断与调度器的边界:irqpoll、线程化中断与实时性
软中断的执行位置还有一个常被忽略的维度:线程化中断(threaded IRQ)。内核可以把中断处理注册为内核线程(request_threaded_irq),硬中断只做最小应答,重活在线程上下文执行——线程可以被调度、被抢占、受 cgroup 管控,这是实时系统「把中断从不可抢占的黑盒变成可调度的公民」的手段。网络软中断没有直接线程化开关,但等价物存在:CONFIG_PREEMPT_RT 实时补丁把软中断也线程化(IRQ 线程化全覆盖),延迟敏感场景(音频、工控)的实际延迟从毫秒级抖动压到百微秒以内——这是 PREEMPT_RT 被合并进主线(5.15 起)的最大动因。主线的折中方案是 threaded-irqs 仅用于特定驱动加 irqpoll 模式(中断轮询化)。应用工程师的决策点:普通后端服务不需要碰这层,「si 高就绑核分流」(第 26 篇主线)已经覆盖 95% 的场景;但当你的服务承诺个位数毫秒的硬延迟(实时音视频处理、工控网关),PREEMPT_RT 内核加线程化中断加 CPU 隔离(isolcpus)就是进入另一档工程的门票,验收标准也从「平均快」变成「最坏情况可控」(P9999 而非 P99)。知道这层边界存在,可以避免两个错误:把实时工程复杂度带进普通服务(无谓的运维负担),或在需要它的场景里用普通手段硬扛(不可控的延迟尾巴)。
网络栈的 CPU 视角监控:把 si 纳入容量模型
si 的观测要升级到容量模型层面才好用:单核 si 百分比是「现象」,「每核每秒能处理多少包」才是容量常数。测法:流量爬坡压测(tcpreplay 或真实流量增长),记录单核 si 从 30% 到 95% 对应的 pps(ip -s link 差分),得到该核的包处理能力曲线——通用 x86 + 万兆卡的典型值 0.3-0.5 Mpps/核(含协议栈),开 GRO 后大包场景 1-2 Mpps/核。有了常数,容量规划变成除法:峰值 pps ÷ 单核能力 = 需要的收包核数,再叠加第 19 篇的 NUMA 约束(收包核要与网卡同节点)与第 31 篇的隔离要求(收包核与业务核分家),得出「这台机器能接多少流量」的硬预算。监控侧的对应动作:si 按核拆分采集(mpstat 或 node_exporter 的软中断指标按 CPU 维度),告警条件是「任一核 si > 80% 持续 5 分钟」而非「机器平均 CPU」——平均值的盲区恰恰是本篇全部问题的藏身处。一个反直觉的容量事实作结:RSS 8 队列绑 8 核后,si 的可扩展性上限是「8 核 × 单核能力」,超过它不是加队列能解决的(队列数受网卡硬件限制),而是要进入 XDP/多网卡分流的架构决策——容量模型的价值就是把「什么时候该换架构」从救火现场提前到规划会议。
小结
软中断是「把中断处理拆两半」的下半部:硬中断只摘包触发标记,重活(协议栈、socket 分发)在 softirq 上下文执行,budget 限制防止霸占 CPU。默认单队列单核的 IRQ 绑定让高流量只打满一个核,NAPI 用「关中断轮询」消灭中断风暴,RSS 多队列加 NUMA 感知的 IRQ 绑核是吞吐扩展的正解,irqbalance 的自动化反而常帮倒忙。si 高不一定是病——判断标准是单核是否成为吞吐瓶颈;诊断动线五步(类型、核与队列、根因定性、修复、验证)把模糊的告警拆成有明确对错答案的检查题。XDP 的「早点说不要」是这条路径的终极省钱法则。
读者留言
COMMENTS 暂无还没有留言,来说第一句?