news 2026/10/15 8:20:51

Linux内核CFS调度器PELT负载跟踪机制详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux内核CFS调度器PELT负载跟踪机制详解

简介:一份深入讲解Linux CFS调度器PELT(per entity load tracking)算法的PDF文档,面向Linux内核开发者与调度器研究者。文档从Linux 3.8之前基于runqueue跟踪负载的缺陷切入,说明PELT如何通过跟踪每个调度实体的负载来提升调度精确度与稳定性,并给出基于Linux 4.18.0的代码分析。内容覆盖1024us周期划分、衰减因子y的计算、负载贡献公式以及decay_load()与runnable_avg_yN_inv数组的实现思路,配合连续运行4096us后的负载贡献示例,便于对照内核源码理解。资源为单一PDF文件,大小358KB,目前已有572人学习。适合希望深入理解CFS负载跟踪机制、搞清PELT原理与实现细节的中高级Linux内核读者。

1. CFS调度器为什么要引入PELT:把负载从rq拆分到调度实体

CFS调度器在 Linux 3.8 之前,负载跟踪的粒度是整个运行队列(runqueue,简称 rq)。这种方式只能看到一个总量,既说不清负载来自哪个进程,也扛不住任务进出队列带来的大幅波动——哪怕工作负载很稳定,rq 级的负载值也会抖得厉害。PELT(per entity load tracking)把统计粒度拆到每个调度实体(scheduling entity,简称 se),用固定的 1024us 周期配合衰减系数来算负载,调度器终于能回答“当前负载是谁贡献的”这个问题。这份 PDF 是基于 Linux 4.18.0 的源码笔记,把 PELT 的动机、负载公式、衰减查表、sched_avg 结构和更新路径完整串了一遍,适合正在啃内核源码但被一堆 sum/avg 弄晕的人,也适合做负载均衡排查时想搞清楚 load_avg 和 util_avg 到底怎么算出来的开发者。

2. PELT负载公式与1024us周期:衰减因子y和d1/d2/d3怎么拆

2.1 从rq级到per-entity:拆到什么粒度才合理

在 3.8 之前,CFS 在 rq 上做负载跟踪,调度器能看到“这个 CPU 上忙不忙”,但看不到“忙是因为哪个任务”。而且 rq 级负载统计的是整个队列的可运行总和,任务频繁增减、睡眠唤醒都会让这个值快速跳变,这给负载均衡和功耗调度都带来不少误判。PELT 的想法很直接:把负载统计下沉到每个调度实体,每个 se 单独维护自己的负载历史。

PELT 的基本计量方式是把物理时间切成固定 1024us 的周期。在每个周期内,如果一个实体处于 runnable 状态(正在 CPU 上跑,或者在等 CPU)的时间是 x,那它对系统负载的贡献就是 x/1024。历史周期里的贡献不能直接丢掉,否则负载就变成瞬时值了,所以要对过去的负载乘一个衰减因子,越老的周期权重越小:

L = L0 + L1*y + L2*y^2 + L3*y^3 + ... + Ln*y^n

这里 y 就是衰减系数。初次看到这个公式,估计你和我当时一样觉得是玄学。关键约束是y32 = 0.5,也就是经过 32 个周期(约 32ms)后,历史负载正好衰减一半。由y^32 = 0.5反推,得到y = 0.97857206。这个值不是随便拍的,它决定了负载的“记忆长度”:32 个周期剩一半,64 个周期剩四分之一,到 2016 个周期(约 2 秒)后在内核的定点计算里基本就衰减成 0 了。后文代码里的decay_load()就是围绕这个 y 值做定点加速。

2.2 手算4096us连续运行后睡眠的例子

光看公式不容易有手感,拿 PDF 里的经典例子走一遍。假设一个 task 从第一次加入 rq 开始,连续运行 4096us,之后一直睡眠。取若干采样点,看负载贡献怎么变:

时刻(us)负载贡献说明
10231023不满一个周期,只统计实际运行时间
20472025上一周期衰减一次,再加当前周期 1024
30713005继续累加并衰减
40953963运行满 4 个周期,负载接近峰值
51193877任务已睡眠,只有历史衰减
61433792继续衰减
71673709继续衰减
81913627继续衰减

