news 2026/11/3 10:53:39

Linux PELT负载跟踪源码拆解:1024us衰减与sched_avg

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux PELT负载跟踪源码拆解:1024us衰减与sched_avg

简介:《CFS调度器(4)-PELT(per entity load tracking)》是一份面向Linux内核调度学习者与性能调优开发者的PDF技术笔记,聚焦CFS中PELT机制的引入缘由、跟踪模型与工程实现。内容从Linux 3.8前按运行队列跟踪负载导致来源不明、波动较大的问题切入,解释per-scheduling entity跟踪如何细化负载归因,并展开1024us周期、衰减因子y、负载累加公式、decay_load()移位乘法优化及runnable_avg_yN_inv预计算等关键点,配有连续运行与睡眠场景的数值示例,代码分析基于Linux 4.18.0。资源包为1个PDF文件,约358KB,便于单篇查阅与归档。已有573人学习,适合希望理解CFS负载跟踪原理、补齐内核调度知识链路的读者。

1. PELT 为什么值得单独拆:从 rq 级负载到 per-entity 的转折

linux CFS 的负载跟踪在 3.8 之后换了一套账本:以前看 runqueue,现在看每个调度实体。你如果只用 /proc/loadavg 判断忙闲,很容易被一个已经睡了的线程留下的衰减尾巴骗到——这不是 bug,是 PELT 把历史负载按周期衰减后仍然算在账上。这份资料围绕 Linux 4.18.0 把 PELT 的 1024us 周期、y^32=0.5、sched_avg 字段和 accumulate_sum 拆到函数级,适合想改调度策略、调负载均衡、排查 CPU 利用率虚高的内核与性能工程师。先跟一遍 1023us 到 8191us 的算例,再对着源码把 update_load_avg 串起来,比从调度器入口硬啃省力。

2. 1024us 与 y^32=0.5:把衰减账算平再读源码

2.1 为什么把时间切成 1024us 的周期

PELT 要解决的是 rq 级负载跟踪的两个老毛病:一是看不出负载到底来自哪个调度实体,二是工作负载稳定时 rq 级读数仍然跳得厉害。把时间切成 1024us 的周期,每个周期里一个实体处于 runnable 状态多久,就对负载贡献多少,这样负载来源被压到 se 这一级,读数也不再是整条 runqueue 混在一起的一锅粥。runnable 时间包含两段:在 rq 里等着上 CPU 的时间,以及正在 CPU 上跑的时间,这两段都算“可运行”,所以 load_sum 和 util_sum 的统计口径并不一样,后面读字段时别混。一个周期内 runnable 了 x 微秒,贡献就是 x/1024,多个周期的贡献按 y 的幂次衰减后累加,公式写成 L = L0 + L1y + L2y^2 + L3y^3 + ... + Lny^n。y 不是随手拍的,取 y^32 = 0.5,也就是经过 32 个周期、大约 32ms 后,历史负载衰减一半,这样既保留历史又不让旧账压死新任务。

2.2 手工算例:4096us 运行后睡眠的八个时刻

正文给了一个很适合拿纸笔跟的算例:一个 task 从加入 rq 后一直运行 4096us,然后一直睡眠,问 1023us、2047us、3071us、4095us、5119us、6143us、7167us、8191us 这些时刻的负载贡献。1023us 时就是 L0=1023;到 2047us 时,上一个周期的 1023 要乘 y 衰减一次,再加上本周期跑满的 1024,写成 L1 = 1023 + 1024y = 2025;到 3071us 时 L2 = 1023 + 1024y + 1024y^2 = 3005;到 4095us 时 L3 = 1023 + 1024y + 1024y^2 + 1024y^3 = 3963。4096us 之后 task 睡眠,5119us 时新增贡献为 0,只剩历史项衰减,L4 = 0 + 1024y + 1024y^2 + 1024y^3 + 1024y^4 = 3877;6143us 时 L5 = 0 + 0 + 1024y^2 + 1024y^3 + 1024y^4 + 1024y^5 = 3792;7167us 时 L6 = L5y = 3709;8191us 时 L7 = L6y = 3627。这组数最直观的地方是:睡眠之后负载不是立刻归零,而是按周期往下掉,所以看瞬时读数时别把衰减尾巴当成“任务还在跑”。

