土法炼钢兴趣小组的算法知识备份

【Linux 网络子系统深度拆解】软中断与 ksoftirqd:网络包处理的调度引擎

文章导航

分类入口
linuxnetworking
标签入口
#softirq#ksoftirqd#net_rx_action#net_tx_action#threaded-napi#preempt-rt#bpftrace#linux-kernel#network-stack

目录

上两篇分别拆解了 收包路径发包路径。你可能注意到,无论收包还是发包,数据路径中最频繁出现的一个词就是”软中断”(softirq)——net_rx_action 运行在 NET_RX_SOFTIRQnet_tx_action 运行在 NET_TX_SOFTIRQ,整个网络栈几乎全靠软中断驱动。

但如果你用 top 看到一个 CPU 的 si(softirq)占比 90%,你能回答这些问题吗?

本文从内核源码出发,完整拆解软中断的调度机制。


一、软中断:不可抢占的延迟执行机制

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

这是一个深思熟虑的设计权衡:

SCHED_NORMAL 是妥协——ksoftirqd 与用户进程公平竞争 CPU。在实际高 PPS 场景下,你会看到:

  1. 硬中断来了 → inline __do_softirq() 处理前 10 轮
  2. 放弃 → 唤醒 ksoftirqd
  3. ksoftirqd 可能要等几百微秒才能被 CFS 调度到
  4. 这个调度延迟就是”尾延迟”的根源之一

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_SOFTIRQnet_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。问题:

  1. 不公平:budget 先到先得,高速网卡可能饿死低速网卡
  2. 不可控ksoftirqdSCHED_NORMAL 运行,无法给网络处理设置更高优先级
  3. 不隔离:无法用 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 线程可见于 pstop、perf

6.4 Threaded NAPI 的代价

  1. 上下文切换开销:每次 NAPI poll 都是一次线程唤醒/调度,比 inline softirq 多 1-3μs
  2. 吞吐量略降:在 10Gbps+ 线速转发场景,上下文切换开销可能导致 5-10% 的吞吐下降
  3. 适用场景:延迟敏感型(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();
}
#endif

7.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

诊断要点

  1. NET_RX 远大于 NET_TX:正常——RX softirq 做的工作远比 TX 多
  2. CPU 间 NET_RX 不均匀:RSS/RPS 配置不均,某些 CPU 承受更多中断
  3. NET_RX 持续快速增长:该 CPU 正在高负载处理网络包
  4. 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.svg

9.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=50

10.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 场景下大部分性能问题的根源:

  1. __do_softirq() 主循环:最多 10 轮重启、2ms 时间限制、need_resched() 检查——三个条件决定了 inline 处理的上限
  2. 二级 budget:外层 __do_softirq() 控制重启次数,内层 net_rx_action() 控制每轮包数——两者嵌套形成完整的公平性保障
  3. ksoftirqd 的代价:以 SCHED_NORMAL 运行的兜底线程,调度延迟是尾延迟的核心来源
  4. Threaded NAPI 替代:per-device 线程,可控优先级,适合延迟敏感场景
  5. time_squeeze 是核心指标:它直接反映 CPU 算力是否跟得上收包速率
  6. CONFIG_PREEMPT_RT:所有 softirq 转线程,最坏延迟可控,但吞吐量下降——仅用于极端实时场景

下一篇我们进入协议栈内部,从 IP 层的路由查找、分片重组和转发路径开始拆解。


参考文献

  1. Linux 内核源码,kernel/softirq.c,6.6 LTS / 6.8
  2. Linux 内核源码,include/linux/interrupt.h,6.8(softirq 向量定义于第 551 行,raise_softirq 声明于第 613 行)
  3. Linux 内核源码,include/linux/netdevice.h,6.8(softnet_data 定义于第 3280 行,napi_struct 定义于第 350 行)
  4. Linux 内核源码,net/core/dev.c,6.6 LTS(net_rx_actionnet_tx_action 实现)
  5. Thomas Gleixner, “PREEMPT_RT and Softirqs”, LWN.net
  6. Jakub Kicinski, “Threaded NAPI”, Linux 5.12 patchset

上一篇发包路径全解:从 send() 到网线

下一篇IP 层内核实现:路由查找、分片与转发

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-04-20 · linux / networking

【Linux 网络子系统深度拆解】收包路径全解:从 NIC 中断到 socket 接收队列

一个网络包从网卡 DMA 到用户态 recvmsg(),要走过硬中断、NAPI 轮询、GRO 聚合、协议分发、IP 路由、Netfilter 钩子、TCP/UDP 处理、socket 队列八个阶段。本文从 Linux 6.6 内核源码出发,逐函数拆解完整的 RX 收包路径,量化每一跳的 CPU 开销,并用 bpftrace 实测各阶段延迟分布。

2026-04-20 · linux / networking

【Linux 网络子系统深度拆解】sk_buff 全解:内核网络包的终极容器

sk_buff 是 Linux 内核网络栈的通用货币——每一个收到或发出的网络包,都必须装在这个容器里走完全程。本文从 Linux 6.6 内核源码出发,拆解 sk_buff 的内存布局、四大指针操作、clone 与 copy 的代价差异、skb_shared_info 的 fragment 机制,并用 bpftrace 实测 sk_buff 分配热点和生命周期。


By .