Linux RT 分析Linux 6.12.107

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 分支。

非 RTirq_exit 上最长 2ms 的不可抢占段
非 RT:irq_exit 上最长 2ms 的不可抢占段
同一场景 · 换成 PREEMPT_RT 内核
PREEMPT_RTsoftirq 在 irq 线程 / ksoftirqd 里,可被 RT 任务抢占
RT:softirq 在 irq 线程 / ksoftirqd 里,可被 RT 任务抢占

第二层:kernel/softirq.c 的两个分支

先看结论local_bh_disable() 从加计数变成拿 per-CPU 锁,pending softirq 在 bh_enable 时就地跑或交给 ksoftirqd;ksoftirqd 仍是普通优先级。

非 RTraise → irq_exit → __do_softirq 循环 → ksoftirqd
非 RT:raise → irq_exit → __do_softirq 循环 → ksoftirqd
同一场景 · 换成 PREEMPT_RT 内核
PREEMPT_RTsoftirq_ctrl 本地锁、bh 段可抢占、pending 在线程里消化
RT:softirq_ctrl 本地锁、bh 段可抢占、pending 在线程里消化

计数模型

kernel/softirq.c:91-105:非 RT 的「count」就是 preempt_count;RT 的 count 是 per-CPU 的 softirq_ctrl.cnt 加上 task_struct::softirq_disable_cntinclude/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_tinclude/linux/local_lock_internal.h:127)。__local_bh_disable_ip():156-185):

__local_bh_enable_ip():218-264)是 RT 上真正执行 softirq 的地方:最外层退出且有 pending 时,若当前上下文不可抢占则只 wakeup_softirqd():244-251),否则把计数调到 SOFTIRQ_OFFSET 后直接 __do_softirq():253-259)——在调用者自己的线程、自己的优先级上跑

raise 与 ksoftirqd

网络栈的 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_xmittask_struct 新增字段) include/linux/netdevice.h:3304-3339, 4983-5003include/linux/sched.h:1021-1023
软 IRQ 合并默认 gro_flush_timeout=20000napi_defer_hard_irqs=1 不开 :11202-11210
per-CPU 缓存保护 bh 关闭即独占 local_lock_nested_bh() 显式加锁(napi_alloc_cacheprocess_queueipv4_tcp_skbpf_sp、sigpool…) net/core/skbuff.c:284, 320-367, 762-859net/core/dev.c:6113-6276net/ipv4/tcp_ipv4.c:892, 1018
NAPI 预算 netdev_budget=3002 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 注

性能影响

净影响: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-11210include/linux/netdevice.h:3302-3339

调优要点

小结