2.3 decay_load 与定点数:y^n 怎么变成移位和乘法

内核不会为了算负载去跑浮点,decay_load() 把 valy^n 拆成移位和 32 位乘法。代码里先做边界判断:n 大于 LOAD_AVG_PERIOD63 时直接返回 0,LOAD_AVG_PERIOD 是 32,也就是超过 2016 个周期后认为衰减到 0;然后利用 y^32=0.5,把 n 拆成 n/32 和 n%32,前者用右移表示 1/2 的幂,后者查 runnable_avg_yN_inv 表,最后 mul_u64_u32_shr(val, runnable_avg_yN_inv[local_n], 32) 一次乘加完成。runnable_avg_yN_inv 存的是 y^n*2^32,n 从 0 到 31,数组首项是 0xffffffff,第二项 0xfa83b2da,第三项 0xf5257d14,和正文给出的表一致。这个表的生成逻辑在 sched-pelt.c 的 calc_runnable_avg_yN_inv() 里,本质是 ((1UL << 32) - 1) * pow(0.97857206, i),i 取 0 到 31。读这段代码时重点看两个参数:LOAD_AVG_PERIOD=32 是衰减半衰期的周期数,runnable_avg_yN_inv 是定点化后的 y 幂表,二者合起来保证常数时间算衰减。

static u64 decay_load(u64 val, u64 n) { unsigned int local_n; if (unlikely(n > LOAD_AVG_PERIOD * 63)) /* 超过 2016 个周期直接归零 */ return 0; local_n = n; if (unlikely(local_n >= LOAD_AVG_PERIOD)) { val >>= local_n / LOAD_AVG_PERIOD; /* y^32 = 0.5,用右移折半 */ local_n %= LOAD_AVG_PERIOD; } val = mul_u64_u32_shr(val, runnable_avg_yN_inv[local_n], 32); return val; }

这段代码的逻辑是先把大周期数拆成“折半次数”和“查表余数”,再做一次 64 位乘 32 位右移 32 位的定点乘法。参数 val 是待衰减的负载和,n 是经过的周期数;边界 2016 不是随便写的,是 32*63,超过这个范围后 y^n 小到可以当 0。实际读源码时别只看函数名,local_n / LOAD_AVG_PERIOD 和 local_n % LOAD_AVG_PERIOD 这两步决定了衰减精度,改任何一个常量都会让算例对不上。

2.4 用 python 复现一遍衰减账

把正文的八个时刻用 python 跑一遍,比盯着公式硬看快。下面的代码按“每跨一个周期,旧负载乘 y,新周期加上本周期贡献”的递推写,runs 数组前四个周期跑满 1024us,后四个周期睡眠新增 0,输出时刻按 1023 + k*1024 取周期末尾。

y = 0.97857206 # y^32 = 0.5 反推出来的单周期衰减因子 runs = [1024, 1024, 1024, 1024, 0, 0, 0, 0] # 前 4 个周期运行,后 4 个周期睡眠 L = 0.0 for k, c in enumerate(runs): L = L * y + c # 旧负载衰减一次,再加本周期贡献 print(f"{1023 + k * 1024}us: {L:.0f}")

这段代码的逻辑是把连续时间离散成 1024us 周期,参数 y 控制历史衰减速度,runs 控制每个周期是否跑满。运行结果会落在 1023、2025、3005、3963、3877、3792、3709、3627 附近,和正文算例的尾数差异来自 y 的截断和取整。你可以把 runs 改成 [512, 1024, 0, ...] 观察半周期贡献,也可以把 y 改成 0.9 看衰减变慢后负载尾巴有多长;但别把这个 y 当成可调参数,内核里的 y 由 y^32=0.5 定死,用户态调不了。

