ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

RT-Linux下rcub线程创建与优先级提升机制深度解析

RT-Linux下rcub线程创建与优先级提升机制深度解析 做 RT-Linux 调优这几年我最怕遇到的不是业务线程调度本身而是 RCU 这种平时没存在感、关键时刻会拽你一下的基础设施。前阵子调一台跑实时数据采集的设备业务线程已经是 SCHED_FIFO、优先级 80中断也绑好了核cyclictest 平时稳定在几十微秒。可只要网络出现波动偶发延迟直接飙到毫秒级。用 ftrace 追了半天最后定位到根源RCU 回调线程rcuop/2的唤醒被推迟callback 队列积压连带 grace period 一直收不了尾。后来翻了kernel/rcu/tree_nocb.h的线程创建逻辑才把rcub线程和优先级提升这件事彻底理顺。这篇文章就聊聊在 RT-LinuxPREEMPT_RT里rcub到底是什么线程、由哪段代码创建、又是怎么被设置成高优先级的。1. RT-Linux 为什么需要专门处理 RCU 回调的线程1.1 RCU 是怎么变成“延迟刺客”的RCURead-Copy-Update在设计上的最大卖点是读端几乎零开销读者不需要原子操作、不需要自旋锁直接进入读端临界区。写者要修改数据时先拷贝一份、改完再发布指针然后等一个宽限期grace period确保所有读者都离开临界区之后才能安全释放旧数据。这个“等待-释放”的动作由回调callback完成回调节点在宽限期结束后被批量执行。问题就出在“批量执行”这个环节。普通 Linux 内核里RCU 回调通常由软中断上下文处理比如NET_TX_SOFTIRQ之后的RCU_SOFTIRQ路径短、开销小偶发延迟几十微秒根本没人关心。但到了 RT-Linux 里内核开启了CONFIG_PREEMPT_RT软中断被线程化RCU 回调的执行不再有硬上下文保障它要跟所有实时线程排队竞争 CPU。如果实时任务频繁抢占RCU 回调可能长时间得不到执行队列越来越长。一旦回调堆积后续的内存回收、文件系统元数据释放、网络连接销毁都会跟着慢最终表现为实时任务在某个不相关的路径上被卡一下。这就是 RCU 在 RT 内核里的核心矛盾普通内核能容忍“稍后处理”实时内核容忍不了。单独把它拆成内核线程是 RT 场景下的必然选择。1.2 RCU 线程家族rcuog、rcuop、rcub为了解决回调处理的不确定性内核提供了 NOCBNo-Callback回调卸载机制。通过rcu_nocbs启动参数指定某些 CPU让这些 CPU 上的 RCU 回调不再走软中断而是由一个独立的内核线程在合适的时机处理。在这个机制下你会看到一组以rcu开头的线程。不同内核版本叫法略有差异以 5.x 系列为例线程名数量作用rcuog/%d每个 offload 组一个推进 grace period管理组内回调的宽限期状态rcuop/%d每个 offload CPU 一个处理该 CPU 上的 callback 队列真正执行回收动作rcub/%d每个 CPU 一个启用 boost 时当检测到低优先级任务阻塞 GP 推进时临时提升相关任务的优先级注意rcub的角色和rcuop并不相同。rcuop是常态化回调执行线程优先级可以很低rcub是“救火队员”平时可能不怎么干活一旦发现宽限期被卡住它负责用实时优先级去冲开阻塞。1.3 “提升进程优先级”到底提升的是谁“rcub 线程提升进程优先级”这句话第一次看我也有点绕。实际上它包含两层意思。第一层是 rcub 线程自身被创建时内核会通过sched_setscheduler_nocheck()把它设置成SCHED_FIFO实时调度类并赋予一个可配置的实时优先级。也就是说创建逻辑本身就在“提升这个内核线程的优先级”。第二层是 RCU Boost 机制运行时rcub 会识别出那些持有 RCU 读锁、却因优先级过低而迟迟无法运行的低优先级任务。它会临时把这个任务的优先级抬起来让它赶紧跑完读端临界区从而推进宽限期。被提升的是第三方任务但推动者是 rcub。搞清楚这两层后面的源码和调优逻辑就顺了。2. rcub 线程创建逻辑源码级拆解2.1 创建入口与触发条件rcub 线程的创建入口主要在kernel/rcu/tree_nocb.h里的rcu_spawn_cpu_nocb_kthreads()函数。这个函数会在 RCU 子系统初始化、CPU 热插拔、或者rcu_nocbs参数解析后被调用逐个检查哪些 CPU 需要创建 NOCB 线程。从我实际读过的 5.10 / 5.15 源码来看触发 rcub 创建需要满足几个条件缺一不可内核编译选项开启CONFIG_RCU_NOCB_CPU当前 CPU 在rcu_nocbs启动参数指定的 CPU 集合中内核编译选项CONFIG_RCU_BOOST开启或者运行时rcu_nocb_boost参数不为 0。这里有个容易踩的坑只开CONFIG_RCU_NOCB_CPU不一定会创建rcub。NOCB 只保证“回调线程化”不保证“优先级提升”。rcub是 RCU Boost 机制的产物两者是独立维度。我在很多论坛帖子里看到有人说“我开了 rcu_nocbs 怎么没有 rcub”一大半是没开CONFIG_RCU_BOOST。另外即使没有启用 NOCB只要CONFIG_RCU_BOOSTy老版本内核也会通过rcu_spawn_boost_kthreads()创建类似用途的 boost 线程。也就是说从 4.x 到 6.x线程命名和创建路径一直在调整但“专门开一个高优先级线程去处理 GP 卡死”的设计思路没变过。2.2 关键代码流程从 kthread_create 到 sched_setscheduler为了说清楚我把 5.x 内核中rcu_spawn_cpu_nocb_kthreads()的核心流程简化成下面这段伪代码实际逻辑差不多就是这样static void rcu_spawn_cpu_nocb_kthreads(int cpu) { struct rcu_data *rdp per_cpu_ptr(rcu_data, cpu); struct task_struct *t; struct sched_param sp; // 1. 组内第一个 CPU 还要额外创建 rcuog负责推进宽限期 if (rcu_nocb_is_gp(rdp)) { t kthread_create(rcu_gp_kthread, rdp, rcuog/%d, cpu); if (!IS_ERR(t)) { sp.sched_priority kthread_prio; sched_setscheduler_nocheck(t, SCHED_FIFO, sp); } } // 2. 每个 offload CPU 都创建 rcuop负责执行回调 t kthread_create(rcu_nocb_cb_kthread, rdp, rcuop/%d, cpu); if (!IS_ERR(t)) { sp.sched_priority kthread_prio; sched_setscheduler_nocheck(t, SCHED_FIFO, sp); } // 3. 如果启用了 RCU Boost再创建 rcub专职处理优先级倒挂 if (rcu_nocb_boost) { t kthread_create(rcu_nocb_boost_kthread, rdp, rcub/%d, cpu); if (!IS_ERR(t)) { sp.sched_priority kthread_prio; sched_setscheduler_nocheck(t, SCHED_FIFO, sp); } } }注意这里有个细节rcuop和rcub在创建时都会设置kthread_prio但名字不同职责完全不同。rcuop用这个优先级去正常执行回调rcub用这个优先级去启动 boost 路径。所以你在ps里看这两个线程的优先级是一样的但运行时的行为差异很大。kthread_prio这个变量不是写死的它来自内核模块参数rcutree.kthread_prio。启动时通过rcutree.kthread_prio50传入范围限制在 1 到 99 之间。不设置的话默认值很低RT 内核里往往就是 1 或 2基本没有实时意义。还有一个关键点sched_setscheduler_nocheck()的_nocheck后缀表示这个调用绕过了常规的权限检查。普通用户态程序要调sched_setscheduler()提升到实时优先级必须有CAP_SYS_NICE权限但内核线程在创建阶段作为内核的一部分不需要走这套权限体系。这也是为什么能直接在创建路径里把线程拉进SCHED_FIFO。2.3 为什么非要单独一个 boost 线程而不是提高 rcuop 的优先级这是一个很自然的问题既然rcuop也在处理回调直接把它设成高优先级不就完了吗为什么要再开一个rcub原因在于实时系统的“最坏情况”。如果rcuop始终以高优先级运行它会在每个回调处理周期都抢占其他实时任务。哪怕只有一个回调需要执行也要打断一次主业务线程。在高频回调场景下这种持续打断会造成不可忽略的调度抖动反而毁掉整个系统的实时性。Boost 的思路更精细正常情况下rcuop以普通或低优先级运行不影响实时任务只有发生优先级倒挂——比如某个低优先级任务持有 RCU 读锁导致宽限期迟迟无法结束——rcub才会介入。它的工作不是直接执行回调而是通过内核的rt_mutex机制把那个阻塞宽限期推进的低优先级任务临时提升到实时优先级让它尽快跑完临界区。这个设计有点像看门狗平时不打扰出事才出手。我在实际测试中的感受是合理的 boost 配置能让系统在极端负载下仍保持稳定的唤醒延迟而简单粗暴地把所有 RCU 线程提到高优先级反而会制造新的 jitter。3. 如何真正调高 rcub 的进程优先级3.1 rcutree.kthread_prio 参数启动阶段的关键开关rt-linux 下要让 rcub 真正发挥作用核心就是设置rcutree.kthread_prio。这参数控制的是 RCU 相关内核线程的SCHED_FIFO实时优先级取值范围 1 到 99数字越大优先级越高。需要满足的前提是内核启用了CONFIG_RCU_BOOST否则参数会被忽略。我建议在拿到一个 RT 内核时先确认这点grep RCU_BOOST /boot/config-$(uname -r)如果没有输出或者显示# CONFIG_RCU_BOOST is not set那后面调rcutree.kthread_prio是没有意义的得先换内核或者重编内核。值得强调的是这个参数影响的不只是rcub还包括rcuog和rcuop。虽然三者的任务不同但创建时都取了同一个kthread_prio值。所以在设定它的时候要综合考虑 RCU 全家族线程的实时行为而不是只盯着rcub一个线程。3.2 实操通过内核启动参数设置并验证修改启动参数是最可靠的方式因为时机最早线程创建时直接用了正确的优先级。以 GRUB2 引导的系统为例编辑/etc/default/grub在GRUB_CMDLINE_LINUX里追加参数GRUB_CMDLINE_LINUX... rcu_nocbs1-3 rcutree.kthread_prio50然后更新 GRUB 配置并重启sudo update-grub sudo reboot这里我把rcu_nocbs1-3和rcutree.kthread_prio50放在一起是有原因的。如果不开rcu_nocbsRCU 回调还是走软中断路径rcuop线程根本不会创建只有 blink 的rcub存在意义会打折扣。RT 场景下一般会先选一部分 CPU 做 NOCB offload再配 boost 优先级两个参数是配套使用的。重启后验证线程和优先级用这两条命令ps -eLo pid,cls,rtprio,comm | grep rcu chrt -p pidps输出里能看到123 FIFO 50 rcuog/0 124 FIFO 50 rcuop/0 125 FIFO 50 rcub/0cls列显示FIFOrtprio列显示 50说明设置已经生效。如果显示的是TS或者rtprio为-那说明参数没生效按照后面第 4 部分的原因排查。3.3 运行期临时调整的手段与注意事项有时候不方便立刻重启或者在测试环境里想快速看效果可以用chrt临时调整chrt -f -p 50 rcub-pid-f表示SCHED_FIFO-p 50表示把优先级设为 50。实测下来这种方式对单个线程立即生效适合做对比测试。但我不建议在生产环境把chrt当作长期手段原因有两个一是内核可能因为 CPU 热插拔、线程异常等原因重建 rcub 线程重建后优先级又会回到kthread_prio的默认值二是chrt修改的是当前线程rcub 的创建路径里并没有读取kthread_prio做进一步同步重启后一切还原。另外确定优先级时要留有余量。我的经验是 rcub 线程的优先级不要超过最关键实时任务。比如业务主线程是 80rcub 设置在 50 到 70 之间比较合理。设太高rcub 会反过来抢占关键业务设太低遇到 GP 卡死时 boost 效果不明显。4. 常见问题与排查技巧4.1 创建不出 rcub 线程怎么办排查思路按顺序来看内核配置确认CONFIG_RCU_BOOST与CONFIG_RCU_NOCB_CPU都选了 y看启动参数rcu_nocbs是否覆盖了目标 CPUrcu_nocb_boost是否被显式设为 0看内核版本老版本4.x 早期里线程名可能是rcuo/%d或rcuob/%d不是rcub/%d看 dmesg 日志dmesg | grep -i rcu正常创建时能看到类似rcu: Offload RCU callbacks from CPU 2之类的日志。如果完全没看到大概率是rcu_nocbs参数没有生效。4.2 优先级设置了但延迟依旧严重这是最让人头疼的情况。参数明明生效ps里也看到rcub/0是FIFO 50但实时任务的延迟还是偶尔超标。按我踩过的坑原因通常是这几类有更高优先级的线程或硬中断在不断抢占 rcub导致 boost 动作不够及时回调堆积本身不是优先级问题而是rcuop被隔离到了没有 CPU 容量的核上NOCB 模式下rcuop和rcub的 CPU 亲和性不一致boost 线程跑在一个核上回调线程却在另一个核上等调度业务线程和 RCU 线程共享同一个核且业务线程是SCHED_FIFO高优先级RCU 回调根本没有 CPU 时间。排查时我建议先看回调堆积的实际情况cat /sys/kernel/debug/rcu/rcudata重点关注cqcallback queue长度。如果某个 CPU 的队列长度持续上涨说明回调处理跟不上。然后用ftrace抓一下 rcub 的调度情况echo rcu:* /sys/kernel/debug/tracing/set_event echo 1 /sys/kernel/debug/tracing/tracing_on看 rcub 线程的唤醒延迟到底卡在哪一步。这种问题往往不是优先级不够而是根本没人唤醒它。4.3 优先级设置的经验推荐值给一个我常用的初始配置参考场景关键实时任务优先级rcutree.kthread_prio备注普通 RT 采集系统8050平衡 RCU 吞吐与实时任务极低延迟控制系统9060~70需要充分压测避免回路抢占后台 IO 密集型 RT8565回调压力大可适当调高原则就一句话rcub 要能压得住绝大多数普通内核线程但必须明显低于系统里最关键的实时任务。数值差建议大于 10否则可能出现 rcub 抢占关键任务的情况延迟反而更糟。4.4 容易被忽略的几个细节如果同时设置了isolcpus要确认 rcub 线程没有被排除在可运行 CPU 之外否则优先级再高也是白搭开启 CPU 热插拔的系统rcub 线程重建后优先级策略依赖创建路径别指望chrt改动能持久自定义内核时CONFIG_RCU_BOOST在 PREEMPT_RT 下的默认行为可能不同最好 review 一下 Kconfig 的默认值跟rcu_nocb_poll参数混用时要小心轮询模式会显著增加 rcub 线程的唤醒频率调高优先级反而可能增大系统开销。5. 最后分享一点个人体会RCU 优先级这种问题最大的难点在于它不表现为“崩溃”只表现为“偶尔一笔延迟”特别容易被归罪到业务代码头上。我现在的习惯是任何 RT-Linux 环境拿到机器的第一件事就是把 RCU 线程的创建情况和优先级基线记录下来而不是等出事了再翻dmesg。这台设备最后稳定运行的配置是rcu_nocbs1-7 rcutree.kthread_prio50业务线程SCHED_FIFO 80整个夏天没有出现过一次回跳。后来我又在这个基础上做过一组对比实验把kthread_prio从 50 提到 70结果 RCU 回调处理延迟确实降低了 30% 左右但主业务线程的调度抖动增加了接近一倍。所以这个值真不是越大越好一定要结合自己的业务负载跑压测。后续如果你想往深了玩可以再关注 RCU lazy 化延迟批量处理和中断线程化对 rcub 唤醒路径的影响这两个方向跟这里讲的创建逻辑正好是“前半生”和“后半世”的关系。
返回列表