看前四个采样点,任务还在运行,每个新周期会贡献 1024us 的 runnable 时间,同时历史负载全部乘一次 y。写成递推就是:

L1 = 1023 + 1024*y = (L0 + 1) * y L2 = 1023 + 1024*y + 1024*y^2 = (L1 + 1) * y L3 = 1023 + 1024*y + 1024*y^2 + 1024*y^3 = (L2 + 1) * y

注意(L0+1)*y这个变形,它利用了L0 = 1023刚好凑成 1024 的特性。从 5119us 开始,任务不再产生新的 runnable 贡献,所以计算式里不再加 1024,只剩历史总和乘衰减系数:

L4 = (L3 + 1) * y ≈ 3877

这里的 +1 是因为 4095us 到 4096us 那 1us 仍在运行,要先把上一个周期补齐再一起衰减。再往后就是纯衰减,L5 = L4*y,L6 = L5*y,每个周期乘一次 y。整张表走完,你会发现一个规律:算当前负载根本不用保存所有历史周期,只要记住“上一时刻的负载总和”,乘一次衰减系数,再加上当前周期的贡献就够了。这正是 PELT 能落地成 O(1) 计算的原因。

2.3 d1/d2/d3与通用公式:把问题变成数学推导

如果把上面的递推推广到任意两次更新时间之间,就需要把时间间隔 d 拆成三段。假设上一时刻的负载贡献是 u,距离上次更新已经过了 d 个 us。d 可以分成:

|<- d1 ->|<-- d2 -->|<- d3 ->| ---x---|---| ... |---|---x (now)

d1 是补齐上一个不完整周期的时间,d2 是完整周期的总时间,d3 是当前不足一个周期的剩余时间。用 p 表示这次更新跨越的周期数(p = 1 + d2/1024),那么通用计算公式是:

u' = (u + d1) * y^p + 1024 * (y + y^2 + ... + y^(p-1)) + d3

这个公式看着长,拆开就清楚:(u+d1)*y^p是把历史负载和补齐用的 d1 一起衰减 p 次;中间1024 * Σy^n是 d2 里每个完整周期的贡献,按距离当前时刻的远近距离分别衰减;最后的 d3 是当前周期还没满的部分,不衰减。拿 PDF 里的例子验证:上一时刻 u=1010,period_contrib=8,经过 d=2124us。d1=1016(补齐 1024-8),d2=1024,d3=84,p=2。套公式得到:

u' = 1010*y^2 + 1016*y^2 + 1024*y + 84 ≈ 3024

这和逐周期累加算出来的结果一致。有了这个通用公式,内核代码里accumulate_sum()和__accumulate_pelt_segments()的职责就很清楚了:前者算 Step 1(历史负载衰减),后者算 Step 2(d1、d2、d3 各自的贡献)。后文拆代码时,你会反复看到这三个变量。

3. 用查表代替浮点:runnable_avg_yN_inv与decay_load的实现细节

3.1 为什么内核不直接用浮点

调度路径上的函数对性能极其敏感,内核不允许在这里做浮点运算,所以 y^n 不能靠 pow() 现算。PELT 的做法是把 y^n 提前算好存成一张定点表,调用时直接用乘法加移位完成“乘衰减系数”的操作。

具体约定是:runnable_avg_yN_inv[n] = y^n * 2^32,n 取 0 到 31。因为y^32 = 0.5,所以 n 超过 31 时可以拆成“先除若干个 2,再查表”。生成这张表的代码在内核文档对应的Documentation/scheduler/sched-pelt.c里,函数是calc_runnable_avg_yN_inv(),核心逻辑等价于:

runnable_avg_yN_inv[i] = (2^32 - 1) * pow(0.97857206, i), 0 <= i < 32

实际算出来的表长这样,0xffffffff对应 y^0,后面的值逐个递减:

static const u32 runnable_avg_yN_inv[] = { 0xffffffff, 0xfa83b2da, 0xf5257d14, 0xefe4b99a, 0xeac0c6e6, 0xe5b906e6, 0xe0ccdeeb, 0xdbfbb796, 0xd744fcc9, 0xd2a81d91, 0xce248c14, 0xc9b9bd85, 0xc5672a10, 0xc12c4cc9, 0xbd08a39e, 0xb8fbaf46, 0xb504f333, 0xb123f581, 0xad583ee9, 0xa9a15ab4, 0xa5fed6a9, 0xa2704302, 0x9ef5325f, 0x9b8d39b9, 0x9837f050, 0x94f4efa8, 0x91c3d373, 0x8ea4398a, 0x8b95c1e3, 0x88980e80, 0x85aac367, 0x82cd8698, };

注意:这张表是“衰减系数 × 2^32”的定点表示,真正要算val * y^n时,用 64 位乘法把 val 和表项相乘,再右移 32 位,就得到近似的val * y^n。整个过程没有浮点,只有一次乘法、一次移位。

3.2 decay_load()两个分支

decay_load()是 PELT 最基础的函数,负责计算val * y^n,其中 n 的单位是“周期数”而不是 us。完整实现如下:

static u64 decay_load(u64 val, u64 n) { unsigned int local_n; if (unlikely(n > LOAD_AVG_PERIOD * 63)) /* 分支1 */ return 0; local_n = n; if (unlikely(local_n >= LOAD_AVG_PERIOD)) { /* 分支2 */ val >>= local_n / LOAD_AVG_PERIOD; local_n %= LOAD_AVG_PERIOD; } val = mul_u64_u32_shr(val, runnable_avg_yN_inv[local_n], 32); return val; }

逻辑说明:LOAD_AVG_PERIOD的值是 32,LOAD_AVG_PERIOD * 63 = 2016。当 n 大于 2016 时,历史负载在定点计算下已经小到可以忽略,直接返回 0。这个边界是 PELT 负载“记忆长度”的数学依据,也解释了为什么长睡眠任务唤醒后负载几乎清零。

第二个分支处理 n 超过一个“半衰周期”的情况。因为y^32 = 0.5,所以y^n = (1/2)^(n/32) * y^(n%32)。代码里用val >>= local_n / LOAD_AVG_PERIOD实现除以 2 的整数次幂,再对local_n %= 32查表得到y^(n%32)的定点值。最后一句mul_u64_u32_shr(val, runnable_avg_yN_inv[local_n], 32)完成定点乘法:val 乘以表项,再右移 32 位。这里的 shift 和表的 2^32 刻度正好抵消。

参数说明:val 是上一时刻的负载总和(load_sum、runnable_load_sum、util_sum 都走这个函数);n 是经过的周期数。注意 n 不是 us,调用方在accumulate_sum()里已经把时间差除以 1024 转成了 periods。如果 n=33,代码会先右移 1 位(相当于除以 2),再查runnable_avg_yN_inv[1]乘一次 y,得到val * 0.5 * y = val * y^33的定点近似。

3.3 LOAD_AVG_MAX=47742:为什么不是等比数列求和值

__accumulate_pelt_segments()里有个看起来很巧的计算:

c2 = LOAD_AVG_MAX - decay_load(LOAD_AVG_MAX, periods) - 1024;

它把“完整周期贡献的等比数列求和”变成了两个衰减值相减。数学推导在代码注释里写得很清楚:LOAD_AVG_MAX就是1024 * (1 + y + y^2 + ...)这个无穷等比级数的极限值。用等比数列求和公式算一下,1024 / (1 - 0.97857206) ≈ 47808,但内核代码里收敛出来的常量是 47742,差了几十个数。

这是因为LOAD_AVG_MAX不是用公式一次算出来的,而是用calc_converged_max()做了整数迭代:

void calc_converged_max(void) { int n = -1; long max = 1024; long last = 0, y_inv = ((1UL << 32) - 1) * y; for (; ; n++) { if (n > -1) max = ((max * y_inv) >> 32) + 1024; if (last == max) break; last = max; } printf("#define LOAD_AVG_MAX %ld\n", max); }