提示:算例里的时间点取的是周期末尾,实际内核用 delta 累加 period_contrib 再整除 1024,边界上会差几微秒,别拿算例尾数去卡内核打印值。

3. sched_avg 与调度实体:字段、初始化与 task/group 的分岔

3.1 sched_avg 里每个字段对应哪本账

Linux 用 struct sched_avg 记录 se 或者 cfs_rq 的负载信息,字段不多,但每个字段的统计口径不一样。last_update_time 是上一次负载更新时间,用来算时间间隔;load_sum 是基于 runnable 时间的负载贡献总和,runnable 包含等 CPU 和正在跑;runnable_load_sum 也是 runnable 口径,但在组调度里和 load_sum 的用法有差别;util_sum 是基于 running 时间的贡献总和,running 只算真正在 CPU 上执行的时间;period_contrib 记的是上次更新不足 1024us 周期的那一段;load_avg、runnable_load_avg、util_avg 是对应的平均值。读 /proc/sched_debug 时经常看到 load_avg 和 util_avg 并排,如果一个任务等 CPU 很久但跑得少,load_avg 会上去,util_avg 不会跟着涨,这就是两本账的差别。

字段统计口径读的时候注意
last_update_time上次更新时间迁移、tick 漏更时先查它
load_sumrunnable 时间贡献总和包含等待和运行
runnable_load_sumrunnable 口径的负载和组调度里和 load_sum 有区别
util_sumrunning 时间贡献总和只算真正上 CPU
period_contrib不足一个周期的余量算周期数时要先加上它
load_avg / runnable_load_avg / util_avg平均值看趋势,不看单点

3.2 task se 与 group se 初始化差异

init_entity_runnable_average() 把 sched_avg 清零后,对 task se 和 group se 走了两条路。task se 的 runnable_load_avg 和 load_avg 初始化为 scale_load_down(se->load.weight),注释写得很直白:任务先按满负载看待,等它有机会稳定到真实负载水平;group se 则初始化为 0,因为组里还没挂任何任务。runnable_weight 对 task se 就是 se->load.weight,对 group se 是实体权重的一部分,表示组 runqueue 的可运行部分,后续会不断更新。这个差异是很多误判的来源:你看到某个 group se 的 load_avg 是 0,不一定代表组里没活,可能只是还没更新;你看到 task se 初始 load_avg 等于权重,也不代表它真的跑了那么重,只是内核先给了一个保守初值。

struct sched_avg { u64 last_update_time; u64 load_sum; u64 runnable_load_sum; u32 util_sum; u32 period_contrib; unsigned long load_avg; unsigned long runnable_load_avg; unsigned long util_avg; }; void init_entity_runnable_average(struct sched_entity *se) { struct sched_avg *sa = &se->avg; memset(sa, 0, sizeof(*sa)); if (entity_is_task(se)) sa->runnable_load_avg = sa->load_avg = scale_load_down(se->load.weight); /* task 先按满负载初始化 */ se->runnable_weight = se->load.weight; /* group se 后续会更新这个值 */ }

这段代码的逻辑是先清空账本,再按实体类型给初值。参数 se->load.weight 是调度实体的权重,scale_load_down() 在开启权重缩放时把值压回低位;task se 的初值等于权重,group se 的 load_avg 保持 0。读初始化代码时别忽略注释,注释里“heavy tasks until they get a chance to stabilize”就是告诉你初值不是真实负载,刚创建的任务读数偏高是正常的。

3.3 在源码里定位结构体和初始化函数

照着资料读源码,第一步不是从 fair.c 第一行开始翻,而是用 grep 把结构体和初始化点钉出来。Linux 4.18.0 里 struct sched_avg 在 include/linux/sched.h 附近,struct sched_entity 也在同一带,update_load_avg 和 accumulate_sum 在 kernel/sched/fair.c。下面几条命令可以在源码根目录直接跑,先把位置和调用点找齐,再打开编辑器跟。

