硬中断和软中断是怎么分工的
处理高负载服务时,很多人会遇到一个现象:业务进程 CPU 占用似乎不高,但整机延迟在涨,top 里的 si 一栏已经超过 30%,或者某个 ksoftirqd 进程占满了整颗 CPU。这种时候,问题通常不在业务代码,而在 Linux 中断处理路径上的软中断。

硬中断是硬件设备发给 CPU 的异步信号。比如网卡收到数据包后,通过中断通知 CPU 有数据要处理。CPU 会暂停当前任务,跳去执行驱动注册的中断处理函数。这个函数运行在中断上下文里,优先级极高,它会关中断、禁止抢占,所以必须短平快:拷贝数据、清中断标识、唤醒必要的内核逻辑,然后就返回。
如果所有协议栈处理都在硬中断里做,一次网络收包就会让整机长时间暂停响应。所以内核把工作拆成两段:硬中断阶段只做开始的动作,把耗时部分丢给软中断。软中断不是一个线程,而是一组挂在内核全局队列上的回调逻辑,它会在当前 CPU 从异常或中断返回用户态之前被检查一遍。如果处理不完,内核会唤醒 ksoftirqd 内核线程继续处理。
这里要区分两个容易混淆的概念:ksoftirqd 是软中断的执行载体之一,而不是全部。大多数时候,软中断直接在中断返回路径上执行,只有持续不断到达的包才会触发 ksoftirqd 接管。也就是说,你看到 ksoftirqd 高,意味着软中断处理已经跟不上网络速率了。
在实际系统里,最常见的软中断是网络收包对应的 NET_RX,以及定时器 TIMER。很多高负载问题的根源都在这两个现场里。
高负载下,软中断为什么先顶不住
软中断容易成为瓶颈,不是因为内核写得差,而是因为它把大量的协议栈处理集中到了 CPU 的内核态。网络收包是一个明显案例:网卡支持 NAPI 时,硬中断触发频率会被降下来,驱动通过轮询批量拿数据,然后把 sk_buff 交给协议栈。之后 IP 校验、TCP 分片重组、socket 队列、内存分配基本都在 NET_RX 软中断里完成。
当机器跑的是 Nginx 或 Java 网关这类网络型服务时,每秒几十万个包并不夸张。每个包都走一遍协议栈,CPU 的 si 时间自然上涨。问题在于软中断抢占优先级高于普通进程调度,流量一大,用户态业务进程就不断被打断。QPS 还没到上限,P99 延迟先上来了。
另一个细节容易被忽略:软中断处理是局部性的。硬中断可以靠中断亲和性绑定到多个核,但如果驱动不是多队列,或 RPS 没有配置,软中断依然会集中在同一个 CPU 上。多核机器的优势会被这种不均衡吃掉不少。具体你可以观察 /proc/softirqs,很多情况下会看到某个 CPU 上的 NET_RX 计数远远高于其他核。
虚拟化环境里还有一层复杂性。物理网卡产生的中断会先被宿主机处理,再由 vhost 线程投递给 guest。guest 看到的网络软中断,本质上是宿主机给它制造的税。如果宿主机中断绑定不合理,guest 里即使调整内核参数,效果也有限。
如果你维护长连接服务,另一个典型场景是大批量定时器到期。TIMER 软中断会在某个时间点冲高,直接把 CPU 打满。这种问题单看网络参数往往定位不到,因为触发源在应用层的定时器管理。
怎么定位软中断瓶颈
定位软中断问题,不需要上来就用动态追踪。普通命令已经能看到大部分情况。
# 查看各类软中断从开机到现在的累计计数,对比各 CPU 分布
cat /proc/softirqs
# 查看 CPU 时间占比,重点关注 si 和 ksoftirqd 进程
top -1
# 确认热点是否在收包路径上
perf top -G -e cycles
这几个命令组合起来,能回答三个问题:是不是软中断高,是哪种类型的软中断高,分布在哪些 CPU 上。比如 /proc/softirqs 里 NET_RX 在 CPU0 上累计值远高于其他核,说明收包路径集中;top 里 si 时间高但 ksoftirqd 不高,说明软中断多数在中断返回路径上直接消耗 CPU;如果 ksoftirqd 也拉满,说明当前软中断执行已经系统化地霸占了核。
如果确认是 NET_RX 高,下一步看网卡队列:ethtool -l eth0 可以查看当前和最大队列数。多队列网卡配合中断亲和性才能把流量分散到多个核。
三个容易踩的误区
很多团队在处理这类问题时,会陷入几个固定思路。
- 误区一:把硬中断当成主要开销。实际上对于网络包,硬中断只做搬运,大头在软中断的协议栈处理。只调长大网卡 coalesce 参数,减少硬中断频率,不一定能缓解软中断。
- 误区二:硬中断均衡到多个核就够了。硬中断分散,不等于软中断分散。尤其驱动单队列时,所有包仍会进入同一个队列并由同一个队列所在核心处理,后续的软中断自然集中。
- 误区三:si 高就加 CPU。如果软中断只集中在一个核,多核机器加 CPU 并不会让问题自动消失,必须先解决负载均衡。
这些误区的共同点,是把中断数量和中断处理成本混为一谈。真正消耗 CPU 的是处理逻辑在软中断上下文里的执行时间,不是中断触发的次数。
优化手段怎么选
软中断优化的核心思路不是消灭它,而是让处理负载更合理地分布,并减少协调代价。不同的手段适应不同阶段和硬件。
| 手段 | 机制 | 适用场景 | 代价与注意点 |
|---|---|---|---|
| 中断亲和性 | 把网卡硬件中断绑定到指定 CPU | 低负载或队列数小于核数时 | 只影响硬中断,软中断可能仍集中 |
| RPS | 接收路径上按哈希把 skb 分到不同 CPU | 网卡不支持下发多队列 | 增加调度和缓存一致性开销 |
| RFS | 按流维持 CPU 亲和,提升缓存命中 | 长连接多、单连接流量大 | 需要额外 hash 表,内存占用略升 |
| 中断合并 | 攒批再硬中断,降低触发频率 | 追求吞吐,延迟不敏感 | 会提高单包延迟,不适合低延迟服务 |
| Busy Polling | 用户态轮询 socket 收包 | 低延迟高性能中间件 | CPU 占用高,使用场景受限 |
| XDP/eBPF | 在驱动侧或早期路径处理/丢弃包 | DDoS 过滤、高吞吐转发 | 开发成本高,需匹配驱动支持 |
你不需要一开始就上 XDP。多数团队首先要做的是确认硬中断队列数量、配置正确的多队列和 RPS,再把延迟敏感服务的 coalesce 调小一些。这样就能解决大部分软中断集中问题。
落地时值得注意的几个原则
基于实际排查经验,有几个原则值得写下来。
- 先量化再改动。记录 /proc/softirqs 和 top si 的基线,否则无法判断优化是否有效。
- 队列数不要超过物理核数,也不要绑到超线程核上,否则缓存局部性反而变差。
- 硬中断亲和性与 RPS 的设置不要重叠。硬中断已经绑定到的 CPU,不要再参与 RPS 分摊,否则会同时处理物理中断和远程 socket 队列,形成竞争。
- 修改 sysctl 或 ethtool 参数后要做 A/B 对比,重点观察 P99 延迟而不是平均 CPU。
- 在容器环境里,内核参数通常是宿主共享的,排序问题会变成租户隔离问题,需要提前规划。
另外要提一下 scheduler 相关参数。内核里有 net.core.busy_poll,也有调整 ksoftirqd 时间片的参数,但这些只能暂时压低症状,不能改变软中断负载总量。如果确认是某个特定业务导致的软中断高,从应用层减包、合并请求、或减少定时器频率,往往更有效。
怎么理解软中断这个瓶颈
软中断是 Linux 为了兼顾实时响应和系统吞吐设计出来的机制。它在高负载下成为瓶颈,本质上是因为内核态的协议栈处理速度跟不上硬件到达速度。优化时可以把它看成另一个 CPU 消费者:它有自己的优先级、亲和性和配额,需要在业务进程、硬中断和软中断之间做资源分配。
回到最初的问题:为什么高负载下软中断先顶不住?因为它既继承了硬件中断的紧急属性,又没有线程层面的睡眠能力,必须一口气处理完一批包。当到达速率超过处理速率,它就开始霸占 CPU,以牺牲用户态进程为代价换取网络栈向前推进。理解了这条因果链,再做 RPS、中断亲和性、或者上 eBPF,心里就有数了。
如果你下次遇到某个核 si 高、业务延迟抖动,不要第一时间改业务代码。拉着 /proc/softirqs 和 ethtool -l 先看几分钟,大概率能少走很多弯路。
原创文章,作者:fudengji,如若转载,请注明出处:https://fudengji.cn/article/626/