这个循环等价于反复执行max = max * y + 1024,直到两次迭代结果相同。由于每一步都用整数近似代替精确浮点,收敛值自然和无穷等比级数有差异。理解这一点很重要:后文___update_load_avg()里会用LOAD_AVG_MAX - 1024 + sa->period_contrib作分母归一化负载,如果你自己按数学公式推 LOAD_AVG_MAX,会跟内核实际行为对不上。

4. sched_avg数据结构与更新路径:load_sum、util_sum、period_contrib分别记什么

4.1 字段语义:六个参数分别代表什么

每个调度实体 se 和每个 cfs 就绪队列 cfs_rq 内部都嵌了一个struct sched_avg,PELT 的所有计算结果都存在这里。结构体定义如下:

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; };

字段含义整理成表:

字段含义
last_update_time上一次负载更新的时间,用于计算两次更新之间的时间差
load_sum基于 runnable 时间的负载贡献总和,runnable 包含“在 rq 等 CPU”和“正在 CPU 运行”两部分
runnable_load_sum基于可运行时间的负载贡献总和,语义与 load_sum 基本一致,用于组调度时区分不同来源
util_sum基于 running 时间的负载贡献总和,只统计真正在 CPU 上执行的时间
period_contrib上次更新时不足 1024us 的时间残留,跨周期计算时必须先把它补齐
load_avg平均负载,等于 load_sum 除以归一化上限
runnable_load_avg可运行时间维度的平均负载
util_avg平均利用率,等于 util_sum 除以归一化上限

简单记忆:load_sum 是“这任务在或不在 CPU 上都算”,util_sum 是“只在 CPU 上才算”。所以正常情况下util_avg <= load_avg,一旦你看到 util_avg 大于 load_avg,基本可以断定数据源选错了。

4.2 init_entity_runnable_average:task se和group se差别

调度实体初始化函数是init_entity_runnable_average(),它把 sched_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); se->runnable_weight = se->load.weight; }

注意 task se 和 group se 的行为完全不同。对 task se,load_avg和runnable_load_avg直接初始化为进程权重se->load.weight的折算值。内核这么做是故意的:新任务刚创建时没有历史负载,如果从 0 开始累计,负载均衡器会把它当成轻任务,可能做出错误决策,所以先按“重任务”给一个初始值,等运行几轮后负载自然收敛到真实水平。

对 group se 则保持 0。因为一个 task group 刚创建时底下没有任何任务,负载就是 0。runnable_weight字段对 task se 来说永远等于se->load.weight,对 group se 来说是可运行子实体的权重和,会随子任务入队出队不断更新。这个字段是后面update_cfs_rq_load_avg()里计算组负载的关键。

4.3 ___update_load_sum与accumulate_sum:周期累计与衰减

负载更新的主路径是___update_load_sum(),它先算时间差,再决定是否需要衰减计算:

static __always_inline int ___update_load_sum(u64 now, int cpu, struct sched_avg *sa, unsigned long load, unsigned long runnable, int running) { u64 delta; delta = now - sa->last_update_time; delta >>= 10; /* ns 转换成 us */ if (!delta) return 0; sa->last_update_time += delta << 10; if (!load) runnable = running = 0; if (!accumulate_sum(delta, cpu, sa, load, runnable, running)) return 0; return 1; }

逻辑说明:delta >>= 10是把纳秒差值除以 1024 转成微秒。PELT 的最小时间粒度是 1us,如果两次更新间隔连 1us 都不到,直接跳过这次计算。sa->last_update_time += delta << 10是把消费掉的时间补回 last_update_time,注意它用的是“转成 us 后再乘回 1024”的方式,意味着不足 1us 的纳秒余数被丢弃了,多次短间隔更新会累积少量精度损失。

accumulate_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; /* 1 */ periods = delta / 1024; /* 2 */ if (periods) { sa->load_sum = decay_load(sa->load_sum, periods); /* 3 */ 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); /* 4 */ } sa->period_contrib = delta; /* 5 */ 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; }

