资讯动态

PELT负载跟踪深度拆解:从1024us周期到util_avg收敛验证

发布时间:2026/10/29 11:19:32 来源:尧图企业网站定制
简介面向Linux内核调度与性能优化方向的学习者这份PDF围绕CFS调度器中的PELTper entity load tracking机制展开重点回答Linux 3.8之前以运行队列跟踪负载时无法定位负载来源、负载值波动大的问题并说明按调度实体跟踪负载的思路。内容涉及1024us周期划分、负载贡献计算、衰减因子与负载累加公式还结合实例推演任务连续运行4096us后进入睡眠时各时间点的负载值并讲解decay_load()函数、runnable_avg_yN_inv数组及移位乘法替代浮点运算的实现方式代码分析基于Linux 4.18.0。资源包为1个PDF文件约358KB篇幅精炼适合作为调度器负载跟踪的专题笔记查阅。已有573人学习便于读者理解PELT的引入动机、核心公式与内核实现细节为分析CFS调度行为和能效优化提供参考。1. 从一次负载误判说起这份 PELT 拆解文档到底值不值得啃线上压测时遇到过一件怪事一台 8 核机器明明只有两个线程在跑满 CPU/proc/loadavg却冲到 6 以上调度器把新任务往空闲核上塞得犹犹豫豫尾延迟直接翻倍。当时第一反应是统计口径问题翻到内核源码里才发现问题出在负载跟踪的粒度上——rq 级别的负载把「谁在贡献负载」这件事糊成了一团稳定负载下数值也会大幅抖动。这正是 PELTper entity load tracking要解决的核心痛点也是这份《CFS调度器4-PELT(per entity load tracking).pdf》从头讲到尾的主线。文档基于 Linux 4.18.0 源码把 PELT 的引入动机、1024us 周期切分、衰减因子 y、decay_load()的定点实现、struct sched_avg字段语义、accumulate_sum()的两段式公式推导一直到update_load_avg()的调用时机全部串了一遍。它不是 API 手册而是一份能让你对着源码逐行核对的推导笔记适合正在做内核调度调优、写负载均衡相关模块或者准备面试被追问util_avg怎么来的那批人。下面我按「先立公式、再落代码、最后踩坑」的顺序把它拆成能直接复现的步骤。2. PELT 的数学底座1024us 周期与衰减因子怎么落到定点数上2.1 为什么 rq 级跟踪不够用Linux 3.8 之前CFS 以 runqueue 为单位累计负载。这个做法有两个硬伤一是负载来源不可分辨一个重任务和十个轻任务在 rq 上可能呈现相近的数值调度器做迁移决策时拿不到「哪个实体更重」的信息二是数值抖动大任务频繁 enqueue/dequeue 会让 rq 负载在相邻 tick 之间跳变负载均衡器基于这种噪声做判断迁移效果自然不稳定。PELT 把跟踪对象下沉到调度实体se每个 se 维护自己的sched_avgrq 负载再由其下所有 se 聚合而来来源清晰、曲线平滑。文档里那句「无法确定当前负载的来源」就是整篇的出发点理解这一点后面所有公式才有落脚处。2.2 负载贡献的级数展开与 y 的取值PELT 把物理时间切成 1024us 的周期一个实体在周期内 runnable 的时间为 x则该周期贡献 x/1024。历史贡献按衰减因子逐周期打折总贡献写成L L0 L1*y L2*y^2 L3*y^3 ... Ln*y^n关键参数是y^32 0.5即经过 32 个周期约 32ms历史贡献衰减一半由此解出y 0.97857206。文档用「task 连续跑 4096us 后睡眠」的例子把 1023us、2047us、3071us 等时刻的贡献逐个算了一遍规律很干净当前负载 上一周期负载 × y 当前周期贡献。这个递推关系是整个实现的地基后面accumulate_sum()的两段式拆分不过是它的工程化表达。我一般会拿这个例子手算一遍再去看代码否则很容易在period_contrib的加减上绕晕。2.3 用移位和查表替代浮点runnable_avg_yN_inv内核不能在内核态随便用浮点所以y^n * 2^32被预先算成整数存进runnable_avg_yN_inv[32]数组decay_load(val, n)通过mul_u64_u32_shr(val, runnable_avg_yN_inv[n%32], 32)完成定点乘法。n 超过 31 时利用y^32 0.5做移位折叠先val n/32再对余数查表。文档给出了数组前几项0xffffffff, 0xfa83b2da, ...和calc_runnable_avg_yN_inv()的生成逻辑还点明 n 超过LOAD_AVG_PERIOD * 63即 2016 个周期时直接返回 0作为溢出保护。这段是典型的「公式看着吓人、实现只有十几行」把数组生成脚本跑一遍decay_load就没有黑匣子了。3. 把公式翻译成代码accumulate_sum()与sched_avg字段对照3.1struct sched_avg每个字段到底存什么文档把结构体摊开讲得很细我整理成对照表方便对着源码看字段含义更新时机last_update_time上次负载更新时间用于算 delta每次___update_load_sum()load_sum基于 runnable 时间的贡献总和accumulate_sum()内累加runnable_load_sum可运行部分贡献总和task se 下与load_sum相等同上util_sum基于 running 时间的贡献总和同上乘scale_cpuperiod_contrib上次更新不足 1024us 的余量每次更新末尾写入load_avg/runnable_load_avg/util_avg对应 sum 除以 divider 的平均值___update_load_avg()初始化函数init_entity_runnable_average()有个容易忽略的细节task se 的load_avg和runnable_load_avg初始化为se-load.weight注释说这是「先当重任务看等它稳定到真实负载」group se 则初始化为 0。这个差异直接影响刚 fork 出来的进程在最初几个 tick 里的调度权重调优时如果发现新进程抢占异常先回头查这里。3.2 两段式拆解Step 1 衰减旧值Step 2 补当前段通用公式是u (u d1) * y^p 1024 * Σy^n d3代码拆成两步。Step 1 由accumulate_sum()里的decay_load()完成把三个 sum 按跨越的周期数 p 衰减Step 2 交给__accumulate_pelt_segments()其中 c2 的推导是全文最巧的一段——利用无穷级数max 1024 * Σ(n0..inf) y^n把有限和转成max - decay_load(max, periods) - 1024LOAD_AVG_MAX取 47742。下面是我按文档逻辑复现的核心片段用来验证手算和代码是否对得上/* 复现 __accumulate_pelt_segments 的 c2 计算验证与手算一致 */ #define LOAD_AVG_MAX 47742 static u32 accumulate_segments(u64 periods, u32 d1, u32 d3) { u32 c1, c2, c3 d3; /* y^0 1当前段不衰减 */ c1 decay_load((u64)d1, periods); /* 补齐上一周期的余量衰减 p 次 */ c2 LOAD_AVG_MAX - decay_load(LOAD_AVG_MAX, periods) - 1024; return c1 c2 c3; }periods是本次更新跨越的完整周期数d1是补齐上次不足一周期的部分d3是当前周期剩余部分。参数含义搞清后accumulate_sum()里delta sa-period_contrib; periods delta / 1024;这两行就不再是玄学——它把「上次欠的零头」并进本次时间差才能正确算出跨了几个周期。LOAD_AVG_MAX的 47742 是代码迭代算出来的和等比数列理论值有取整误差文档特意提醒了这点别拿计算器较真。3.3 更新入口与调用时机update_load_avg()在三个场景被调用enqueue_entity、dequeue_entity、scheduler tick。它先调__update_load_avg_se()更新 se再调update_cfs_rq_load_avg()聚合 rq。___update_load_sum()里delta 10把 ns 转成 us不足 1us 直接返回避免无意义衰减___update_load_avg()用divider LOAD_AVG_MAX - 1024 period_contrib做除法得到平均值。想验证自己理解对不对最直接的办法是在这几个函数里加trace_printk跑一个固定负载的 benchmark看util_avg是否在约 32ms 内收敛到理论值附近——收敛曲线对不上八成是period_contrib的更新位置看漏了。4. 避坑与排查五个让负载数值失真的常见问题4.1util_avg长期偏低CPU 频率上不去现象跑满单核的进程util_avg只到 700 左右cpufreq 的 schedutil governor 迟迟不拉高频。原因通常是arch_scale_freq_capacity()和arch_scale_cpu_capacity()返回的容量系数没配好cap_scale()把贡献按比例缩了。解决确认设备树或 ACPI 里的 capacity 值对比cpu_capacity的 sysfs 输出容量系数错误会让所有 util 类指标系统性偏移。4.2 新进程一上来就抢 CPU现象刚 fork 的轻量线程在头几个 tick 里被当成重任务挤占同核其他进程。原因在init_entity_runnable_average()把 task se 的load_avg初始化为se-load.weight这是有意为之的保守策略。解决如果业务对启动抖动敏感可以在 fork 后短暂观察或调整sched_latency_ns让调度周期更短、收敛更快不要直接改初始化逻辑那会破坏负载均衡的收敛前提。4.3 手算衰减值和代码对不上现象按公式算y^10 * 1024和decay_load(1024, 10)结果差几个数。原因是定点数截断和runnable_avg_yN_inv的取整误差文档明确说 47742 这类常量是迭代算出来的。解决验证时用相对误差判断千分之一以内即正常别追求逐位相等真要精确对照直接跑Documentation/scheduler/sched-pelt.c生成数组再比。4.4 组调度下runnable_weight语义混淆现象开了CONFIG_FAIR_GROUP_SCHED后group se 的runnable_weight和weight不相等按 task se 的经验去读会算错。原因runnable_weight表示组内可运行部分占的权重会随组内任务状态动态更新恒小于等于weight。解决读 group se 负载时以runnable_load_avg为准别拿weight直接乘调试时打印两者比值异常偏小说明组内大量任务在睡眠。4.5 tick 间隔过大导致衰减跳变现象开了NO_HZ_FULL或 tickless 后某些核长时间不触发 scheduler tick负载更新间隔拉大decay_load一次跨很多周期数值出现台阶。原因PELT 依赖周期性更新更新间隔越大单次衰减的粒度越粗。解决对延迟敏感的业务核评估 tickless 配置或在 enqueue/dequeue 路径上确认update_load_avg()被正常调用必要时用trace_sched_load_avg_*事件观察更新频率。5. 进阶验证用 trace 事件把util_avg收敛曲线画出来理解 PELT 最有效的方式不是读是让它自己说话。内核在CONFIG_SCHED_DEBUG下提供了sched_load_avg_task、sched_load_avg_cpu等 tracepoint配合trace-cmd可以把某个进程的util_avg随时间的变化抓下来。我一般这么干先起一个固定占空比的负载比如每 10ms 跑 5ms 的忙循环抓 200ms 的 trace导出util_avg序列看它是否在约 32ms 后稳定在 50% 附近。收敛太快说明衰减因子理解反了收敛太慢或震荡说明period_contrib更新有问题。下面是我常用的抓取命令参数按实际 CPU 和 PID 替换# 抓取指定 PID 的负载跟踪事件持续 1 秒 trace-cmd record -e sched_load_avg_task -e sched_load_avg_cpu \ -p function --func-stack -P PID sleep 1 trace-cmd report | grep -E util_avg|load_avg | head -50-e指定事件-P过滤进程sleep 1控制采样窗口。报告里重点看util_avg字段的爬升斜率理论上一阶系统的时间常数约 32ms实测斜率明显偏离就回到___update_load_avg()检查 divider 是否用了正确的period_contrib。另一个验证点是LOAD_AVG_MAX的边界——把负载拉到 100% 持续足够久util_sum应逼近LOAD_AVG_MAX - 1024 period_contrib对不上就说明cap_scale的容量系数在捣鬼。这套方法我在排查一次调度抖动时用过最后定位到是某核的 capacity 被错误标定trace 曲线在 60% 处就压平了比翻代码快得多。从那以后我每次改调度相关参数都强制先抓一段 trace 看收敛曲线再下结论省得在公式里空转。希望这份拆解能帮你在自己的环境里把 PELT 跑通、验对。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