# 找结构体定义和初始化函数 grep -rn "struct sched_avg" include/linux/sched.h kernel/sched/fair.c grep -rn "init_entity_runnable_average" kernel/sched/fair.c # 找 update_load_avg 的调用点,enqueue/dequeue/tick 都会出现 grep -rn "update_load_avg(" kernel/sched/fair.c # 找衰减表和常量 grep -rn "runnable_avg_yN_inv\|LOAD_AVG_PERIOD\|LOAD_AVG_MAX" kernel/sched/fair.c

这几条命令的逻辑是先定位再精读,参数 -rn 表示递归并显示行号,路径按 4.18 的源码树写。grep 结果里如果 update_load_avg 出现在 enqueue_entity、dequeue_entity、task_tick_fair 附近,就说明负载更新跟着入队、出队和 tick 走;如果只盯着 accumulate_sum 看,很容易漏掉“什么时候触发更新”这个前提。定位到之后,先读结构体字段,再读初始化,最后读更新函数,顺序别反。

3.4 runnable_weight 与 load.weight 的关系

task se 的 runnable_weight 和 load.weight 相等,group se 的 runnable_weight 总是小于等于 weight,表示组里可运行部分占组权重的比例。这个值在 __update_load_avg_se() 里对 task se 会重新赋成 se->load.weight,对 group se 则由上层更新。读负载均衡代码时,task_h_load() 一类计算会用到 runnable_weight,如果你把 group se 的 load_avg 直接当任务负载去算迁移,就会把“组里可能没活”的组算得很重。实际排查时看到 group se 的 runnable_weight 和 weight 差很多,先别怀疑衰减算错,去看组里任务是不是都睡了。

4. accumulate_sum 拆解:一次更新里 step1 和 step2 各干什么

4.1 update_load_avg 的三个调用点

update_load_avg() 不是定时器专属,enqueue_entity 把进程加进就绪队列时会调,dequeue_entity 把进程摘出去时会调,scheduler tick 周期性更新时也会调。函数先取 cfs_rq_clock_task(cfs_rq) 作为 now,然后在 se->avg.last_update_time 存在且没有 SKIP_AGE_LOAD 标志时调 __update_load_avg_se() 更新 se,再调 update_cfs_rq_load_avg() 更新 cfs_rq。这个顺序很重要:先更新实体,再更新队列,rq 的负载是下面所有 se 负载的总和。读调用点时要注意 flags,SKIP_AGE_LOAD 会跳过 se 的 aging,迁移、attach 一类路径上会出现,别看到 last_update_time 没变就以为 tick 丢了。

4.2 accumulate_sum 的 step1 和 step2

accumulate_sum() 把公式拆成两步。第一步是 Step 1:如果跨过了周期边界,就用 decay_load() 把 sa->load_sum、sa->runnable_load_sum、sa->util_sum 按 periods 衰减;第二步是 Step 2:把 d1、d2、d3 三段拼起来算新增贡献,其中 d1 是离当前最远的不完整周期剩余部分,d2 是完整周期,d3 是当前周期剩余部分。代码里先把 delta 加上 sa->period_contrib,再 periods = delta / 1024,这样上一次不足一个周期的余量不会被丢掉;然后 delta %= 1024 得到当前周期余量,contrib = __accumulate_pelt_segments(periods, 1024 - sa->period_contrib, delta)。最后 sa->period_contrib = delta,把本次余量留给下一次。参数 load、runnable、running 在调用点非 0 即 1,分别决定要不要把 contrib 累加到 load_sum、runnable_load_sum、util_sum。

