尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Linux实时任务被节流?一文读懂RT throttling日志与排查调优

Linux实时任务被节流?一文读懂RT throttling日志与排查调优 深夜两点半实物仿真机上的模型跑到第三个工况日志里突然多出一行sched: RT throttling activated。如果你用 Linux 做过实时控制、跑过 Simulink Real-Time、dSPACE 这类实时仿真或者自己用 PREEMPT_RT 内核写过实时主循环对这行字多半不会陌生——它一出现接下来的控制步长很可能开始溢出曲线断层整个系统像被人绊了一脚。我在好几个项目里都撞上过这条日志第一次以为是内核崩了后来才搞明白它其实是 Linux RT 调度器的一盏报警灯某个 CPU 上 SCHED_FIFO/SCHED_RR 实时任务的 CPU 预算被耗尽调度器把它们暂时冻结了。这篇文章会把这条日志的来龙去脉、背后的参数逻辑、完整排查命令和处置方案一次性讲透适合正在和这条日志搏斗的工程师也想把 RT 调度机制彻底搞清楚的读者。1. 这一行日志到底在说什么RT 预算被耗尽1.1 日志来自内核的哪个环节为什么它往往只出现一次这条日志不是硬件报警也不是某个驱动随手打的它来自内核调度器的实时调度路径具体说就是kernel/sched/rt.c里 RT 运行队列被节流throttle的逻辑。当调度器发现当前周期内实时任务累计运行时间超限就会把这个 CPU 的 RT 队列标记为 throttled同时打印这条消息。字符串里的 Sched 是调度器子系统的统称RT 指的是实时调度类throttling 是节流动作三个词放在一起就是说实时任务的运行配额用完了系统开始限制它。比较关键的一点是内核在这里用的是带 once 语义的打印也就是说正常前提下整台机器从开机到关机这条消息只会出现一次。很多工程师在 dmesg 里看到它一次以为只是偶发抖动结果问题后续反复出现却没有任何新日志就是这个原因。千万别用这条日志出现了几次来判断问题的严重程度它更像一个第一次按下就卡住的报警灯只负责告诉你这种事发生过了。恢复时也不一定会有对应的 deactivated 日志所以靠 dmesg 还原节流时间线是行不通的要用后面讲到的实时状态文件来观察。1.2 period 和 runtime内核发给实时任务的信贷额度要理解节流先看两个内核参数在/proc/sys/kernel/下可以直接读取参数默认值含义kernel.sched_rt_period_us1000000RT 预算周期默认 1 秒kernel.sched_rt_runtime_us950000每个周期内 RT 任务最多累计运行 0.95 秒简单说内核给每个 CPU 的实时运行队列发了一张通行证在一个 1 秒的周期里SCHED_FIFO 和 SCHED_RR 任务加起来最多跑 950 毫秒剩下 50 毫秒必须让给普通进程CFS 调度类。一旦某个周期的 RT 累计运行时间达到 950 毫秒这个队列就被标记为节流状态里面的实时任务全部暂停直到下一个周期开始。这个额度是按每个 CPU 运行队列分别记账的不是整机一共 0.95 秒。8 核机器上每个核各有自己的 rt_rq各自有 950ms 的额度同时内核实现了 RT runtime 共享机制某个核没用完的额度可以被其他核借用。所以实际触发条件并不是整机 RT 负载很高而是某一个核上的 RT 总占用率持续超过 95%。这点在排查时非常重要很多人拿着top一看整机负载也就 30%怎么也想不通为什么会被节流就是因为没把视角切到单核粒度。1.3 触发之后系统里到底发生了什么一旦节流激活运行队列里的实时任务进入有能力但没资格跑的状态它们仍然是 runnable但调度器选任务时不会选它们。此时这个 CPU 如果有普通进程就会去跑普通进程没有就 idle。同时内核会挂一个定时器到下个周期边界重新评估并解除节流。整个过程对用户态程序来说就像被人从 CPU 上硬生生拽下来几十毫秒。用个生活类比通行证规定你 1 分钟内在高速上最多跑 57 秒超时就必须在服务区等到下一分钟整点才能继续。对普通应用来说这 50ms 微不足道但对一个 1kHz 的固定步长控制回路来说少跑 50ms 就是 50 拍的数据断档。更麻烦的是节流不会主动把实时任务搬到其他核心去执行除非内核的 push/pull 逻辑恰好介入否则任务就这样被晾着直到下一周期。所以对实时应用来说这是硬伤不是抖动。2. 内核为什么要把实时任务锁起来2.1 历史教训一个失控的 FIFO 任务能拖垮整台机器如果你觉得 95% 这个上限太苛刻先看看没有它的年代会发生什么。早年内核里一个 RT 线程如果写了死循环或者某个驱动把中断线程优先级设到 99整个系统会直接锁死普通进程永远抢不到 CPU你在终端敲任何命令都没反应连 SSH 都登不进去只能按电源键。问题在于 SCHED_FIFO 这类实时调度策略的设计初衷是只要可运行就必须优先但它没有内置任何自我约束一个写崩了的实时任务可以把整台机器变成砖头。内核在 2.6.25 时代引入了 RT 带宽控制机制也就是sched_rt_period_us和sched_rt_runtime_us这套东西目的非常朴素即使实时任务写得再烂系统也要保证每个周期至少有一段窗口能让普通进程运行给管理员留一条救命的通道。这个机制不是给实时任务添堵而是给整个系统上了一份保险。2.2 95% 与 5% 的权衡逻辑从数字上看950ms/1000ms 相当于把单个核 95% 的算力优先给了实时任务剩下的 50ms 是特意留出来的呼吸窗口。内核并不是真的给普通进程预留了 5% 的 CPU——如果在那个窗口里根本没有普通进程需要运行CPU 照样会 idle 或继续跑 RT 任务在下个周期开始后。这里的本质是实时任务可以无限次使用超过预算的 CPU但每个周期必须强制让出一小段。这个设计假设是实时任务偶尔会超出预算但大多数时候不会持续超纲。对于事件驱动型的实时应用比如驱动、网络协议栈、信号处理这个假设基本成立95% 的上限绰绰有余。但对于持续重负载的固定步长控制回路一条腿已经踩在红线边缘了只要某一拍的运行时间稍微变长累计时间就会冲过 950ms 大关然后被狠狠冻住一整个周期。2.3 RT_GROUP_SCHED 带来的行为变化很多发行版的内核默认打开了CONFIG_RT_GROUP_SCHED也就是 RT 组调度。这个选项打开后节流的记账单位不再只是单个 CPU 运行队列而是每个任务组task group也要按同样的周期和额度来约束。你在/sys/fs/cgroup/cpu/下看到的cpu.rt_period_us、cpu.rt_runtime_us就是组调度的控制文件。打开组调度后一个容易被忽略的后果是某个 cgroup 里的 RT 任务即使整个系统预算没用完也会被自己组的额度卡住。换个说法组调度让额度这件事从全局细化到了分组权限隔离更清晰但也让排查多了一个维度。如果你在容器里跑实时任务先检查容器对应 cgroup 的 RT 额度否则全局参数调得再大也没用。3. 什么负载会触发它典型场景与可复现排查链路3.1 三类最容易踩中的场景第一类是单个 RT 线程忙等。用sched_setscheduler把线程设为 SCHED_FIFO 99然后在循环里做纯计算不睡不等一个核的占用率直接 100%每秒钟必触发一次节流。这种写法常见于新手写的测试程序也是最容易复现的场景。第二类是实时仿真或控制回路占用率过高。比如你在普通工控机上用 Ubuntu 加 PREEMPT_RT 内核跑 Simulink 模型或者自写固定步长控制主循环任务本身不是死循环但每一拍的计算量太大。一个 1kHz 的控制回路每拍算 0.96ms占用率 96%已经顶穿红线平均负载看起来没什么异常但每秒钟会被冻结 50ms控制周期自然就乱了。dSPACE、Speedgoat 这类专用实时硬件一般有自己独立的运行环境不太受 Linux 这层限制但如果你做的是软件在环SIL或快速原型验证跑在普通 Intel 工控机上就一定会遇到这个问题。第三类是多个高优先级线程挤在同一个核上。中断线程IRQ thread常常以 SCHED_FIFO 优先级 98/99 运行控制线程以 60 运行采集线程以 55 运行如果它们通过 CPU 亲和性被绑到同一个核三个线程的占用率叠加很容易冲过 95%。这种情况最隐蔽因为单独看任何一个线程都不算异常。3.2 从 dmesg 到线程级的完整排查命令遇到这条日志我建议按下面的链路一步步走每一步都有明确目的。# 1. 确认日志确实存在并看时间点 dmesg -T | grep -i throttling # 2. 读出当前全局预算 cat /proc/sys/kernel/sched_rt_period_us cat /proc/sys/kernel/sched_rt_runtime_us # 3. 看每个 CPU 的 RT 队列实时状态 grep -E ^rt_rq|rt_throttled|rt_time|rt_runtime /proc/sched_debug第 3 步是定位的核心。/proc/sched_debug里每个 CPU 都有自己的rt_rq段关键字段是rt_time本周期已累计的 RT 运行时间和rt_runtime本周期允许的 RT 运行时间。rt_throttled为 1 表示当前处于节流中为 0 表示正常。多刷几次如果能观察到rt_time反复逼近甚至等于rt_runtime说明这个核的 RT 预算持续吃紧。接下来定位是哪些线程在吃预算# 4. 找出所有实时线程 ps -eLo pid,tid,comm,cls,rtprio,psr,pcpu | grep -E FF|RR # 5. 查看具体进程的调度策略和优先级 chrt -p pid # 6. 按线程观察 CPU 占用 pidstat -t -p pid 1ps输出里的CLS列显示 FFFIFO或 RRRound RobinRTPRIO是实时优先级PSR是当前运行在哪个 CPU 上%CPU是占用率。把所有实时线程列出来按 PSR 分组加一下占用率哪个核超了 95%一目了然。如果现场不方便装 pidstat用top加-H再按P排序也能看到线程级占用。3.3 一次性抖动与持续超标的判断并不是每次看到 throttling 都意味着系统设计有缺陷。有一种情况是某个瞬间多个任务同时想抢 CPU比如控制周期边界正好和高优先级中断对齐短暂超过 950ms之后又恢复正常。这种一次性抖动在日志上无法区分必须靠观察rt_time和rt_runtime的比例变化来判断。我的办法是持续采样至少 10 个周期。如果每个周期rt_time都稳定到达rt_runtime说明是持续超标问题在系统设计如果只是偶尔一次达到且rt_time的峰值离rt_runtime很远那大概率是偶发竞争。另外可以配合perf做一次短时采样看调度切换发生时的上下文perf sched record -- sleep 10 perf sched latencyperf sched latency能统计每个任务的调度延迟和等待时间节流发生时 RT 线程的等待时间会突然出现一个很大的尖峰这个尖峰基本就是被冻结的 50ms。4. 参数调优与方案取舍能调的、别碰的、最后手段4.1 调整全局预算的正确姿势如果确认是持续超标第一步不是急着禁用节流而是算清楚你的实时任务到底需要多少 CPU。默认 95% 确实紧把它放宽到 99% 并不是不行但要意识到普通进程的窗口从 50ms 缩到了 10ms系统抗风险能力明显下降。调整方法是在/etc/sysctl.d/下新建配置文件cat /etc/sysctl.d/99-rt.conf EOF kernel.sched_rt_period_us1000000 kernel.sched_rt_runtime_us990000 EOF sysctl --system另一个思路是加大周期。比如把 period 改成 4 秒runtime 改成 3.8 秒表面上看比例还是 95%但节流窗口从 50ms 变成了 200ms——冻结反而更久对实时任务更糟。所以周期和运行时间要一起考虑对固定步长控制来说宁可缩短周期也不要拉长冻结窗口。调完之后要重新观察rt_time确认峰值降到额定值的 80% 以下才算真正安全。4.2 禁用节流与真实风险kernel.sched_rt_runtime_us-1意味着彻底关闭 RT 节流这也是网上被引用最多的解决方案。执行非常简单sysctl -w kernel.sched_rt_runtime_us-1但我强烈建议你把这一步当作最后手段。关闭节流等于回到了 2.6.25 之前的时代一旦某个 RT 线程出问题整台机器可能连杀掉它的机会都没有。开发机上临时关一下做实验可以产线工控机、无人设备、长时间无人值守的仿真节点都不建议这么干。有些实时发行版会在安装时默认把 runtime 设为 -1那是发行版针对特定场景做的决策不代表你的环境也适合。如果确实需要接近 100% 的预算又不愿意完全禁用可以配合 CPU 隔离来做也就是下一节的内容。4.3 cgroup 环境下的 RT 额度配置在 cgroup v1 的 CPU 控制器下每个子控制组都有自己的 RT 额度文件。先检查当前值再按需设置cat /sys/fs/cgroup/cpu/rt-app/cpu.rt_period_us cat /sys/fs/cgroup/cpu/rt-app/cpu.rt_runtime_us # 设置 1 秒周期、0.95 秒运行时间 echo 1000000 /sys/fs/cgroup/cpu/rt-app/cpu.rt_period_us echo 950000 /sys/fs/cgroup/cpu/rt-app/cpu.rt_runtime_us # 把目标进程放入该组 echo pid /sys/fs/cgroup/cpu/rt-app/tasks不同内核版本对新建控制组的默认值处理不完全一样所以每次操作前先cat确认而不是凭记忆假设。cgroup v2 的情况要特别注意CPU 控制器里的cpu.max是给 CFS 普通进程用的配额RT 带宽在 v2 下没有对应的组级控制文件换句话说cgroup v2 里实时任务的预算仍然由全局的sched_rt_runtime_us决定。如果你在容器里跑实时线程要搞清楚容器用的是 v1 还是 v2 的 cgroup 布局再去决定改哪一层。4.4 CPU 隔离、PREEMPT_RT 内核与组合打法真正适合实时控制场景的解法是把 CPU 隔离和预算调整结合起来。在 GRUB 内核命令行里加上isolcpus2,3 nohz_full2,3 rcu_nocbs2,3isolcpus把 2、3 号核从普通调度中摘出来nohz_full关闭这两个核上的周期性时钟中断rcu_nocbs把 RCU 回调挪走目的都是减少对实时任务的干扰。然后用taskset -c 2把你的实时主循环绑到隔离核上。这里要澄清一个常见误解PREEMPT_RT 实时内核并不会绕过 RT 节流。实时内核的本质是把内核中的不可抢占区间大幅缩短、把自旋锁替换为可优先级继承的实时互斥锁从而把调度延迟压到微秒级但调度器还是同一个调度器sched_rt_runtime_us照样生效。换句话说你换了实时内核该节流还是节流。所以完整打法通常是隔离 CPU 绑定实时任务 在隔离核上把预算放宽或干脆设成 -1。因为隔离核上已经几乎没有普通任务禁用节流的风险被控制在一个很小的范围内。5. 现场排查复盘与实时任务的长期建议5.1 一次实时仿真机的完整复盘之前处理过一台跑实时仿真的工控机现象是模型运行到某工况时固定步长溢出控制曲线出现规律性断档dmesg 里正好躺着sched: RT throttling activated。我按上面的链路查了一遍/proc/sched_debug显示 CPU2 的rt_throttled频繁为 1rt_time每个周期都顶满。再用ps -eLo一看CPU2 上挤了三个实时线程主控制线程SCHED_FIFO 60、数据采集线程SCHED_FIFO 55、还有一个网卡中断线程SCHED_FIFO 98。这三个线程因为历史原因都被 CPU 亲和性绑在了 2 号核上平时占用率不高但模型在某个工况下计算量猛增加上中断线程抢占立刻冲破 95%。处理方式没有改全局预算而是做了三件事把主控制线程改用taskset绑到隔离核 4采集线程降为 SCHED_OTHER只保留普通优先级网卡中断通过/proc/irq/中断号/smp_affinity挪到其他核。改动之后控制核的rt_time峰值降到了额定的 60% 左右断档消失。这个案子最值得记住的一点是问题不在预算太小而在不该挤在一起的任务被放在了一个核上。5.2 实时主循环编写时的预算意识写实时程序时很多习惯能直接从源头避免节流。第一等下一个周期不要用忙等用clock_nanosleep加TIMER_ABSTIME等方式主动睡眠把让出的 CPU 时间交还给调度器。第二实时线程里不要做日志打印、内存分配、文件写入这些可能引发锁等待或页错误的事必要的数据可以写入预分配的无锁环形缓冲区由普通线程负责落盘。第三必要时用mlockall(MCL_CURRENT | MCL_FUTURE)锁住内存页避免运行时缺页。第四设计负载时留出余量如果你把实时主循环的 CPU 占用率控制在 80% 以下即使出现偶发竞争离 95% 红线也还有一段缓冲。对于周期性的实时任务还有一个思路是考虑SCHED_DEADLINE调度类。它基于截止时间做调度带宽控制独立于 RT throttling参数设的是运行时间、周期和截止时间语义上更符合每个周期必须跑完固定工作量的控制场景但它的前提是你的任务确实具备明确的周期特征。5.3 这套经验如何迁移到其他工业实时场景不只是 Linux 下玩 Simulink 或 dSPACE 的人会关心这个问题。做西门子博途、WinCC RT Advanced 这类上位机组态发布的工程师虽然大部分时间面对的是 Windows 环境但一旦把实时采集或控制逻辑部署到 Linux 工控机上同样会撞上调度预算的概念。实时性的本质从来都是在限定时间内完成指定计算Linux 的 RT throttling 就是其中一个非常具体的约束点。理解了它再看任何实时平台的手册思路都是通的先确认谁在跑实时任务、预算给的是多少、超了会发生什么。我个人排查这类问题时有个习惯第一件事永远不是改参数而是先搞清楚到底是谁在吃预算。参数只是把问题往后挪了挪真正吃掉 CPU 的线程不找到下一次换一个工况还是会翻车。你现在如果再在 dmesg 里看到sched: RT throttling activated应该已经有了一整套应对思路先看它是不是只在首次出现再翻sched_debug找到超标的核顺着线程列表揪出元凶最后才决定是调预算、挪任务还是隔离核心。
返回列表