04 · softirq 与网络栈:从中断返回路径搬到线程
结论:非 RT 内核在
irq_exit()上就地执行 softirq,最长 2 ms / 10 轮不可抢占,超出才交给 ksoftirqd。RT 内核彻底不在 hardirq 上下文跑 softirq:raise只是置位 + 唤醒,实际执行发生在 irq 线程的local_bh_enable()里或 ksoftirqd 里;local_bh_disable()从「preempt_count 加 512」变成拿 per-CPU 的softirq_ctrl.lock(一把 rt_mutex)。网络栈随之做了一系列 RT 分支:backlog NAPI 恒为线程、__napi_schedule_irqoff退化、qdisc busylock 恒争用、xmit 递归计数改为 per-task。
第一层:谁在什么上下文里跑 softirq
先看结论:RT 彻底不在 hardirq 上下文跑 softirq,2 ms 的不可抢占黑洞消失;网络栈为此做了一整套 RT 分支。
第二层:kernel/softirq.c 的两个分支
先看结论:
local_bh_disable()从加计数变成拿 per-CPU 锁,pending softirq 在bh_enable时就地跑或交给 ksoftirqd;ksoftirqd 仍是普通优先级。
计数模型
kernel/softirq.c:91-105:非 RT 的「count」就是 preempt_count;RT 的 count 是 per-CPU 的 softirq_ctrl.cnt 加上 task_struct::softirq_disable_cnt(include/linux/sched.h:1225-1227)。softirq_count() 在 RT 上读任务字段而不是 preempt_count(include/linux/preempt.h:110-116),因此 bh 关闭段可以被抢占、甚至睡眠,而 in_serving_softirq()/in_task() 仍能给出正确答案(:128-133)。
RT 的 local_bh_disable()
kernel/softirq.c:120-127 定义 struct softirq_ctrl { local_lock_t lock; int cnt; };local_lock_t 在 RT 下就是 spinlock_t(include/linux/local_lock_internal.h:127)。__local_bh_disable_ip()(:156-185):
WARN_ON_ONCE(in_hardirq())(:161)——RT 上 hardirq 里不允许local_bh_disable()。- 任务首次进入 bh 关闭段且可抢占时:
local_lock(&softirq_ctrl.lock)+rcu_read_lock()(:165-174)。 - 计数写入 per-CPU 变量并镜像到
current->softirq_disable_cnt(:176-185)。
__local_bh_enable_ip()(:218-264)是 RT 上真正执行 softirq 的地方:最外层退出且有 pending 时,若当前上下文不可抢占则只 wakeup_softirqd()(:244-251),否则把计数调到 SOFTIRQ_OFFSET 后直接 __do_softirq()(:253-259)——在调用者自己的线程、自己的优先级上跑。
raise 与 ksoftirqd
- RT 的
invoke_softirq()(:295-299)只在should_wake_ksoftirqd()(:290-293,即本 CPU 没人持 bh 锁)时唤醒 ksoftirqd;非 RT 版本(:444-465)在没开threadirqs时直接在 IRQ 栈上__do_softirq()。 raise_softirq_irqoff()(:695-710)两种内核共用,差异全部由should_wake_ksoftirqd()承担。- 预算:
MAX_SOFTIRQ_TIME = msecs_to_jiffies(2)、MAX_SOFTIRQ_RESTART = 10(:500-501);handle_softirqs()(:536-603)超预算或need_resched()时唤醒 ksoftirqd。RT 上 ksoftirqd 里不再报告 RCU 静止态(:591-592)。 - ksoftirqd 仍是
SCHED_OTHER(:992-997没有设置 FIFO),所以在 RT 上任何 RT 任务都能压住它——网络吞吐下降的主要来源之一。 SOFTIRQ_ON_OWN_STACK在 RT 下关闭(arch/Kconfig:1046):softirq 在任务栈上跑。- tasklet:
tasklet_unlock_spin_wait()在 RT 上用local_bh_disable(); local_bh_enable();代替cpu_relax()自旋(:876-887),否则抢占了 softirq 线程的任务会活锁。 - idle 路径:
tick-sched.c:1164-1166在 bh 锁被持有时压掉「softirq pending 但要停 tick」的告警。
网络栈的 RT 分支(net/core/dev.c)
| 机制 | 非 RT | RT | 位置 |
|---|---|---|---|
backlog NAPI(netif_rx 入队的包) |
softirq 处理;thread_backlog_napi 参数可改线程 |
恒为 napi/backlog-N 线程,use_backlog_threads() 返回 true |
net/core/dev.c:204-225, 12297 |
| backlog 队列锁 | local_irq_save |
spin_lock_irqsave(&sd->input_pkt_queue.lock) |
:229-244 |
__napi_schedule_irqoff() |
假定硬中断已关的快路径 | 退化为 __napi_schedule() |
:6339-6351 |
| busy poll | preempt_disable() 包裹 |
不关抢占 | :6540-6602 |
| qdisc busylock | 仅 qdisc_is_running() 时争用 |
恒争用(持锁者可能被抢占) | :3957-3967 |
xmit 递归计数 / xmit_more / skip_txqueue |
per-CPU softnet_data.xmit |
per-task current->net_xmit(task_struct 新增字段) |
include/linux/netdevice.h:3304-3339, 4983-5003;include/linux/sched.h:1021-1023 |
| 软 IRQ 合并默认 | gro_flush_timeout=20000、napi_defer_hard_irqs=1 |
不开 | :11202-11210 |
| per-CPU 缓存保护 | bh 关闭即独占 | local_lock_nested_bh() 显式加锁(napi_alloc_cache、process_queue、ipv4_tcp_sk、bpf_sp、sigpool…) |
net/core/skbuff.c:284, 320-367, 762-859;net/core/dev.c:6113-6276;net/ipv4/tcp_ipv4.c:892, 1018 |
| NAPI 预算 | netdev_budget=300、2 jiffies |
同 | net/core/hotdata.c:13-15 |
NET_RX_BUSY_POLL |
default y | default y 仅当无 NETCONSOLE | net/Kconfig:345 |
netif_rx()(:5374-5387)在任务上下文调用时自动 local_bh_disable()——RT 上驱动都从 irq 线程调用它,所以这条路径成为常态。
ARM64 注
- 非 RT 的 arm64 在独立 IRQ 栈上跑 softirq:
arch/arm64/Kconfig:270select HAVE_SOFTIRQ_ON_OWN_STACK,arch/arm64/kernel/irq.c:84-86do_softirq_own_stack()→call_on_irq_stack();RT 下SOFTIRQ_ON_OWN_STACK被arch/Kconfig:1046关闭,softirq 在任务栈上执行(THREAD_SIZE需容纳得下网络栈深度,arm64 默认 16K)。 - 网卡驱动在 arm64 SoC 上多为 DT 平台设备(stmmac、fec、dwmac 等),中断为 GIC SPI,可线程化;
napi/、ksoftirqd的亲和性同样要落在 housekeeping CPU。 - TSN:
taprio的门控定时器用HRTIMER_MODE_ABS(net/sched/sch_taprio.c:1953, 2077),RT 下变 soft 到期、在 softirq 线程里跑,软件门控的抖动从几 µs 升到几十 µs——车载以太网时间敏感流量应使用硬件 offload(taprio flags 0x2)或etf的硬件SO_TXTIME;gPTP(ptp4l)本身跑用户态,给它 FIFO 优先级即可。
性能影响
净影响:softirq 不再贡献最坏延迟;网络小包 pps 是所有负载中退化最大的一项(60%–90%)。
| 指标 | 非 RT | RT | 说明 |
|---|---|---|---|
| softirq 造成的最坏抢占延迟 | ≤ 2 ms(一轮预算)+ 排队 | ≈ 0(在线程里,可抢占) | 核心收益 |
| softirq 执行开销 | 零切换(IRQ 栈上) | 至少一次线程切换;bh 锁进出 | |
| 网络小包 pps | 100% | 60% – 90% | 线程化 + backlog 线程 + busylock + 无软合并 |
| 网络大包带宽 | 100% | 85% – 98% | 每包开销被摊薄 |
| NAPI 轮询被抢占 | 不会 | RT 任务可随时抢占 → 环满丢包 | 需把网卡 irq 线程/ksoftirqd 固定在 housekeeping CPU |
| bh 段之间的干扰 | 不可抢占 → 无 | 同 CPU 两个 bh 段串行在一把锁上,可能睡眠等待 | 有 PI,但多一次切换 |
源码定位
| 主题 | 位置 |
|---|---|
| 计数模型说明 | kernel/softirq.c:91-119 |
softirq_ctrl |
kernel/softirq.c:120-136 |
__local_bh_disable_ip / __local_bh_enable_ip(RT) |
kernel/softirq.c:156-264 |
should_wake_ksoftirqd / invoke_softirq(RT vs 非 RT) |
kernel/softirq.c:287-299, 418-465 |
do_softirq_post_smp_call_flush(RT 专有) |
kernel/softirq.c:301-319 |
| 预算与主循环 | kernel/softirq.c:487-614 |
run_ksoftirqd / 线程定义 |
kernel/softirq.c:944-997 |
| tasklet RT 处理 | kernel/softirq.c:868-919 |
irq_exit 分叉点 |
kernel/softirq.c:652-665 |
softirq_count() RT 定义 |
include/linux/preempt.h:110-133 |
| 网络栈 RT 分支 | net/core/dev.c:204-244, 3957-3967, 6339-6351, 6540-6602, 7003-7050, 11202-11210;include/linux/netdevice.h:3302-3339 |
调优要点
ps -eo pid,cls,rtprio,psr,comm | grep -E 'ksoftirqd|napi/':确认它们都在 housekeeping CPU;必要时把关键网卡的napi/或irq/线程chrt -f到合适优先级。- 吞吐优先的网卡:
ethtool -C <if> rx-usecs 50 adaptive-rx on做硬件合并;sysctl net.core.netdev_budget_usecs适当增大。 - RT 应用不要依赖
local_bh_disable()提供的「独占本 CPU」语义(RT 下它不成立);内核代码同理,per-CPU 数据要用local_lock/local_lock_nested_bh。
小结
softirq_ctrl一把 per-CPUlocal_lock_t就是 RT 的 bh 语义;计数镜像到task->softirq_disable_cnt。irq_exit()在 RT 上只唤醒 ksoftirqd;softirq 的真正执行点是 irq 线程的local_bh_enable()。- 网络栈:backlog NAPI 恒线程化、busylock 恒争用、xmit 状态 per-task、默认不开软 IRQ 合并——都是为了「持 bh 的任务可能被抢占」。
- 承上启下:softirq 里跑得最多的是定时器到期——下一章讲 RT 怎样把定时器也分流到线程。