代码里的五个关键点分别对应前面公式的各个部分:注释 1 先把上次不足 1024us 的时间残留加上去;注释 2 算出跨越的完整周期数;注释 3 把历史三个 sum 一次性衰减;注释 4 用__accumulate_pelt_segments(periods, 1024 - period_contrib, delta)计算新周期的贡献,第一个参数是周期数,第二个参数是 d1(补齐上一周期的时间),第三个是 d3(当前不足周期的时间);注释 5 更新 period_contrib 为本次末尾的不足周期时间。最后按 load、runnable、running 三个开关,决定把 contrib 累加到哪些 sum 里。这里传进来的 load、runnable、running 在 se 场景下都是 0/1 布尔值,所以 se 的 load_sum 和 runnable_load_sum 数值上完全相同,区别只体现在组调度统计时。

4.4 更新时机:enqueue、dequeue、tick

update_load_avg()是入口,它的调用点很集中:向 rq 添加进程时(enqueue_entity)、从 rq 删除进程时(dequeue_entity)、以及调度器周期性 tick 时。每次更新都是先更新 se,再更新 se 挂载的 cfs_rq:

static inline void update_load_avg(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags) { u64 now = cfs_rq_clock_task(cfs_rq); struct rq *rq = rq_of(cfs_rq); int cpu = cpu_of(rq); int decayed; if (se->avg.last_update_time && !(flags & SKIP_AGE_LOAD)) __update_load_avg_se(now, cpu, cfs_rq, se); decayed = update_cfs_rq_load_avg(now, cfs_rq); }

__update_load_avg_se()内部先处理runnable_weight,再调___update_load_sum()。之后update_cfs_rq_load_avg()会把 cfs_rq 的负载重新聚合一次。rq 级负载实际上就是底下所有 se 负载的聚合结果。调试时我一般会先在/proc/sched_debug里看 cfs_rq 和 se 的 load_avg、util_avg,确认数值趋势和预期一致,再去翻 trace 事件定位是哪条更新路径触发的。如果数值跳变异常,先查last_update_time是否长时间没更新,再查是不是跨了迁移路径。

5. 排查与避坑:PELT负载四类常见偏差和定位手段

5.1 手算负载与内核输出总是差几个us

现象:按照 1024us 周期和 y 值手算负载贡献,再去对比/proc/sched_debug里读出的 load_avg,发现总有 2~5us 的出入,有时能差到 10us 以上。

原因:第一,___update_load_sum()里delta >>= 10是向下取整,不足 1us 的时间被丢掉;第二,period_contrib的残留时间会在跨周期时参与补齐,手算时如果漏算了上一次的 period_contrib,结果必然偏小;第三,内核用查表和定点乘法近似 y^n,而手算用浮点 y=0.97857206,两者本身就有取整差异。

解决:不要拿手算结果去对内核的绝对值。先确认自己用的 period_contrib 是否和当前内核版本一致,再看衰减周期有没有超过 32 的整数倍(超过后内核走的是右移分支,浮点差异会叠加)。想要精确对拍,启动 tracepoint 抓sched_load_avg事件,拿到内核自己的中间值再验证公式方向对不对。

5.2 新任务load_avg一开始等于权重,误以为负载异常

现象:fork 出来的新 task,load_avg一瞬间就有几千甚至上万,看起来跟跑了很久的重任务一样,但util_avg却从 0 开始慢慢爬。

原因:init_entity_runnable_average()对 task se 初始化时直接把load_avg设成权重的折算值,这是有意为之,让调度器在任务没有历史数据时先按“偏重”处理,避免刚创建就被误判为轻任务。util_avg则从 0 开始累计,两者天然不同步。

解决:分析负载时把 load_avg 和 util_avg 分开看:load_avg 是调度权重维度的指标,util_avg 才是真实 CPU 利用率。如果你想验证某个新任务的真实开销,等它运行几十毫秒后再读数,别在刚 fork 完的时刻下结论。如果不想让新任务冲击组负载,可以把任务放进独立 task group,用 group se 的负载隔离。

5.3 长睡眠任务唤醒后负载归零,以为负载“丢了”

现象:一个任务睡眠超过 2 秒再被唤醒,它的util_avg和load_avg几乎从零重新开始积累,之前跑过的负载历史好像完全不存在。

原因:decay_load()对n > 2016直接返回 0。2016 个周期约等于 2.06 秒,超过这个时间,定点衰减结果小到可以被安全忽略。这是 PELT 刻意设计的行为:负载是近期行为的统计,不是永久的任务画像。