static __always_inline u32 accumulate_sum(u64 delta, int cpu, struct sched_avg *sa, unsigned long load, unsigned long runnable, int running) { unsigned long scale_freq, scale_cpu; u32 contrib = (u32)delta; u64 periods; scale_freq = arch_scale_freq_capacity(cpu); scale_cpu = arch_scale_cpu_capacity(NULL, cpu); delta += sa->period_contrib; /* 补上上次不足一个周期的余量 */ periods = delta / 1024; /* 1024us 一个周期 */ if (periods) { sa->load_sum = decay_load(sa->load_sum, periods); sa->runnable_load_sum = decay_load(sa->runnable_load_sum, periods); sa->util_sum = decay_load((u64)(sa->util_sum), periods); delta %= 1024; contrib = __accumulate_pelt_segments(periods, 1024 - sa->period_contrib, delta); /* Step 2 */ } sa->period_contrib = delta; /* 余量留给下一次 */ contrib = cap_scale(contrib, scale_freq); if (load) sa->load_sum += load * contrib; if (runnable) sa->runnable_load_sum += runnable * contrib; if (running) sa->util_sum += contrib * scale_cpu; return periods; }

这段代码的逻辑是“先还旧账,再记新账”。参数 delta 是两次更新之间的时间差,单位已经由调用方从 ns 转成 us;period_contrib 是上次剩下的不足 1024us 的部分;periods 是本次跨过的完整周期数。scale_freq 和 scale_cpu 是频率和 CPU 容量缩放,contrib 算完还要按这两个因子缩放,所以同样的运行时间在不同 CPU 上对 util_sum 的贡献可能不同。读这段时重点看 delta += sa->period_contrib 和 sa->period_contrib = delta 这一进一出,少了任何一步,周期边界上的负载就会越算越偏。

4.3 __accumulate_pelt_segments 里 c2 的等比数列技巧

__accumulate_pelt_segments() 算的是 d1、d2、d3 三段贡献。c1 = decay_load(d1, periods) 是补齐上一周期剩余部分再衰减 periods 次;c3 = d3 是当前周期剩余部分,不需要衰减;c2 是完整周期部分的和,公式写成 1024 * sum(y^n),n 从 1 到 p-1。正文给了一个很巧的转换:把 c2 看成 LOAD_AVG_MAX 减去尾部再减去 1024,代码写成 c2 = LOAD_AVG_MAX - decay_load(LOAD_AVG_MAX, periods) - 1024。LOAD_AVG_MAX 是 1024*(1 + y + y^2 + ... + y^n) 的极限,等比数列求和后 n 趋向无穷,值是 47742。这个 47742 不是手算出来的整数,是 calc_converged_max() 用定点数迭代到 last == max 得到的,所以拿数学公式算出来的值可能和它差一点,别拿计算器结果去对内核常量。

static u32 __accumulate_pelt_segments(u64 periods, u32 d1, u32 d3) { u32 c1, c2, c3 = d3; /* y^0 == 1 */ c1 = decay_load((u64)d1, periods); c2 = LOAD_AVG_MAX - decay_load(LOAD_AVG_MAX, periods) - 1024; return c1 + c2 + c3; }

这段代码的逻辑是把无限衰减和拆成“总和减尾部”。参数 periods 是跨过的完整周期数,d1 是上一周期剩余部分,d3 是当前周期剩余部分。c2 里减去的 1024 是因为求和从 n=1 开始,不含 n=0 的 1024;decay_load(LOAD_AVG_MAX, periods) 是尾部衰减。读这段时如果对 c2 没感觉,就把 periods=1 代进去:c2 = 47742 - 47742y - 1024,差不多就是 1024y 的量级,和手工算例对得上。

4.4 ___update_load_sum 与 ___update_load_avg 的平均值换算

___update_load_sum() 先算 delta = now - sa->last_update_time,然后 delta >>= 10 把 ns 转成 us;如果 delta 为 0 直接返回,说明两次更新间隔不到 1us,没必要衰减;否则 sa->last_update_time += delta << 10,把时间基往前推,再调 accumulate_sum()。accumulate_sum() 返回 periods,如果 periods 为 0,说明没跨周期,___update_load_sum() 返回 0,平均值也不更新。___update_load_avg() 里 divider = LOAD_AVG_MAX - 1024 + sa->period_contrib,然后 load_avg = div_u64(load_sum, divider),runnable_load_avg 和 util_avg 同理。divider 加上 period_contrib 是为了把当前不足一个周期的余量算进分母,这样平均值不会在周期边界上跳变。读平均值换算时别只盯 load_sum,divider 的变化同样影响读数。

