上两篇分别拆解了 收包路径 和 发包路径。你可能注意到,无论收包还是发包,数据路径中最频繁出现的一个词就是”软中断”(softirq)——net_rx_action
运行在
NET_RX_SOFTIRQ,net_tx_action
运行在
NET_TX_SOFTIRQ,整个网络栈几乎全靠软中断驱动。
但如果你用 top 看到一个 CPU 的
si(softirq)占比 90%,你能回答这些问题吗?
__do_softirq()一次最多重入多少轮?什么条件下放弃让给ksoftirqd?ksoftirqd以什么优先级运行?它被调度的延迟怎么测量?- 为什么高 PPS 场景下某些 CPU 的尾延迟突然飙升 5 倍?
- Threaded NAPI 和
ksoftirqd有什么本质区别?什么时候该用哪个?
本文从内核源码出发,完整拆解软中断的调度机制。
一、软中断:不可抢占的延迟执行机制
1.1 什么是软中断
硬中断(hardirq)由硬件触发,必须尽快完成——通常只做”记录事件、触发后续处理”。真正耗时的工作被推迟到软中断(softirq)中执行。软中断运行在中断上下文(不关联任何用户进程),但可以被硬中断打断。
与 tasklet、workqueue 的关系:
| 机制 | 上下文 | 并发 | 典型用途 |
|---|---|---|---|
| softirq | 中断上下文 | 同一向量可在多 CPU 并行 | 网络、块设备、RCU |
| tasklet | softirq 之上 | 同一 tasklet 不可并行 | 老式驱动回调 |
| workqueue | 进程上下文 | 可睡眠 | 需要锁/分配的延迟工作 |
软中断的核心优势是零上下文切换开销——它在硬中断返回后的同一 CPU 上立即执行,不需要进程调度。代价是它不能睡眠、不能持有 mutex。
1.2 十向量优先级体系
内核定义了 10
个软中断向量(include/linux/interrupt.h:551):
enum {
HI_SOFTIRQ = 0, // 高优先级 tasklet
TIMER_SOFTIRQ, // 定时器
NET_TX_SOFTIRQ, // 网络发包
NET_RX_SOFTIRQ, // 网络收包
BLOCK_SOFTIRQ, // 块设备完成
IRQ_POLL_SOFTIRQ, // I/O 轮询
TASKLET_SOFTIRQ, // 普通 tasklet
SCHED_SOFTIRQ, // 调度器负载均衡
HRTIMER_SOFTIRQ, // 高精度定时器
RCU_SOFTIRQ, // RCU 回调(始终最后)
NR_SOFTIRQS // = 10
};__do_softirq() 从 bit 0 扫描到 bit
9,编号越小优先处理。注意
NET_TX_SOFTIRQ(2)在
NET_RX_SOFTIRQ(3)之前——发包完成的清理(释放
skb、重启 qdisc)先于收包处理。这个设计意味着:在同一轮
softirq
循环中,已发送包的内存会先被释放,腾出空间给即将到来的收包分配。
1.3 per-CPU 的 pending bitmask
每个 CPU 维护一个 10 位的 pending
掩码(include/linux/interrupt.h:529):
#define local_softirq_pending() (__this_cpu_read(local_softirq_pending_ref))
#define set_softirq_pending(x) (__this_cpu_write(local_softirq_pending_ref, (x)))
#define or_softirq_pending(x) (__this_cpu_or(local_softirq_pending_ref, (x)))raise_softirq(NET_RX_SOFTIRQ) 只是在当前 CPU
的 pending 掩码上设置 bit 3。设置是原子操作,没有锁——因为
softirq pending 是 per-CPU 变量。
二、softirq 的触发与入口
2.1 谁来触发软中断
网络相关的触发点有三个:
触发 NET_RX_SOFTIRQ:
// net/core/dev.c — napi_schedule_prep() 成功后
void __napi_schedule(struct napi_struct *n)
{
// 将 napi 加入当前 CPU 的 poll_list
list_add_tail(&n->poll_list, &sd->poll_list);
// 触发 NET_RX_SOFTIRQ
__raise_softirq_irqoff(NET_RX_SOFTIRQ);
}触发 NET_TX_SOFTIRQ:
// net/core/dev.c — 驱动 TX 完成或 qdisc 需要重启时
// 1. __netif_schedule() — qdisc 有待发送包
// 2. net_tx_action 中处理 completion_queue
static inline void __netif_schedule(struct Qdisc *q)
{
if (!test_and_set_bit(__QDISC_STATE_SCHED, &q->state))
__netif_reschedule(q);
// 最终调用 raise_softirq_irqoff(NET_TX_SOFTIRQ)
}第三个隐含触发点是
local_bh_enable()——当进程上下文重新允许软中断时,如果
pending 不为零,立即进入 do_softirq()。
2.2 从触发到执行的三条路径
softirq 的执行入口有三条:
1. 硬中断返回路径
irq_exit_rcu() → __irq_exit_rcu() → invoke_softirq()
→ 若 pending != 0: __do_softirq()
特点:零延迟,当前 CPU 立即执行
2. local_bh_enable() 返回
local_bh_enable() → __local_bh_enable_ip()
→ 若 pending != 0: do_softirq()
特点:进程上下文中恢复 BH 时执行
3. ksoftirqd 线程
ksoftirqd/N 线程轮询 local_softirq_pending()
特点:兜底机制,以 SCHED_NORMAL 优先级运行
路径 1 是最常见的网络场景——网卡硬中断 → NAPI schedule →
硬中断返回 → 立即进入 __do_softirq()。
三、__do_softirq() 主循环详解
这是整个软中断系统的核心,在
kernel/softirq.c
中实现。以下是简化后的主循环伪码:
asmlinkage __visible void __do_softirq(void)
{
unsigned long end = jiffies + MAX_SOFTIRQ_TIME; // 2ms
int max_restart = MAX_SOFTIRQ_RESTART; // 10
int softirq_bit;
// 快照当前 pending 并清零(之后新到的 softirq 会在下一轮处理)
__u32 pending = local_softirq_pending();
set_softirq_pending(0);
// 关闭 BH 计数(标记当前在 softirq 上下文)
__local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET);
restart:
// 从 bit 0 到 bit 9 扫描
while ((softirq_bit = ffs(pending))) {
unsigned int vec_nr = softirq_bit - 1;
struct softirq_action *h = softirq_vec + vec_nr;
h->action(h); // 调用处理函数
// vec_nr=2: net_tx_action()
// vec_nr=3: net_rx_action()
pending >>= softirq_bit;
}
// 检查是否有新的 pending
pending = local_softirq_pending();
if (pending) {
if (time_before(jiffies, end) // 还没超 2ms
&& --max_restart > 0 // 重启次数 < 10
&& !need_resched()) // 没有更高优先级进程等待
{
goto restart; // 再来一轮
}
// 放弃:唤醒 ksoftirqd
wakeup_softirqd();
}
__local_bh_enable(SOFTIRQ_OFFSET);
}3.1 三个退出条件
__do_softirq() 在以下任一条件满足时停止
inline 处理,转交 ksoftirqd:
| 条件 | 含义 | 内核常量/变量 |
|---|---|---|
time_before(jiffies, end) 为假 |
已经处理超过 2ms | MAX_SOFTIRQ_TIME = 2 * HZ/100 |
max_restart <= 0 |
已经重启 10 轮 | MAX_SOFTIRQ_RESTART = 10 |
need_resched() 为真 |
有更高优先级进程等待 CPU | 调度器 TIF_NEED_RESCHED 标志 |
关键洞察:在高 PPS
场景下,网卡持续产生硬中断,每次硬中断返回都会重置
NET_RX_SOFTIRQ 的 pending 位。这意味着
__do_softirq() 可能连续重启 10 轮,每轮
net_rx_action() 又有自己的 budget(300 包 /
2ms)。极端情况下,一次 inline softirq
执行可以处理数千个包。
3.2 softirq 中的 net_rx_action 二级 budget
net_rx_action() 在 softirq
内部还有自己的退出条件(详见 03-rx-path
第三节):
void net_rx_action(struct softirq_action *h)
{
int budget = netdev_budget; // sysctl: 300
unsigned long time_limit =
jiffies + usecs_to_jiffies(netdev_budget_usecs); // sysctl: 2000μs
while (!list_empty(&sd->poll_list)) {
struct napi_struct *n = list_first_entry(...);
int work = napi_poll(n, weight); // weight 通常 = 64
budget -= work;
if (budget <= 0 || time_after_eq(jiffies, time_limit)) {
sd->time_squeeze++; // 关键计数器!
break;
}
}
}两级 budget 形成嵌套关系:
__do_softirq() 外层:max_restart=10, time=2ms
└─ net_rx_action() 内层:budget=300, time=2ms
└─ napi_poll() 每次:weight=64
time_squeeze++ 意味着”这一轮
net_rx_action 还有包要处理,但 budget
或时间耗尽了”。如果 __do_softirq()
还能重启,会再次调用 net_rx_action()(重新获得
300 的 budget)。只有当外层也放弃时,才轮到
ksoftirqd。
四、ksoftirqd:兜底的 per-CPU 线程
4.1 ksoftirqd 是什么
每个 CPU 有一个 ksoftirqd/N
内核线程(include/linux/interrupt.h:615):
DECLARE_PER_CPU(struct task_struct *, ksoftirqd);
static inline struct task_struct *this_cpu_ksoftirqd(void)
{
return this_cpu_read(ksoftirqd);
}ksoftirqd 运行在
SCHED_NORMAL(CFS
调度器),优先级与普通进程相同。它的主循环非常简单:
// kernel/softirq.c — ksoftirqd 线程函数(简化)
static void run_ksoftirqd(unsigned int cpu)
{
local_irq_disable();
if (local_softirq_pending()) {
__do_softirq(); // 复用同一个主循环
local_irq_enable();
cond_resched(); // 给其他进程调度机会
return;
}
local_irq_enable();
}4.2 为什么 ksoftirqd 用 SCHED_NORMAL
这是一个深思熟虑的设计权衡:
- 如果 ksoftirqd 优先级太高(如
SCHED_FIFO):高 PPS 时 softirq 会无限饿死用户进程——网卡一直有包进来,ksoftirqd永远不让出 CPU,用户态程序没机会运行recvmsg()消费数据。 - 如果 ksoftirqd 优先级太低:softirq 处理被延迟,网卡 ring buffer 溢出丢包。
SCHED_NORMAL 是妥协——ksoftirqd
与用户进程公平竞争 CPU。在实际高 PPS 场景下,你会看到:
- 硬中断来了 → inline
__do_softirq()处理前 10 轮 - 放弃 → 唤醒
ksoftirqd ksoftirqd可能要等几百微秒才能被 CFS 调度到- 这个调度延迟就是”尾延迟”的根源之一
4.3 ksoftirqd 被唤醒到实际运行的延迟
用 bpftrace 测量 ksoftirqd 的调度延迟:
# 追踪 ksoftirqd 从唤醒到上 CPU 的延迟
bpftrace -e '
tracepoint:sched:sched_wakeup /comm == "ksoftirqd"/ {
@wakeup[tid] = nsecs;
}
tracepoint:sched:sched_switch /comm == "ksoftirqd" && @wakeup[tid]/ {
$lat = (nsecs - @wakeup[tid]) / 1000; // μs
@latency_us = hist($lat);
delete(@wakeup[tid]);
}
'在负载饱和的机器上,你可能会看到 p99 延迟在 200-500μs
之间——这就是 __do_softirq() 放弃后、包在
poll_list 中等待 ksoftirqd
的额外延迟。
五、NET_TX_SOFTIRQ 的特殊性
net_tx_action() 与
net_rx_action() 的行为差异很大。TX 端的 softirq
主要做两件事:
5.1 释放已发送的 skb
驱动在 TX 完成中断中把已发送的 skb 挂到
sd->completion_queue,然后触发
NET_TX_SOFTIRQ。net_tx_action()
负责遍历这个链表并释放 skb:
static __latent_entropy void net_tx_action(struct softirq_action *h)
{
struct softnet_data *sd = this_cpu_ptr(&softnet_data);
// 1. 释放已完成发送的 skb
if (sd->completion_queue) {
struct sk_buff *clist = sd->completion_queue;
sd->completion_queue = NULL;
while (clist) {
struct sk_buff *skb = clist;
clist = clist->next;
__kfree_skb(skb); // 释放内存
}
}
// 2. 重启被暂停的 qdisc
if (sd->output_queue) {
// 遍历 output_queue,对每个 qdisc 调用 qdisc_run()
// 让暂停的发包队列继续发送
}
}5.2 为什么 TX 不像 RX 那样”重”
RX 端的 net_rx_action() 要遍历 NAPI poll
list、调用驱动 poll、走整个协议栈——是真正的”重活”。TX 端的
net_tx_action() 只是释放内存和重启
qdisc,通常很快完成。
这也是为什么你在 /proc/softirqs 中看到
NET_RX 的计数远远大于
NET_TX——不是因为收包比发包多,而是因为 RX
处理更重,更容易触发重新调度。
六、Threaded NAPI:绕过 softirq 的替代方案
6.1 传统模型的问题
softirq 是全局的——一个 CPU 上所有网卡的 NAPI 共享同一个
NET_RX_SOFTIRQ 和同一个 budget。问题:
- 不公平:budget 先到先得,高速网卡可能饿死低速网卡
- 不可控:
ksoftirqd以SCHED_NORMAL运行,无法给网络处理设置更高优先级 - 不隔离:无法用 cgroup 限制网络 softirq 的 CPU 用量
6.2 Threaded NAPI(Linux 6.1+)
从 Linux 5.12 引入、6.1 完善的 Threaded NAPI 为每个 NAPI 实例创建独立的内核线程:
// include/linux/netdevice.h — napi_struct 中的线程字段
struct napi_struct {
// ...
struct task_struct *thread; // 关联的内核线程
// ...
};
// NAPI 状态标志
enum {
NAPI_STATE_THREADED = 8, // 启用线程模式
NAPI_STATE_SCHED_THREADED = 9, // 在线程中调度
};启用方式:
# 方法一:sysfs(per-device)
echo 1 > /sys/class/net/eth0/threaded
# 方法二:ethtool(推荐,per-queue)
# Linux 6.1+启用后的变化:
传统模型:
硬中断 → raise_softirq(NET_RX_SOFTIRQ)
→ __do_softirq() → net_rx_action()
→ 遍历 poll_list → napi_poll()
Threaded NAPI:
硬中断 → napi_schedule() → 唤醒 napi->thread
→ napi_threaded_poll() → napi_poll()
→ 完全绕过 softirq 和 net_rx_action
6.3 Threaded NAPI 的优势
| 维度 | 传统 softirq | Threaded NAPI |
|---|---|---|
| 调度公平 | budget 先到先得 | 每设备/队列独立线程,CFS 公平调度 |
| 优先级控制 | 不可调(softirq 上下文) | 可用 chrt / cgroup 设置线程优先级 |
| CPU 亲和性 | 跟随硬中断 CPU | 可用 taskset 绑定到任意 CPU |
| 尾延迟 | ksoftirqd 调度引入 |
直接线程调度,延迟更可预测 |
| 调试可见性 | softirq 不可见于 ps |
线程可见于 ps、top、perf |
6.4 Threaded NAPI 的代价
- 上下文切换开销:每次 NAPI poll 都是一次线程唤醒/调度,比 inline softirq 多 1-3μs
- 吞吐量略降:在 10Gbps+ 线速转发场景,上下文切换开销可能导致 5-10% 的吞吐下降
- 适用场景:延迟敏感型(Redis、交易系统),而非吞吐量极限型(线速路由器)
6.5 softnet_data 中的线程状态追踪
softnet_data 中有专门的标志追踪当前是在
softirq 还是 threaded NAPI
中(include/linux/netdevice.h:3292):
struct softnet_data {
// ...
bool in_net_rx_action; // 在 softirq net_rx_action 中
bool in_napi_threaded_poll; // 在 threaded NAPI 中
// ...
};这两个标志用于 RPS 投递决策——如果目标 CPU 正在
net_rx_action 中,投递到 backlog 队列;如果在
threaded NAPI 中,行为不同。
七、CONFIG_PREEMPT_RT:softirq 的根本性重写
7.1 RT 内核对 softirq 的改造
在标准内核中,softirq
运行在中断上下文,不可被抢占。这对实时性是致命的——一次
net_rx_action
可能持续数毫秒,期间实时任务无法得到 CPU。
CONFIG_PREEMPT_RT 内核将所有 softirq
转为内核线程执行(include/linux/interrupt.h:599-606):
#ifdef CONFIG_PREEMPT_RT
// RT 内核:softirq 在专门的 kthread 中运行
// do_softirq_post_smp_call_flush 有不同实现
extern void do_softirq_post_smp_call_flush(unsigned int was_pending);
#else
// 标准内核:inline 执行
static inline void do_softirq_post_smp_call_flush(unsigned int unused)
{
do_softirq();
}
#endif7.2 RT 内核的网络行为差异
| 维度 | 标准内核 | PREEMPT_RT 内核 |
|---|---|---|
| softirq 执行上下文 | 中断上下文(不可抢占) | 内核线程(可抢占) |
| 最大连续执行时间 | 受 budget + MAX_SOFTIRQ_RESTART 限制 | 可被实时任务随时抢占 |
| 锁语义 | spin_lock 关闭抢占 |
spin_lock 转为
rt_mutex(可睡眠) |
| 网络吞吐 | 最高 | 下降 10-30%(更多上下文切换) |
| 延迟可预测性 | 差(softirq 不可抢占) | 好(所有路径可抢占) |
| 适用场景 | 通用服务器 | 工控、金融交易、5G 基站 |
7.3 何时需要 RT 内核
对网络来说,PREEMPT_RT
的主要价值是限制最坏情况延迟。如果你的业务对
p99.9 延迟敏感(如交易撮合引擎要求最坏 50μs),标准内核的
softirq 可能无法满足——一轮 net_rx_action 处理
300 个包可能就要 2ms,这期间交易线程完全无法运行。
但对大多数场景,Threaded NAPI 是更好的折中——它不改变锁语义,不牺牲太多吞吐量,同时让网络处理可被调度器管理。
八、/proc/softirqs 与 /proc/net/softnet_stat 解读
8.1 /proc/softirqs
$ cat /proc/softirqs
CPU0 CPU1 CPU2 CPU3
HI: 1 0 0 0
TIMER: 2847362 2891045 2821100 2853912
NET_TX: 18234 17892 19001 18456
NET_RX: 45827134 42891023 44102987 43928761
BLOCK: 892341 901234 887562 895123
IRQ_POLL: 0 0 0 0
TASKLET: 12345 11234 12890 11987
SCHED: 3891234 3912345 3878901 3901234
HRTIMER: 0 0 0 0
RCU: 9812345 9901234 9789012 9834561诊断要点:
- NET_RX 远大于 NET_TX:正常——RX softirq 做的工作远比 TX 多
- CPU 间 NET_RX 不均匀:RSS/RPS 配置不均,某些 CPU 承受更多中断
- NET_RX 持续快速增长:该 CPU 正在高负载处理网络包
- HI 异常增长:tasklet 提升为高优先级,可能挤占网络处理
8.2 /proc/net/softnet_stat
$ cat /proc/net/softnet_stat
# col1:processed col2:dropped col3:time_squeeze
00a1b2c3 00000000 0000045a 00000000 00000000 ... # CPU0
009f8d7e 00000000 00000392 00000000 00000000 ... # CPU1三个核心列(十六进制):
| 列 | 字段 | 含义 | 告警阈值 |
|---|---|---|---|
| 1 | processed |
该 CPU 处理的总包数 | 仅供参考 |
| 2 | dropped |
input_pkt_queue 满导致的丢包数 |
> 0 需要关注 |
| 3 | time_squeeze |
net_rx_action budget 耗尽次数 |
持续增长 = CPU 不够 |
time_squeeze
是最重要的告警指标。它意味着
net_rx_action() 在 budget
或时间限制内没能处理完所有 NAPI poll
list,被迫中断。如果这个计数器持续增长,说明 CPU
算力不足以在 budget 内处理所有收到的包。
解读脚本:
# 每秒打印 time_squeeze 增量
watch -n 1 'cat /proc/net/softnet_stat | \
awk "{printf \"CPU%d: processed=%d dropped=%d squeeze=%d\n\", \
NR-1, strtonum(\"0x\"$1), strtonum(\"0x\"$2), strtonum(\"0x\"$3)}"'九、可观测性实战:bpftrace 与 perf
9.1 追踪 softirq 执行时间分布
# 测量每次 NET_RX_SOFTIRQ 的执行时间
bpftrace -e '
tracepoint:irq:softirq_entry /args->vec == 3/ { // 3 = NET_RX
@start[cpu] = nsecs;
}
tracepoint:irq:softirq_exit /args->vec == 3 && @start[cpu]/ {
$dur = (nsecs - @start[cpu]) / 1000; // μs
@net_rx_duration_us = hist($dur);
delete(@start[cpu]);
}
'典型输出:
@net_rx_duration_us:
[4, 8) 12345 |@@@@@@ |
[8, 16) 28901 |@@@@@@@@@@@@@@@ |
[16, 32) 45678 |@@@@@@@@@@@@@@@@@@@@@@@@ |
[32, 64) 61234 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
[64, 128) 38901 |@@@@@@@@@@@@@@@@@@@@ |
[128, 256) 12345 |@@@@@@ |
[256, 512) 3456 |@@ |
[512, 1K) 891 | |
[1K, 2K) 234 | |
p50 通常在 32-64μs,但注意 512μs-2ms 的长尾——这些就是高 PPS 下 budget 没耗尽但处理量很大的情况。
9.2 追踪 time_squeeze 事件
# 捕获每次 time_squeeze 发生时的堆栈和包数
bpftrace -e '
kprobe:net_rx_action {
@entry[cpu] = nsecs;
}
kretprobe:net_rx_action /@entry[cpu]/ {
$dur = (nsecs - @entry[cpu]) / 1000;
@duration_us[cpu] = hist($dur);
delete(@entry[cpu]);
}
'9.3 追踪 __do_softirq 重启次数
# 统计每次 __do_softirq 的执行时间
bpftrace -e '
kprobe:__do_softirq {
@start[tid] = nsecs;
}
kretprobe:__do_softirq /@start[tid]/ {
$dur = (nsecs - @start[tid]) / 1000;
@do_softirq_us = hist($dur);
delete(@start[tid]);
}
'9.4 perf 火焰图定位 softirq 热点
# 采集 softirq 相关的 CPU 热点
perf record -a -g -e irq:softirq_entry -e irq:softirq_exit -- sleep 10
# 或者直接采集 CPU cycles 并过滤 softirq 相关函数
perf record -a -g -e cycles:k -- sleep 10
perf script | grep -A 20 'net_rx_action\|net_tx_action' | head -100
# 生成火焰图
perf script | stackcollapse-perf.pl | flamegraph.pl > softirq-flame.svg9.5 用 mpstat 观察 softirq CPU 占比
# 每秒看各 CPU 的 softirq 占比
mpstat -P ALL 1
# 关注 %soft 列
# 如果某个 CPU 的 %soft > 80%,说明该 CPU 被 softirq 垄断
# 用户态进程在这个 CPU 上会有严重调度延迟十、实战场景与调优
10.1 场景一:time_squeeze 风暴
症状:/proc/net/softnet_stat
的 time_squeeze 列每秒增长数千。
根因:单 CPU 承受的 PPS 超过 budget
上限,net_rx_action 每次都不够用。
调优方案:
# 方案一:增大 budget(适合少量 CPU 高 PPS)
sysctl -w net.core.netdev_budget=600
sysctl -w net.core.netdev_budget_usecs=4000
# 方案二:分散中断到更多 CPU(根本解决)
# 启用 RSS 多队列(硬件层面)
ethtool -L eth0 combined 8
# 或启用 RPS(软件层面)
echo ff > /sys/class/net/eth0/queues/rx-0/rps_cpus
# 方案三:启用 busy polling(绕过 softirq)
sysctl -w net.core.busy_poll=50
sysctl -w net.core.busy_read=5010.2 场景二:ksoftirqd 尾延迟
症状:应用 p99 延迟偶尔飙升 3-5
倍,ksoftirqd 线程 CPU 占用忽高忽低。
根因:__do_softirq() inline
处理放弃后,ksoftirqd 被 CFS
调度,中间有数百微秒的空窗期,ring buffer
中的包只能等待。
调优方案:
# 方案一:提高 ksoftirqd 优先级(谨慎使用)
# 找到 ksoftirqd PID
pgrep -a ksoftirqd
# 设置实时优先级(仅当你确认不会饿死其他进程)
chrt -f -p 1 <ksoftirqd_pid>
# 方案二:启用 Threaded NAPI(推荐)
echo 1 > /sys/class/net/eth0/threaded
# 然后可以用 chrt 精确控制 NAPI 线程优先级
# 方案三:busy polling(让应用自己 poll)
# 适合延迟敏感的少连接场景
setsockopt(fd, SOL_SOCKET, SO_BUSY_POLL, &timeout, sizeof(timeout));10.3 场景三:CPU 间 softirq 负载不均
症状:/proc/softirqs 显示
CPU0 的 NET_RX 是其他 CPU 的 10 倍。
根因:RSS 哈希不均匀,或所有中断绑定到同一 CPU。
调优方案:
# 查看中断分布
cat /proc/interrupts | grep eth0
# 手动设置中断亲和性
echo 2 > /proc/irq/XX/smp_affinity # 绑定到 CPU1
# 或用 irqbalance 自动均衡
systemctl start irqbalance
# 验证 RSS 均匀性
ethtool -x eth0 # 查看 indirection table
ethtool -X eth0 equal 8 # 均匀分配到 8 个队列10.4 关键 sysctl 参数汇总
| 参数 | 默认值 | 作用 | 与内核实现的对应 |
|---|---|---|---|
net.core.netdev_budget |
300 | net_rx_action 每轮包数上限 |
net_rx_action() 中的 budget 变量 |
net.core.netdev_budget_usecs |
2000 | net_rx_action 每轮时间上限(μs) |
net_rx_action() 中的 time_limit |
net.core.dev_weight |
64 | NAPI poll 的默认 weight | 传给 napi_poll() 的 budget |
net.core.dev_weight_tx_bias |
1 | TX weight 对 RX weight 的倍数 | TX 每轮处理量 = weight × bias |
net.core.busy_poll |
0 | poll() 系统调用的 busy poll 超时(μs) | sk_busy_loop() 中的 timeout |
net.core.busy_read |
0 | read() 系统调用的 busy poll 超时(μs) | sk_busy_loop() 中的 timeout |
十一、总结
软中断是 Linux 网络栈的调度引擎。理解它的行为模式,就能理解高 PPS 场景下大部分性能问题的根源:
__do_softirq()主循环:最多 10 轮重启、2ms 时间限制、need_resched()检查——三个条件决定了 inline 处理的上限- 二级 budget:外层
__do_softirq()控制重启次数,内层net_rx_action()控制每轮包数——两者嵌套形成完整的公平性保障 ksoftirqd的代价:以SCHED_NORMAL运行的兜底线程,调度延迟是尾延迟的核心来源- Threaded NAPI 替代:per-device 线程,可控优先级,适合延迟敏感场景
time_squeeze是核心指标:它直接反映 CPU 算力是否跟得上收包速率- CONFIG_PREEMPT_RT:所有 softirq 转线程,最坏延迟可控,但吞吐量下降——仅用于极端实时场景
下一篇我们进入协议栈内部,从 IP 层的路由查找、分片重组和转发路径开始拆解。
参考文献
- Linux 内核源码,
kernel/softirq.c,6.6 LTS / 6.8 - Linux
内核源码,
include/linux/interrupt.h,6.8(softirq 向量定义于第 551 行,raise_softirq声明于第 613 行) - Linux
内核源码,
include/linux/netdevice.h,6.8(softnet_data定义于第 3280 行,napi_struct定义于第 350 行) - Linux 内核源码,
net/core/dev.c,6.6 LTS(net_rx_action、net_tx_action实现) - Thomas Gleixner, “PREEMPT_RT and Softirqs”, LWN.net
- Jakub Kicinski, “Threaded NAPI”, Linux 5.12 patchset
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【Linux 网络子系统深度拆解】收包路径全解:从 NIC 中断到 socket 接收队列
一个网络包从网卡 DMA 到用户态 recvmsg(),要走过硬中断、NAPI 轮询、GRO 聚合、协议分发、IP 路由、Netfilter 钩子、TCP/UDP 处理、socket 队列八个阶段。本文从 Linux 6.6 内核源码出发,逐函数拆解完整的 RX 收包路径,量化每一跳的 CPU 开销,并用 bpftrace 实测各阶段延迟分布。
【Kubernetes 网络深度系列】Linux 网络栈全景:一个包从网卡到用户态的完整旅程
从 NIC 驱动到用户态 read(),一个网络包在 Linux 内核中到底经历了什么?本文拆解 sk_buff、NAPI、softirq、netfilter 的完整收包路径,并用 bpftrace 实测追踪每一跳的延迟。
【Linux 网络子系统深度拆解】sk_buff 全解:内核网络包的终极容器
sk_buff 是 Linux 内核网络栈的通用货币——每一个收到或发出的网络包,都必须装在这个容器里走完全程。本文从 Linux 6.6 内核源码出发,拆解 sk_buff 的内存布局、四大指针操作、clone 与 copy 的代价差异、skb_shared_info 的 fragment 机制,并用 bpftrace 实测 sk_buff 分配热点和生命周期。
【Linux 网络子系统深度拆解】内核网络追踪工具箱:bpftrace/perf/ftrace 实战
从内核 tracepoint 定义出发,系统讲解 bpftrace、perf、ftrace 三大工具在网络诊断中的实战用法:TCP 重传根因分析、softirq 延迟定位、收发包路径延迟剖析、conntrack 表满监控、per-function 火焰图,以及各工具的适用场景与性能开销对比。