解决:如果业务上确实需要保留长睡眠任务的历史负载,PELT 本身做不到,得靠 cgroup 的 task group 在更高层级做聚合统计。另外排查问题时要注意把 sleep 时间和 2016 周期对比一下,看到负载归零先算算是不是衰减边界,别一头扎进代码找“丢失的负载”。

5.4 cfs_rq的load_avg不等于se负载之和,对账翻车

现象:把一个 rq 下所有 se 的 load_sum 累加,跟 cfs_rq 里读到的 load_avg 对不上,差值还不小。

原因:cfs_rq 的负载聚合路径和 se 的单独更新不是同一套逻辑。group se 在 cfs_rq 里只占一个实体位,但它内部的子实体负载变化不会即时反映到父级;而且 se 和 cfs_rq 的衰减时机不同步,cfs_rq 的 period_contrib 是整组共用的,子实体入队出队会打断各自的周期对齐,导致单纯累加不成立。

解决:对账时不要用“子级累加”去验证父级,直接读 cfs_rq 的avg.load_avg和avg.util_avg作为权威值。需要看单个 task 的贡献时,看它自己的se.avg.load_avg,两个维度的数据各自独立使用。如果确实要分析组内构成,先同步所有子实体的 last_update_time,再在一个 tick 边界采样。

5.5 迁移到新CPU后负载跳变,单核数据不好解读

现象:任务从 CPU0 迁移到 CPU1 后,新 CPU 上的 rq 负载瞬间出现一个明显凸起,过几个 tick 才平稳。

原因:迁移时 se 的avg数据会跟着实体一起过去,但新 CPU 的 cfs_rq 聚合只按当前时间补齐衰减,不包含迁移前的完整上下文,所以新 rq 的负载会被短暂拉高。等 se 在新 CPU 上跑几个周期后,衰减机制会把它拉回正常水平。

解决:分析迁移类问题不要只看单个 CPU 的 load_avg,只看单核容易得出“迁移导致负载飙升”的错误结论。建议看整个 LLC 域的负载总和,或者把迁移前后的sched_switch和sched_load_avgtrace 放一起对比,确认跳变是补齐衰减造成的瞬态,而不是调度器误判。这个坑我踩过几次,现在每分析跨 CPU 迁移都先打 trace 再下结论。

6. 进阶验证:用Python脚本复现PELT递推,对照sched_debug看负载曲线

读完前面的公式和代码,最有效的巩固方式是动手写一个最小模拟器,把 4096us 运行再长睡眠的例子跑出来,和内核行为做对比。下面的脚本用浮点近似内核查表,时间粒度为完整 1024us 周期:

import math PERIOD = 1024 Y = 0.5 ** (1 / 32) # 0.97857206 DECAY_LIMIT = 2016 # 超过2016周期直接按0处理 def decay_load(val, periods): """模拟内核decay_load(): val * y^periods""" if periods > DECAY_LIMIT: return 0 if periods >= 32: val >>= periods // 32 periods %= 32 return val * (Y ** periods) def pelt_from_periods(contribs): """contribs: 每个1024us周期内runnable的us数""" total = 0.0 for x in contribs: total = total * Y + x return total def simulate_run_sleep(run_us, sleep_us): """模拟连续运行run_us后睡眠sleep_us,返回关键采样点""" points = [] # 前几个周期:运行期每个周期贡献1024,最后一个不满周期按实际us计 full_periods = run_us // PERIOD remainder = run_us % PERIOD contribs = [PERIOD] * full_periods if remainder: contribs.append(remainder) for i in range(1, len(contribs) + 1): points.append((i * PERIOD - 1, pelt_from_periods(contribs[:i]))) # 睡眠期:后续周期补0,贡献为0 for i in range(1, 8): sleep_contribs = contribs + [0] * i points.append((run_us + i * PERIOD - 1, pelt_from_periods(sleep_contribs))) return points for t, load in simulate_run_sleep(4096, 8191): print(f"t={t:5d}us load={load:8.1f}")