5. 避坑与排查:PELT 读数对不上时先查这四件事

5.1 时间基与单位:delta 右移后为 0 的假死

现象是某个 se 的 load_avg 长时间不动,enqueue/dequeue 都发生了,但 sched_debug 里读数像被冻住。原因常见于时间基单位:___update_load_sum() 里 delta = now - last_update_time 单位是 ns,delta >>= 10 之后才是 us,如果两次更新间隔不到 1024ns,delta 右移后为 0,函数直接返回,last_update_time 也不推进。另一种情况是调用方传进来的 now 不是 cfs_rq_clock_task(),而是 wall clock 或者别的时钟,时间差算出来偏小甚至为负。解决方法是先确认 now 的来源,4.18 的 update_load_avg() 用的是 cfs_rq_clock_task(cfs_rq),排查时在___update_load_sum() 入口打印 now 和 last_update_time,看差值是否按 ns 走;如果差值长期小于 1024,先查调用路径是不是在 tick 之外被高频触发,别急着改衰减表。

注意:PELT 的最小计量单位是 us,不是 ns;拿 ns 差值直接套 1024 会把周期数算大 1000 倍。

5.2 迁移与 last_update_time:跨 CPU 的账怎么带

现象是任务迁移到另一个 CPU 后,load_avg 突然偏高或偏低,负载均衡把它选来选去。原因是 se->avg 跟着任务走,last_update_time 是全局时间基,迁移时如果没把旧 CPU 上的账结清,新 CPU 第一次更新会用当前 now 减去旧的 last_update_time,补出一大段 delta;如果迁移路径上带了 SKIP_AGE_LOAD,又会跳过 se 的 aging,队列负载更新了但 se 没更新。解决方法是看迁移代码里 update_load_avg() 的 flags,确认是否跳过 aging;排查时对比任务迁移前后 /proc/ /sched 里的 se.avg 字段和 /proc/sched_debug 里目标 cfs_rq 的 load_avg,如果 se 的 load_sum 没变但 cfs_rq 变了,先查队列级更新;如果 se 的 last_update_time 停在旧 CPU 时间,再查迁移路径有没有漏掉结账。别用“迁移后读数必须连续”去要求 PELT,它是按周期补账的,跨 CPU 后有一次跳变不稀奇。

5.3 组调度与 runnable_weight:group se 初始 0 的误判

现象是开了 CONFIG_FAIR_GROUP_SCHED 后,某个 group se 的 load_avg 一直是 0,但组里明明有任务在跑;或者 group se 的 runnable_weight 远小于 weight,负载均衡却把它算得很重。原因是 group se 的 load_avg 初始化为 0,runnable_weight 表示组 runqueue 的可运行部分,组里任务都睡时它本来就该小;而 task_h_load() 一类计算会把组权重和 runnable_weight 混着用,读错字段就会误判。解决方法是先确认你看的是 task se 还是 group se:task se 的 runnable_weight 等于 load.weight,group se 的 runnable_weight 小于等于 weight;再看组里任务是否真的 runnable。排查时用 /proc/sched_debug 找对应 cfs_rq 的 load_avg 和 util_avg,再和组 se 的读数对,别拿 group se 的 0 去证明调度器没更新。

5.4 tick 漏更与 NO_HZ:补账边界

