ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Linux RT Throttling激活:实时任务节流原理与调优实践

Linux RT Throttling激活:实时任务节流原理与调优实践 运行Linux实时任务时我最常被问到的不是哪个调度算法更快而是控制台或者dmesg里突然冒出的这行字Sched: RT throttling activated第一次见的人多半心里咯噔一下以为内核崩了或者实时线程被杀了。实际上这是内核在履行“带宽保护”机制你的实时任务暂时被限流了而不是永久失效。对于搞嵌入式Linux、机器人控制、工业实时采集的朋友来说这条日志几乎都绕不开。这篇文章就把这个机制从原理到调参再到排障一次性讲清楚。内容不算长但会把“为什么触发、怎么计算、怎么关闭、关了之后有什么后果”这几个问题串起来适合正在被实时任务卡顿折磨的开发者也适合刚接触Linux RT调度、想搞懂内核那套CPU带宽控制逻辑的初学者。1. 从一条内核日志说起RT throttling 到底是什么1.1 这份日志从哪来什么时候出现先明确“Sched: RT throttling activated”的来源。它来自内核的调度器scheduler模块具体在kernel/sched/rt.c里由start_rt_bandwidth函数触发。意思是某个CPU上的实时调度类任务SCHED_FIFO或SCHED_RR消耗的CPU时间达到了预设上限内核触发节流throttling强制这个CPU上的RT任务暂时停止运行把CPU让给普通任务。触发条件用一句话概括在默认配置下某个CPU核心上运行实时线程的时间超过了该核心CPU总时间的95%。这5%的余量是内核故意留给普通进程的目的是防止RT线程饿死整个系统。注意“某个CPU核心”这个细节。RT throttling是per-CPU每个CPU独立计算的不是全局的。如果你有8个核心一个核心被RT任务占满只会在那个核心上触发节流其他核心不受影响。所以日志打印时会带上CPU编号吗不同内核版本打印信息不太一样常见的是直接打印“Sched: RT throttling activated”后面没有CPU号。新内核在某些场景下会带CPU信息但大多数情况还是这一行裸日志。定位的时候需要结合/proc/schedstat或者/proc/PID/stat去看。1.2 实时调度与带宽控制的底层逻辑要理解throttling先理解Linux的实时调度策略。普通进程用的是CFS完全公平调度器权重按nice值分配实时进程用的是RT调度类包含SCHED_FIFO和SCHED_RR两种。RT任务的特点是绝对优先级只要有RT任务处于可运行状态普通CFS任务就只能排队等着一个RT线程死循环就能把整个CPU吃干净。这就暴露出一个问题如果某个RT线程因为bug进入死循环或者虽然没死循环但持续占用CPU那么系统里所有普通进程——包括sshd、bash、看门狗、日志服务——都会完全得不到CPU时间系统看起来就像“冻住”了。shell敲命令没反应甚至kill都执行不了因为shell进程无法运行。内核为了解决这个风险引入了实时带宽控制RT bandwidth control。机制不复杂给每个CPU设定一个周期period在这个周期内RT任务最多运行runtime时间。比如周期1秒运行时间0.95秒那么RT任务每1秒最多跑0.95秒剩下的0.05秒必须让给普通任务。一旦RT任务跑满了0.95秒内核就触发throttling把该CPU上所有RT任务标记为“节流状态”直到下一个周期开始才恢复运行。这套机制有点像“增值税起征点”允许你免费占多数资源但到了阈值就卡你一下。设计初衷不是限制实时任务的性能而是避免系统被单个失控线程搞到不可用。它的存在救过很多人的命——不是夸张没有这个保护很多无人值守的嵌入式设备一旦RT任务异常就只能硬重启。2. 为什么会被节流核心参数与计算逻辑2.1 sched_rt_period_us 和 sched_rt_runtime_us控制RT带宽的两个核心参数是sched_rt_period_us周期长度单位微秒默认10000001秒。sched_rt_runtime_us在周期内RT任务允许运行的总时间单位微秒默认9500000.95秒。比例刚好95%。/proc/sys/kernel/sched_rt_period_us和/proc/sys/kernel/sched_rt_runtime_us可以直接查看也可以用sysctl kernel.sched_rt_period_us查看。这两个参数是全局的对所有CPU生效不能单独配置某一个CPU。计算是否触发节流的逻辑可以简化成runtime_used delta runtime_max // 触发throttling其中runtime_used是在当前周期内该CPU上所有RT任务累计消耗的CPU时间delta是本次调度新增的消耗时间。当累计时间跨过runtime_max也就是950000us调度器就把该CPU上当前正在运行的RT任务强制dequeue出队同时打上rt_throttled标志。关键细节是节流期间这个CPU上的RT任务不会被调度运行。但RT任务并没有被杀掉状态仍然是TASK_RUNNING只是等着下个周期解锁。如果任务在等待锁或者sleep主动权可能就在别的CPU上那又是另一回事。总之节流后RT线程的优先级暂时失效但进程还活着。2.2 默认值、计算方式和“95%带宽”的由来为什么是95%而不是100%看内核源码里的注释/* * period 1s, runtime 0.95s * we give RT tasks 95% of the CPU time * and leave 5% for normal tasks */这是内核的默认守护策略。5%看起来不多但足以保证系统里的普通管理任务比如监控、登录、看门狗在RT任务失控时还能喘口气。如果你做的系统里有独立的硬件看门狗用户态喂狗线程通常是普通优先级如果RT任务占满100%喂狗线程就没机会跑硬件看门狗会强制重启设备。留5%就是为了避免这种情况。实际场景下很多实时任务的CPU占用并没有超过95%但仍然会触发throttling原因有两个使用了sleep(0)或者忙等待导致RT任务频繁唤醒调度器统计的runtime消耗可能比实际高。多核负载不均衡任务被调度到同一个核心上即使整个系统平均负载很低某个核心也容易达到95%。第二个情况最常见。比如你开4个RT线程做图像处理理论上4个核心平分负载但Linux默认的RT负载均衡在某些内核版本上并不均匀线程可能挤在同一个核心上那么这个核心就会频繁触发throttling而其他核心闲着。这不是线程数量的问题而是调度分布的问题。2.3 为什么内核要留5%给普通任务顺着刚才的看门狗例子往下说。除了喂狗之外还有几个依赖普通优先级的关键服务网络协议栈的软中断虽然软中断不一定完全走RT调度但某些驱动回调依赖软中断触发。SSH登录和shell交互排查问题全靠它RT任务卡死系统后你总得留个口子进去杀进程。日志落盘实时任务写日志日志进程如果完全被饿死你连排查线索都没有。rcu_sched等内核线程很多内核机制依赖普通优先级线程比如RCU回写完全饿死会导致内核报错甚至崩溃。这5%就是“保命带宽”。调大RT任务的占比本质上是在赌你的任务不会失控。如果任务稳定可控可以调大如果任务还在调试期建议先保持默认。我见过有人直接把runtime改成比period还大这种做法非常危险。内核会对runtime做合法性检查大于period的设置不会生效或者行为完全不可预期。正确做法是在period不变的前提下把runtime调整到接近period比如990000。留1%给系统已经是极限。后面会讲具体怎么配。3. 三类常见触发场景与排查方法3.1 实时线程持续占满CPU最典型的触发场景你写了一个RT线程做控制循环控制周期1ms但循环里有耗时的浮点运算或者加锁导致每次循环超过1ms于是线程几乎占满了整个CPU。判断方法很直接先看系统负载和CPU占用。top -H -p PID如果RT线程CPU占用接近100%且dmesg里有RT throttling activated基本就是它。再看具体线程的调度策略和优先级chrt -p PID输出类似pid 1234s current scheduling policy: SCHED_FIFO pid 1234s current scheduling priority: 99如果是SCHED_FIFO且优先级为99这个线程是绝对最高优先级。接着看它跑在哪个核心taskset -pc PID如果它固定在一个CPU上且占用率100%那么throttling几乎是必然的。这种场景下别急着调大带宽先优化你的控制循环本身。检查有没有printf、动态内存分配、锁竞争。尤其printf是重灾区RT线程里打印一个浮点数可能导致微秒级延迟积少成多。3.2 多核下RT任务的分布与CPU隔离第二种情况是RT任务跑了很多个但内核把它们压在同一个核心上。默认情况下Linux的RT调度器有负载均衡但均衡粒度比较粗糙而且高优先级的RT任务会“粘”在当前CPU上以降低迁移开销。如果任务之间有亲和性设置sched_setaffinity或者某些任务调用了pthread_setaffinity_np就会主动把多个任务绑到一个核上。排查时用ps -eLo psr,pid,tid,comm看一下每个线程在哪个CPU上ps -eLo psr,pid,tid,comm | grep your_taskpsr列就是当前CPU编号。如果多个RT线程的psr相同说明它们挤在一起。这时调整方法有两种用taskset手动把线程分散到不同CPU上。用CPU隔离isolation把实时任务预留到特定核心再通过cpuset把其他进程赶走。从长期稳定运行的角度我推荐后者。手动绑核只是临时方案系统启动后其他内核线程可能还是会跑来跑去干扰实时性。CPU隔离更彻底后面第4章会给出具体配置。3.3 内核编译选项与 PREEMPT_RT 的影响第三种触发场景和内核配置有关。如果你用的内核开启了CONFIG_PREEMPT_RT即实时补丁内核很多人叫RT内核调度器的行为和普通内核有差异。PREEMPT_RT内核下中断线程化、锁变成可抢占的RT任务被调度得更激进但RT带宽控制的逻辑依然存在。注意一点普通发行版内核对CONFIG_RT_GROUP_SCHED的开启情况不同这会影响runtime的计算逻辑。如果开启CONFIG_RT_GROUP_SCHEDRT带宽控制会基于cgroup进行分组计算此时/proc/sys/kernel/sched_rt_runtime_us可能不再是全局参数而是cgroup的cpu.rt_runtime_us。查看方式cat /sys/fs/cgroup/cpu/cpu.rt_runtime_us cat /sys/fs/cgroup/cpu/cpu.rt_period_us如果cgroup的配置里有RT限流即使全局参数已经调大cgroup层级仍可能限制RT任务。这就是很多人明明改了sysctl kernel.sched_rt_runtime_us-1却仍然看到throttling的原因。所以排查时先确认内核配置grep RT_GROUP_SCHED /boot/config-$(uname -r)如果输出CONFIG_RT_GROUP_SCHEDy那么全局参数没用了必须调整cgroup配置。如果输出# CONFIG_RT_GROUP_SCHED is not set那按全局参数来就好。4. 工程中如何调整与规避实操配置4.1 临时调整sysctl 与 /proc/sys/kernel最直接的临时调整方法就是修改运行时参数不需要重启。把全局RT带宽占比从95%提到99%执行echo 990000 /proc/sys/kernel/sched_rt_runtime_us或者用sysctlsysctl -w kernel.sched_rt_runtime_us990000如果你想完全禁用RT throttling把runtime设为-1sysctl -w kernel.sched_rt_runtime_us-1注意sched_rt_runtime_us设为-1表示不限制RT任务的运行时间等于关闭RT带宽控制。这是很多实时开发者在调优阶段用的手段但生产环境务必慎用。关闭保护机制后RT任务一旦失控系统就真的可能完全无响应只能硬件复位。period一般不需要改。如果你确实需要更小的周期比如控制周期是200us可以调整period_usecho 100000 /proc/sys/kernel/sched_rt_period_us echo 99000 /proc/sys/kernel/sched_rt_runtime_us这样周期100msRT运行99ms留1ms给普通任务。但改的时候要小心周期太短会增加调度器检查带宽的频率带来额外开销周期太长会让普通任务等待时间变长。对于大多数场景保持默认1秒周期就够了。4.2 永久生效sysctl.conf 与内核启动参数临时参数重启后失效生产环境要写入/etc/sysctl.conf或/etc/sysctl.d/下的独立文件。新建文件/etc/sysctl.d/99-rt.confkernel.sched_rt_period_us1000000 kernel.sched_rt_runtime_us990000执行sysctl --system生效。注意数值校验内核会检查runtime_us不能大于period_us除非设置为-1。所以如果你设runtime_us990000period_us必须大于等于它。另外如果你的内核开启了CONFIG_RT_GROUP_SCHED还需要设置cgroup参数。以cgroup v1为例echo 990000 /sys/fs/cgroup/cpu/cpu.rt_runtime_us echo 1000000 /sys/fs/cgroup/cpu/cpu.rt_period_us需要写到/etc/cgrules.conf或其他cgroup初始化配置里才能永久生效。cgroup v2和systemd下配置会复杂一些需要修改/sys/fs/cgroup/user.slice/cpu.rt_runtime_us这类路径。由于发行版差异较大建议先用cat确认具体路径。除了sysctl还有一种永久方案是改内核启动参数isolcpus。这个不是直接调带宽但能帮你把RT任务固定到独立核心上远离其他压力。比如4核CPU保留CPU2和CPU3给RT任务isolcpus2,3 nohz_full2,3 rcu_nocbs2,3配合irqaffinity把中断也赶走可以获得更稳定的实时行为。这样RT任务跑在隔离核心上受其他干扰变小throttling触发次数也会减少。因为其他核心上的普通任务不会来挤占RT任务本身如果不超过设定带宽仍然会触发但配合调优后大概率能压住。4.3 更好的做法CPU隔离 cpuset 调度策略搭配调大RT带宽只是临时止痛更工程化的做法是“规划”而不是“放宽”。我推荐组合方案用CPU隔离预留核心内核启动参数isolcpus把特定核心从通用调度器中摘除普通进程不会跑上去。用cpuset把RT任务绑到隔离核心创建一个cpuset只包含预留核心把实时线程的亲和性绑进去。设置合适的调度策略和优先级控制循环用SCHED_FIFO优先级99非关键RT任务用SCHED_RR或降低优先级避免互相踩踏。如果RT任务本身就能平稳运行比如1ms周期里只跑0.4ms再配合隔离核心那么即使在默认95%带宽下也不会触发throttling。关键是不要让RT任务超过周期预算。一个实用的计算方式统计你的RT任务在一个时窗内的最大CPU占用率。用perf top或者简单的循环采样脚本观察1秒内RT线程的CPU时间。如果最大占用率是60%~70%那么默认95%带宽足够如果峰值达到98%以上说明任务快碰到天花板了你需要优化代码而不是继续调大带宽。另一种思路是减少RT任务的总时长把实时线程里的非实时工作日志、统计、UI交互拆给普通优先级线程通过SCHED_OTHER处理RT线程只做最核心的控制逻辑。这样既保证实时性又避免触碰throttling。很多真实的工业控制系统就是这个架构一个高优先级控制线程外加几个低优先级的数据上报线程。5. 常见问题排查与避坑经验5.1 日志一直刷怎么办如果RT throttling日志频繁出现说明你的系统长期处于“带宽耗尽”状态不能只靠关闭日志来掩盖。很多初学者第一反应是降低printk级别dmesg -n 1这是把内核消息输出级别调到只显示emergencythrottling日志就不会打印到控制台了。但问题还在只是你看不见了。这种做法只适合临时压制干扰不适合作为解决方案。正确流程是先定位哪个CPU、哪个线程触发了throttling。分两步走。第一步开启内核调度统计echo 1 /proc/sys/kernel/sched_schedstats然后查看/proc/schedstat里面每个CPU的rt_runtime相关字段会给出RT任务消耗时间和节流次数具体字段名取决于内核版本通常在cpuN行中带有rt字样。第二步用perf sched看调度事件perf sched record -- sleep 5 perf sched latency --sort max可以直观看到哪些RT任务延迟高、运行时间长。如果不想上perf简单点就用top -H -p反复刷新观察哪个线程CPU时间一直累计。等到你确定是某个线程后再判断是任务本身超预算还是多核负载不均衡最后决定是优化代码、调参还是迁移CPU。5.2 调整后系统卡顿把sched_rt_runtime_us调大甚至设为-1之后最常见的副作用是系统开始卡顿鼠标、SSH响应都变慢甚至完全无响应。原因很好理解RT任务占用了更多CPU时间普通任务的调度机会变少。5%的余量被吃掉后负责处理输入输出的进程只能在RT任务休息的缝隙里运行。如果RT任务是持续占用CPU那普通任务可能永远等不到插入点。遇到这种情况先冷静按以下顺序回退把sched_rt_runtime_us恢复成默认的950000。查看dmesg里是否还有RT throttling如果消失了说明你的RT任务平时占用其实在95%以内只是偶尔抖动。这种抖动可以用优先级调整来解决而不是无脑放大带宽。如果是RT任务确实长期高占用那就得考虑换更强的CPU核心或者把部分负载迁移到GPU、FPGA等硬件加速单元而不是继续从内核要带宽。另外要注意某些实时控制应用对延迟极其敏感单纯调大带宽不够还要注意isolcpus和中断亲和性。卡顿可能不是带宽问题而是中断频繁抢占RT线程。用htop看哪个CPU中断占用多再用/proc/interrupts检查irqbalance是否把网卡中断调度到了RT核心上。我遇到过一次非常诡异的卡顿调大RT带宽后系统网络延迟飙升ping经常丢包。排查后发现是网卡驱动中断跑到了RT线程所在核心网络收包线程被RT任务挤占。解决办法是设置中断亲和性把网卡中断绑定到其他核心问题立刻消失。所以记住一个教训带宽控制只是调度器层面的保护中断、软中断对实时性的影响同样巨大调优时要全盘考虑。5.3 实时应用场景的扩展RT内核和实时工程说了这么多Linux调度你可能会想那工业界真正用的实时系统是不是都这样配其实不只是标准Linux内核像PREEMPT_RT补丁内核、风河VxWorks、以及MATLAB Simulink Real-TimexPC Target这类工具链底层都有类似的带宽或时间约束机制。举个相关的例子很多人用Simulink做硬件在环仿真目标机运行一个精简实时内核模型中的任务按固定步长执行。如果你配置的步长太小模型计算量过大目标机同样会出现“任务超时”或“scheduling overrun”这和Linux下的RT throttling本质是同一类问题有限的计算资源匹配不上无限的任务时间需求。再比如基于PC的软PLC或工业组态软件常见的有基于Windows的WinCC RT Advanced或者嵌入式Linux上的实时运行环境它们在做运动控制、视觉检测时也会遇到“任务周期抖动”或者“执行超限”的告警。底层的解决方法依然是老三样调整任务优先级、分配专用核心、提高硬件性能。这里给出一条通用原则任何实时系统都不能长期运行在100%负载下至少要留出10%~20%的余量。如果负载长期超过80%即使没有触发显式的throttling任务延迟也会逐渐增大系统稳定性会越来越差。所以遇到throttling时先把它看作一个系统负载过高的警示信号而不是单纯的内核限制。5.4 几个容易踩的坑参数校验、容器与虚拟化最后列几个我实际踩过、或者帮别人排查过的坑供参考。第一个坑容器环境中修改/proc/sys/kernel/sched_rt_runtime_us不生效。容器是共享内核的如果宿主机使用了cgroup v2并且开启cpu.rt_runtime_us限制即使你在容器内改了全局sysctl实际生效的仍是cgroup限制。解决办法是修改宿主机对应cgroup的配置或者改用privileged容器后再操作。别在容器里折腾半天最后发现被宿主机卡住了。第二个坑sched_rt_runtime_us设置成-1以后的恢复。很多人关闭RT throttling之后想恢复默认值直接执行sysctl -w kernel.sched_rt_runtime_us950000但是内核要求period_us和runtime_us要匹配如果你之前没动过period_us这个命令没问题。如果你自定义了period_us比如设成100000再恢复runtime_us950000就会报错因为运行时大于周期。碰到这种情况先把period_us恢复成1000000再改runtime_us。第三个坑CPU隔离后RT线程绑核失败。加了isolcpus之后隔离核心上的RT线程要显式设置亲和性才能跑上去。如果代码里没做亲和性设置调度器默认不会把任务调度到隔离核心导致你的实时任务仍然挤在普通核心上throttling照旧。检查方法taskset -pc PID如果返回的CPU列表里没有隔离核心就需要修改任务代码或者在启动脚本中绑定。第四个坑内核编译选项的影响。如果你是自己编译内核记得检查CONFIG_SCHED_DEBUG和CONFIG_SCHEDSTATS。默认发行版内核可能关闭了某些调试接口导致/proc/schedstat内容不全排查时无从下手。实时系统建议开启这两个选项编译内核时选择Kernel hacking - Scheduling Debugging。写在最后个人经验RT throttling与其说是错误不如说是一个“安全网”被触发。看到它的时候不要急着关掉安全网先想想为什么系统会撞到边界。我调试实时任务有一段时间总是被这个问题困扰后来形成一套固定动作先看dmesg确认触发时间再看top -H定位线程最后用perf sched看延迟分布。挨个排查下来绝大多数情况都是某个线程确实超预算而不是内核参数没调好。如果你只是想快速让小系统跑起来临时把sched_rt_runtime_us调到-1确实方便但请务必保证你的RT任务有完善的异常退出机制比如自己实现一个看门狗线程专门用来在RT任务卡死时强制重启它。不然生产环境上某个凌晨三点RT线程一个野指针导致死循环整个设备直接失联那滋味可不好受。另外分享一个调试小技巧在测试阶段故意把period_us调小到100000runtime_us调成95000让throttling更容易触发。这样可以在低压力下模拟高负载场景提前暴露你代码里的CPU占用峰值问题。等所有功能稳定后再把参数恢复到1秒周期你会发现自己省掉了大量现场排障时间。
返回列表