逻辑说明:pelt_from_periods()里的total = total * Y + x是整个 PELT 最核心的递推——历史负载乘一次衰减系数,再加上当前周期的可运行时间。simulate_run_sleep()先用 1024us 周期的贡献数组模拟连续运行 4096us,再用补 0 模拟睡眠,采样点和前文表格一一对应。

参数说明:PERIOD对应内核的 1024 周期,Y对应 y 值,DECAY_LIMIT对应内核的LOAD_AVG_PERIOD * 63。注意脚本用了浮点,内核用定点查表,所以最终数值会有 1~3us 的偏差,这是正常的,数量级对得上就说明理解一致。

跑出来的结果会看到运行期负载逼近 47742 附近再回落,睡眠期逐步衰减。想继续深挖,可以把脚本里的多个 Y 值改成不同衰减系数(比如Y = 0.99),对比负载收敛速度;也可以把contribs数组替换成实际 trace 抓到的 runnable 时间序列,验证真实任务在一段时间内的负载曲线。从那以后我每次手动分析 PELT 负载,都会先拿这套脚本垫一遍数据,再对照/proc/sched_debug里的真实输出,两个数值贴近了才敢继续往下写分析。希望帮到你。

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

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

ADS中ACPR/EVM/PAE联合仿真:搭建、调试与避坑指南

简介&#xff1a;这份应用笔记共1个PDF文件&#xff0c;压缩包1.8MB&#xff0c;面向通信与射频设计工程师&#xff0c;尤其适合需要评估功率放大器线性度、调制精度和效率的团队。内容以EDGE信号为例&#xff0c;基于ADS Ptolemy协同仿真搭建从信号源到功放子电路再到测量接收…

作者头像 李华
网站建设 2026/10/15 8:20:04

artcraft:面向创意工作者的效率工具,如何降低从灵感到成品的摩擦

1. 从“artcraft”这个名字说起&#xff1a;它到底想解决什么问题第一次看到“artcraft”这个词&#xff0c;我脑子里蹦出来的不是某个具体工具&#xff0c;而是一种很朴素的组合感——art 加 craft&#xff0c;艺术加手艺。这两个词放在一起&#xff0c;本身就带着一种张力&am…

作者头像 李华
网站建设 2026/10/15 8:19:55

线性代数工程实战:从空间变换到SVD矩阵分解避坑指南

简介&#xff1a;面向需要系统梳理线性代数核心知识点的学生与自学者&#xff0c;这份总结以章节形式系统讲解行列式与矩阵两大主题。行列式部分从二阶、三阶行列式的对角线法则出发&#xff0c;依次介绍全排列与逆序数、对换与排列奇偶性、n阶行列式的定义与符号规则、行列式的…

作者头像 李华
网站建设 2026/10/15 8:19:46

AI圈正在悄悄崛起一个新岗位:FDE,懂落地的人正在疯狂溢价!!!

AI岗位那么多&#xff0c;为什么我劝你盯住FDE&#xff1f;别再死磕大模型了。2026年最值钱的AI能力&#xff0c;是让AI在一家公司里“活下来”。我见过太多人&#xff0c;啃了半年Python&#xff0c;调了几百次API&#xff0c;简历上还是只有一个“聊天机器人Demo”。不是你不…

作者头像 李华
网站建设 2026/10/15 8:19:44

技术文章标题优化:提升点击率的实用指南

1. 项目概述"测试文章标题"这个看似简单的短语背后&#xff0c;其实隐藏着内容创作者每天都要面对的挑战。作为在技术社区混迹多年的老鸟&#xff0c;我深知一个好标题对文章传播效果的决定性影响。这篇文章将分享我在CSDN等平台多年实践中总结出的标题优化方法论&am…

作者头像 李华
网站建设 2026/10/15 8:19:43

USB HID Usage Tables 1.3 实战指南:从报告描述符到避坑解析

简介&#xff1a;《USB HID Usage Tables 1.3》是由USB Implementers Forum发布的人机交互设备使用表规范&#xff0c;覆盖1996年至2022年的全部更新&#xff0c;面向硬件工程师、固件与驱动开发人员、系统集成商&#xff0c;用来统一键盘、鼠标、触摸屏、游戏控制器等设备的功…

作者头像 李华