现象是长时间 idle 后突然忙起来,第一次读到的 load_avg 跳得很高,或者反过来,忙了一阵读数却上不去。原因是 NO_HZ_IDLE 下 CPU 空闲时 tick 可能停掉,负载更新不是每 1024us 都发生,等下一次更新时 delta 很大,accumulate_sum() 会一次性补很多周期,衰减和新增贡献一起算,读数自然跳。另一个边界是 delta 超过 LOAD_AVG_PERIOD*63 个周期后 decay_load() 直接返回 0,旧账被清掉,看起来像“负载突然归零”。解决方法是别拿单次采样判断趋势,至少看几个 tick 的 sched_debug 输出;排查长时间 idle 后的跳变,先确认 CPU 是否进了 NO_HZ,再看 last_update_time 和 now 的差值是不是几千个周期。调负载均衡参数时也要考虑这个补账边界,别把一次补账跳变当成参数调坏了。

# 看 cfs_rq 和 se 的负载读数,连续采几次比单次可靠 grep -A 12 "cfs_rq" /proc/sched_debug | head -60 cat /proc/<pid>/sched | grep -E "se\.(load|util|runnable)" # 看调度 tick 和迁移相关 tracepoint 是否有更新 ls /sys/kernel/debug/tracing/events/sched/ | grep -E "sched_stat|sched_migrate"

这几条命令的逻辑是先看读数再看事件,grep -A 12 是把 cfs_rq 后面的字段带出来,/proc/ /sched 里 se.avg 字段在部分 4.18 配置下可能不全,没有就去 sched_debug 找对应队列。参数上别把 head -60 当成固定窗口,队列多的时候要按 CPU 号过滤;tracepoint 先确认 debugfs 挂载,再开事件,别在线上环境长时间开全量 sched 事件。

6. 验证与调参:用 sched_debug 和算例把 PELT 钉死

6.1 用 sched_debug 看 cfs_rq 的 load_avg/util_avg

验证 PELT 最直接的办法是连续采 /proc/sched_debug,看 cfs_rq 的 load_avg 和 util_avg 是不是按预期收敛。跑一个纯 CPU 任务,util_avg 应该往上走,load_avg 跟着走;再让它睡一会儿,两个值都应该往下掉,但不会瞬间归零。采的时候按 CPU 分组,别把不同 CPU 的队列混在一起看。下面这条命令把 cfs_rq 段和关键字段一起抓出来,连续跑几次对比。

for i in $(seq 1 5); do echo "---- sample $i ----" awk '/cfs_rq/ {print; c=12} c>0 {print; c--}' /proc/sched_debug | \ grep -E "cfs_rq|load_avg|util_avg|runnable" sleep 1 done

这段脚本的逻辑是每次抓 cfs_rq 附近若干行,过滤出负载字段,sleep 1 拉开采样间隔。参数 c=12 控制每个 cfs_rq 带出的行数,队列字段多的时候可以加大;grep 里的 runnable 用来顺带看可运行任务数。读结果时先看趋势再看绝对值,load_avg 和 util_avg 的差值能告诉你任务是在等 CPU 还是在跑。

6.2 用 tracepoint 和算例交叉验证

tracepoint 适合看“什么时候更新”,算例适合看“更新成多少”。开 sched:sched_stat_runtime 和 sched:sched_migrate_task,跑一个短任务,确认 enqueue/dequeue/tick 三个调用点都出现;再把第 2 章的 python 算例改成和任务运行时间一致,对比 sched_debug 里的读数。注意 tracepoint 有开销,别在压测高峰期开全量。验证时优先看 update_load_avg 的调用次数和 last_update_time 的推进,读数对不上再回头查 accumulate_sum 的 period_contrib。

6.3 调参边界:哪些能改哪些别动

PELT 的 1024us、LOAD_AVG_PERIOD=32、LOAD_AVG_MAX=47742 是编译期常量,用户态没有 sysctl 能调,改它们等于改算法,负载均衡的收敛速度会跟着变。能调的是 CFS 的调度参数,比如 sched_latency_ns、sched_min_granularity_ns、sched_wakeup_granularity_ns、sched_migration_cost_ns,这些影响任务多久被切、唤醒后能不能抢、迁移代价怎么算,间接影响你看到的负载读数。调之前先记基线,一次只改一个参数,改完至少看三组 sched_debug 采样。

参数作用调 PELT 读数时的注意点
sched_latency_ns调度周期目标改小会让任务切得更频繁,util_avg 采样更碎
sched_min_granularity_ns单任务最小运行时间太小会增加切换,读数抖动变大
sched_wakeup_granularity_ns唤醒抢占粒度影响 runnable 时间,load_avg 跟着变
sched_migration_cost_ns迁移代价估计影响负载均衡选核,间接影响 se 迁移

血泪经验是别把 PELT 常量当性能开关,看到 util_avg 上不去就改 LOAD_AVG_PERIOD,最后负载均衡和功耗一起翻车。从那以后我每次改调度参数前,都先用 python 把 1024us 算例跑一遍,再上板子看 sched_debug,确认读数变化是参数引起的还是衰减补账引起的。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/3 10:53:20

射频自动化测试序列配置框架:从扫频实战到二次开发避坑

简介&#xff1a;面向高通芯片测试、射频验证与产线调试工程师的技术文档&#xff0c;围绕QSEQ测试序列系统梳理序列设计、环境与参数配置、执行记录及结果分析等核心环节&#xff0c;帮助读者理解如何组织复杂测试流程、定位执行异常并衔接QRCT相关操作。压缩包内共1个PDF文件…

作者头像 李华
网站建设 2026/11/3 10:52:44

GHOST 8.3 DOS环境系统备份还原实战指南

简介&#xff1a;《GHOST使用详解&#xff08;新手&#xff09;有图解》是一份面向零基础用户的系统备份与还原入门教程&#xff0c;围绕赛门铁克Ghost工具讲解分区镜像制作、系统恢复及DOS环境下的基本操作&#xff0c;帮助新手在系统故障或病毒侵扰后快速找回干净可用的系统。…

作者头像 李华
网站建设 2026/11/3 10:52:32

F5 BIG-IP密码恢复:console串口、tmsh改密与保存重启

简介&#xff1a;面向运维与网络安全从业者的一份F5 BIG-IP密码恢复专题文档&#xff0c;聚焦管理员遗忘密码后如何通过控制台完成重置&#xff0c;内容覆盖Ver9.0、10.0、11.0等版本&#xff0c;适合具备基础网络设备操作经验、需要应急处理ADC登录故障的读者参考。压缩包共1个…

作者头像 李华
网站建设 2026/11/3 10:52:29

F5 BIG-IP密码恢复:串口控制台与维护模式tmsh改密指南

简介&#xff1a;F5 BIG-IP 设备管理员密码遗忘后的恢复操作文档&#xff0c;面向网络运维工程师、系统管理员及安全测试学习者&#xff0c;聚焦 Ver9.0、10.0、11.0 三个版本的控制台破密与密码重置流程。内容围绕串口终端&#xff08;超级终端、SecureCRT、PuTTY&#xff0c;…

作者头像 李华
网站建设 2026/11/3 10:52:28

F5 BIG-IP密码恢复:console单用户模式重置admin/root

简介&#xff1a;面向运维工程师与网络设备管理人员的F5 BIG-IP密码恢复参考文档&#xff0c;聚焦管理员忘记密码后如何通过控制台完成密码重置&#xff0c;适用Ver9.0、10.0、11.0等版本。包内仅含1个docx文件&#xff0c;压缩包约92KB&#xff0c;篇幅精炼&#xff0c;便于在…

作者头像 李华
网站建设 2026/11/3 10:51:51

软件供应链安全实战指南:从SBOM到运行时防护的四层免疫体系

简介&#xff1a;本资源为奇安信代码安全实验室发布的《2024中国软件供应链安全分析报告》&#xff0c;面向安全研究人员、软件开发工程师及企业安全管理人员&#xff0c;系统回应软件供应链中恶意开源组件泛滥、陈旧漏洞复用、基础组件运维失管等核心风险。报告基于实测数据&a…

作者头像 李华