人可以不挣钱,但一定要干活,今天继续学习内核调度,写文章不容易,大家点点关注
PELT 内部机制与 CPU Capacity 模型
一句话总结:PELT 算法的核心是用指数衰减加权和持续维护 sched_avg 结构体中的统计字段,理解每个字段的含义(记录什么)、更新时机(何时触发)以及衰减逻辑(如何计算),是学习 PELT 算法的关键。也正是因为调度器通过统一接口(如 cpu_util_cfs()、task_util())访问负载数据,上层逻辑与底层跟踪算法解耦,才使得高通 WALT 这类算法可以替换 PELT,提供不同的统计策略(如固定窗口代替指数衰减),而无需修改 EAS、schedutil 等上层逻辑。
1. 目录
PELT 内部机制
· 11.1 pelt.c 入口函数与调用链
· 11.2 调度事件与 sched_avg 更新
· 11.3 运行时间的计算机制
cfs_rq→avg 聚合负载
· 12.1 队列聚合的含义
· 12.2 attach/detach:历史信息不丢失
· 12.3 cfs_rq→avg 的三大用途
· 12.4 util_avg 范围与繁忙判断
CPU Capacity 模型
· 13.1 设备树:capacity-dmips-mhz
· 13.2 归一化计算
· 13.3 运行时缩放与热压力
· 13.4 两种 capacity
· 13.5 完整数据流
· 13.6 实例分析:MTK DTS
PELT 时间域与跨核迁移补偿
· 14.1 睡眠期间 se->avg 的更新行为
· 14.2 高负载任务长时间睡眠对整体负载的影响
· 14.3 队列等待任务的 util_avg 滞后
· 14.4 tick 更新中 now 与 last_update_time 的归属
· 14.5 cfs_rq_clock_pelt() 实现与三层时钟
· 14.6 last_update_time 使用缩放时间
· 14.7 跨核迁移时间域修正:migrate_se_pelt_lag()
PELT 内部机制
2. 11.1 pelt.c 入口函数与调用链
pelt.c 没有单一入口,按调用方分层暴露几个对外接口:
bash
// 对外暴露的函数(被 fair.c 调用)
__update_load_avg_se() ← 更新单个调度实体(task/group)的 sched_avg
__update_load_avg_cfs_rq() ← 更新整个 cfs_rq 的 sched_avg
update_rt_rq_load_avg() ← RT 队列
update_dl_rq_load_avg() ← DL 队列
update_irq_load_avg() ← IRQ 负载(schedutil 用)
2.1 CFS 路径调用链
bash
fair.c: update_load_avg() ← fair.c 封装入口,每次调度事件触发
→ __update_load_avg_se() ← pelt.c,更新 se->avg
→ __update_load_avg_cfs_rq() ← pelt.c,更新 cfs_rq->avg
→ ___update_load_avg() ← 三个下划线,核心衰减计算
→ accumulate_sum()
→ decay_load() ← 最底层:位移 + 查表
2.2 触发时机
| 事件 | 调用点 |
|---|---|
| 任务入队 | enqueue_entity() |
| 任务出队 | dequeue_entity() |
| tick 更新 | entity_tick() |
| 任务迁移 | migrate_task_rq_fair() |
fair.c 的 update_load_avg() 打断点是最方便的入口,而非直接进 pelt.c。3. 11.2 调度事件与 sched_avg 更新
所有事件最终都更新 struct sched_avg,每个任务(se->avg)和每个运行队列(cfs_rq->avg)各持有一份:
c
struct sched_avg {
u64 last_update_time; // 上次更新时间戳
u64 load_sum; // 可运行时间衰减和(含等待队列)
u64 runnable_sum; // 可运行状态时间衰减和
u32 util_sum; // 实际在 CPU 上运行的时间衰减和
u32 period_contrib; // 当前周期内尚未满 1024μs 的余量
unsigned long load_avg; // load_sum 归一化后(乘调度权重)
unsigned long runnable_avg; // runnable_sum 归一化
unsigned long util_avg; // util_sum 归一化,[0, 1024](单任务)
};
3.1 enqueue_entity():任务入队(从睡眠唤醒)
| 操作对象 | 更新内容 |
|---|---|
se->avg |
对睡眠时长做衰减:睡了多久就衰减多少;util_avg 随之降低 |
cfs_rq->avg |
attach:把 se 的贡献量加入队列聚合值 |
更新字段:last_update_time、util_sum/avg(衰减后)、load_sum/avg、runnable_sum/avg
特殊逻辑:睡眠超过 160ms,util_avg 衰减至接近 0,避免"幽灵负载"拉高 cfs_rq
3.2 dequeue_entity():任务出队(进入睡眠)
| 操作对象 | 更新内容 |
|---|---|
se->avg |
把本次运行时间累加进 util_sum,计算最新 util_avg |
cfs_rq->avg |
detach:从队列聚合值中减去 se 的贡献量 |
更新字段:util_sum/avg(加入本轮运行)、load_sum/avg、runnable_sum/avg
特殊逻辑:detach 后 cfs_rq->avg 立即下降,schedutil 可能随之降频
3.3 entity_tick():周期 tick(任务正在运行)
| 操作对象 | 更新内容 |
|---|---|
se->avg |
累加当前 tick 内的运行时间到 util_sum,对已过去的整周期做衰减 |
cfs_rq->avg |
同步更新队列聚合值 |
更新字段:util_sum/avg、period_contrib(周期余量)
特殊逻辑:这是 schedutil 调频的主要数据来源;util_avg 稳定上升直至满载 = 1024
3.4 migrate_task_rq_fair():任务跨 CPU 迁移
| 阶段 | 操作对象 | 更新内容 |
|---|---|---|
| 迁出(旧 CPU) | old_cfs_rq->avg |
detach,移除 se 贡献量 |
| 时间补偿 | se->avg |
migrate_se_pelt_lag():补偿迁移期间的时间差,避免 util_avg 突变 |
| 迁入(新 CPU) | new_cfs_rq->avg |
enqueue 时重新 attach |
更新字段:last_update_time 被校正为新 CPU 的时钟基准
3.5 汇总对照
bash
事件 se->avg 变化方向 cfs_rq->avg 变化方向
─────────────────────────────────────────────────
enqueue 衰减(睡眠惩罚) +attach(增加)
dequeue 累加(运行贡献) -detach(减少)
tick 累加(持续运行) 同步增加
migrate 时间补偿修正 旧-detach / 新+attach
4. 11.3 运行时间的计算机制
PELT 不用"秒表",而是懒惰更新:每次调度事件触发时,回头算"上次更新到现在经过了多久,其中任务在跑吗?"
4.1 第一步:时间差
last_update_time记录的是每次更新的时间,配合上running标志,可以判断出是休眠时间还是运行时间。
bash
// pelt.c: ___update_load_sum()
delta = now - sa->last_update_time; // 距上次更新过了多久(纳秒)
delta >>= 10; // 纳秒 → 1024ns 单位(≈1μs)
sa->last_update_time += delta << 10; // 推进时间戳
now 来自 rq_clock_pelt(rq),是纳秒级单调时钟。在大小核异构系统中还会按 CPU 算力缩放,使不同核的时间可以比较。
4.2 第二步:判断任务状态
bash
// pelt.c: __update_load_avg_se()
___update_load_sum(now, &se->avg,
!!se->on_rq, // load: 任务是否可运行(running 或 waiting)
se_runnable(se), // runnable: 同上(区分组调度节流状态)
cfs_rq->curr == se); // running: 任务是否真的在 CPU 上运行
| 标志 | 为 1 的条件 | 累加的字段 |
|---|---|---|
load |
在队列中(running 或 waiting) | load_sum |
runnable |
同上(未被 bandwidth throttle) | runnable_sum |
running |
cfs_rq->curr == se(真正占着 CPU) |
util_sum |
4.3 第三步:把 delta 切分成周期并累加
bash
delta 时间段(可能跨多个 1024μs 周期):
│◄── d1 ──►│◄── 完整周期 ──►│◄── d3 ──►│
│ 上周期余量 │ period_0 │ period_1 │ 新周期起始 │
↑ ↑
衰减 × y¹ 权重 × y⁰
// pelt.c: accumulate_sum()
contrib = __accumulate_pelt_segments(periods, d1, d3);
sa->util_sum += running * contrib; // running=1 才加,sleeping 时=0
sa->load_sum += load * contrib;
sa->runnable_sum += runnable * contrib;
4.4 完整示例
bash
时间轴(1格 = 1024μs):
t0 t1 t2 t3
│─ 运行 ─│─ 睡眠 ─│─ 运行 ─│ ← 任务状态
└─ 上次更新 ──────────────────┘ ← t3 触发更新
t3 更新时:
delta = t3 - t0(整段时间一次性处理)
├── t0~t1 段:running=1 → util_sum += 贡献 × y² (已过2个周期,衰减)
├── t1~t2 段:running=0 → util_sum += 0 (睡眠,不计入)
└── t2~t3 段:running=1 → util_sum += 贡献 × y⁰ (最近,权重最高)
last_update_time 到 now 整段时间精确累计,精度是纳秒级,不受 HZ 影响。tick 采样是离散的(1ms/4ms/10ms),会漏掉 tick 之间的短暂运行;PELT 完全避开了这个问题。cfs_rq→avg 聚合负载
5. 12.1 队列聚合的含义
cfs_rq->avg 是该 CPU 上所有可运行任务的 se->avg 的实时叠加总和:
bash
CPU0 的 cfs_rq:
task_A se->avg.util_avg = 300
task_B se->avg.util_avg = 200
task_C se->avg.util_avg = 100
─────────────────────────────────
cfs_rq->avg.util_avg = 600 ← 队列聚合值
attach / detach 就是维护这个总和的加减操作:
bash
enqueue: cfs_rq->avg += se->avg // attach
dequeue: cfs_rq->avg -= se->avg // detach
好处是 O(1):任何时刻读 cfs_rq->avg 都直接拿到当前值,不需要遍历所有任务重新求和。
cfs_rq->avg 只统计当前可运行(runnable)的任务,睡眠任务故意不计入。睡眠任务不消耗 CPU,不应拉高当前 CPU 的繁忙程度。6. 12.2 attach/detach:历史信息不丢失
出队减掉 se->avg,历史信息并没有丢失——它保存在 se->avg 里,通过指数衰减持续更新:
bash
task_C 出队(去睡眠):
cfs_rq->avg -= se->avg // cfs_rq 减掉贡献
se->avg 本身保留,util_avg=300 // 历史记录完好,继续衰减
睡眠 32ms 后...
se->avg.util_avg 衰减到 ~150 // PELT 指数衰减(y^32 ≈ 0.5)
task_C 入队(唤醒):
先补算睡眠期间的衰减(decay_load)
再 cfs_rq->avg += se->avg // 带着衰减后的历史重新加入
PELT 的历史感知靠唤醒时带着衰减后的 util_avg 重新参与,而非"记住睡眠任务":
bash
任务繁忙运行期:util_avg = 900
↓ 睡眠 32ms(一个半衰期)
util_avg 衰减到 450
↓ 唤醒,attach 到 cfs_rq->avg
→ 调度器认为这是"中等负载任务",而非新来的空任务
6.1 load_avg 与 util_avg 的区别
| 统计内容 | 用途 | |
|---|---|---|
util_avg |
实际在 CPU 上运行的时间(util_sum 归一化) | schedutil 调频、EAS 选核 |
load_avg |
可运行时间 × nice 调度权重 | 负载均衡(高优先级任务贡献更大) |
runnable_avg |
可运行时间(不含权重) | 组调度内部使用 |
load_avg 包含 nice 权重:高优先级任务(nice=-20,权重≈88456)即使用同等 CPU 时间,对 cfs_rq->avg.load_avg 的贡献也远大于低优先级任务(nice=+19,权重≈15)。
7. 12.3 cfs_rq→avg 的三大用途
7.1 1. schedutil 调频
bash
// cpufreq_schedutil.c
util = cpu_util_cfs(rq); // 读 util_avg(+IRQ占用)
capacity = arch_scale_cpu_capacity(cpu);
freq = map_util_freq(util, max_freq, capacity); // 线性映射,留 25% 余量
util_avg = 0 → 降到最低频
util_avg = capacity → 拉到最高频
util_avg > capacity → 已过载,钉在最高频
7.2 2. EAS 选核:find_energy_efficient_cpu()
bash
// fair.c: find_energy_efficient_cpu()
// 对每个候选 CPU 计算:放入新任务后 cfs_rq->avg.util_avg 会增加多少?
// 基于 energy_model(能耗表)计算各 CPU 的能耗增量
// 选性能够用且能耗增量最小的那颗核
7.3 3. 负载均衡
bash
// fair.c: sched_balance_find_src_group()
// 最忙 group 的 load_avg 超过本地 group 的 125% 才触发迁移
if (busiest_load > local_load * 125 / 100)
→ 计算 imbalance,触发 detach_tasks() + attach_tasks()
注意:负载均衡用 load_avg(含 nice 权重),而非 util_avg
7.4 调用方使用 cpu_util_cfs(),而非裸值
c
// kernel/sched/sched.h
static inline unsigned long cpu_util_cfs(struct rq *rq)
{
unsigned long util = READ_ONCE(rq->cfs.avg.util_avg);
util += cpu_util_irq(rq); // 加上 IRQ 占用
return min(util, capacity_orig_of(cpu)); // 钳位到物理上限
}
8. 12.4 util_avg 范围与繁忙判断
cfs_rq->avg.util_avg 没有上界,可以超过 1024。它是所有可运行任务的叠加,代表需求量而非实际使用量:
bash
CPU0(单核,capacity=1024):
task_A util_avg = 700 (需要 70% CPU)
task_B util_avg = 600 (需要 60% CPU)
──────────────────────────────────────
cfs_rq->avg.util_avg = 1300 > 1024 ← 过载,供不应求
8.1 CPU 容量:基准是 1024
不同核的容量不同,调度器用 arch_scale_cpu_capacity(cpu) 读取:
| 核类型 | capacity_orig | 说明 |
|---|---|---|
| 大核(Cortex-X4) | 1024 | 基准,最强 |
| 中核(Cortex-A720) | ≈ 590 | 次强 |
| 小核(Cortex-A520) | ≈ 215 | 最省电 |
8.2 fits_capacity():80% 阈值
c
// fair.c: EAS 选核
static bool fits_capacity(unsigned long util, unsigned long capacity)
{
return util * 1280 < capacity * 1024; // util < capacity × 80%
}
// 等价于:util / capacity < 0.8
小核 capacity=215:
util_avg < 172 → fits,放小核(省电)
util_avg ≥ 172 → 不选小核,考虑中核(capacity≈590)
util_avg ≥ 472 → 不选中核,考虑大核(capacity=1024)
bash
util_avg 相对 capacity 的决策区间:
0 capacity×50% capacity×80% capacity capacity×N
│───────────┼──────────────┼─────────────┼──────────────►
│ 低负载 │ 中等负载 │ 不放新任务 │ 过载
│ 低频省电 │ 线性调频 │ 拉高频率 │ 钉最高频+触发均衡
CPU Capacity 模型
9. 13.1 设备树:capacity-dmips-mhz
CPU capacity 的原始来源是设备树中的 capacity-dmips-mhz 属性,表示每 MHz 能跑多少 DMIPS(相对值),由芯片厂商测定:
bash
/* arch/arm64/boot/dts/xxx.dtsi */
cpu@0 {
compatible = "arm,cortex-a520";
capacity-dmips-mhz = <382>; /* 小核,IPC 效率较低 */
};
cpu@4 {
compatible = "arm,cortex-a720";
capacity-dmips-mhz = <672>; /* 中核 */
};
cpu@7 {
compatible = "arm,cortex-x4";
capacity-dmips-mhz = <1024>; /* 大核,IPC 效率最高 */
};
capacity-dmips-mhz 只是 IPC 效率(每个时钟周期做多少工作),不是总算力。总算力还要乘以最大频率才能比较。两颗核可能 IPC 效率不同,但因为频率差异,总算力排名可能反转。10. 13.2 归一化计算
c
/* drivers/base/arch_topology.c */
// Step 1:乘最大频率,得到 raw_capacity(总吞吐量的相对值)
raw_capacity[cpu] = capacity_dmips_mhz × max_freq_MHz;
// Step 2:以所有核中最大值为基准,归一化到 1024
void topology_normalize_cpu_scale(void)
{
u64 max_cap = max(raw_capacity[]); // 找出最大值
for_each_possible_cpu(cpu) {
// SCHED_CAPACITY_SHIFT = 10,即 × 1024
scale = (raw_capacity[cpu] << 10) / max_cap;
topology_set_cpu_scale(cpu, scale); // 写入 per_cpu(cpu_scale)
}
}
10.1 计算示例(骁龙 8 Gen 3)
| 核 | capacity-dmips-mhz | 最大频率 | raw | capacity_orig |
|---|---|---|---|---|
| Cortex-X4(大核) | 1024 | 3300 MHz | 3,379,200 | 1024 |
| Cortex-A720(中核) | 672 | 2900 MHz | 1,948,800 | ≈ 590 |
| Cortex-A520(小核) | 382 | 1860 MHz | 710,520 | ≈ 215 |
capacity_orig 存储在 per_cpu(cpu_scale, cpu),由 arch_scale_cpu_capacity(cpu) 读取。
11. 13.3 运行时缩放与热压力
CPU 降频时,实际算力等比下降;过热被限频时,额外扣减热压力:
c
/* drivers/base/arch_topology.c */
// cpufreq 变频时触发通知回调
static void scale_freq_capacity(int cpu, u64 curr_freq, u64 max_freq)
{
per_cpu(arch_freq_scale, cpu) = (curr_freq << 10) / max_freq;
}
// 合并计算最终 capacity,写入 rq
void update_cpu_capacity(unsigned int cpu)
{
unsigned long cap = arch_scale_cpu_capacity(cpu); // capacity_orig
cap = cap_scale(cap, arch_scale_freq_capacity(cpu)); // × 频率比
cap -= arch_scale_thermal_pressure(cpu); // 减热压力
cpu_rq(cpu)->cpu_capacity = cap; // 写入 rq
}
12. 13.4 两种 capacity
| 含义 | 函数 | 用途 | |
|---|---|---|---|
capacity_orig |
最高频时的算力上限(固定值) | capacity_orig_of(cpu) |
EAS 能耗计算、cpu_util_cfs 钳位 |
capacity |
当前频率下的实际算力(动态) | capacity_of(cpu) |
schedutil 调频、fits_capacity 判断 |
bash
大核满频: capacity_orig = capacity = 1024
大核降半频: capacity_orig = 1024,capacity = 512
大核过热限频:capacity = capacity_orig - thermal_pressure < 1024
13. 13.5 完整数据流
bash
设备树 capacity-dmips-mhz
│
× max_freq_MHz
│
raw_capacity[]
│
÷ max(raw_capacity) × 1024 (归一化,最强核 = 1024)
│
per_cpu(cpu_scale) = capacity_orig ← arch_scale_cpu_capacity()
│
× (curr_freq / max_freq) ← cpufreq 变频时更新
│
- thermal_pressure ← 温控限频时扣减
│
cpu_rq->cpu_capacity ← capacity_of(),调度器直接使用
14. 13.6 实例分析:MTK DTS
以下 MTK 芯片 DTS 片段中,cpu7 的 capacity-dmips-mhz(937)看似小于 cpu5/6(1024):
bash
cpu5: cpu@101 {
capacity-dmips-mhz = <1024>;
performance-domains = <&performance 1>; /* 中核集群 */
};
cpu6: cpu@102 {
capacity-dmips-mhz = <1024>;
performance-domains = <&performance 1>; /* 中核集群 */
};
cpu7: cpu@200 {
capacity-dmips-mhz = <937>;
performance-domains = <&performance 2>; /* 大核集群(独立) */
};
14.1 为什么 cpu7 是大核却 capacity-dmips-mhz 更小?
capacity-dmips-mhz 只是 IPC 效率(每 MHz 的工作量),大核(X 系列)的设计取向是堆频率换性能:
| 特性 | 中核 A720(cpu5/6) | 大核 X 系列(cpu7) |
|---|---|---|
| IPC 效率 / MHz | 高(1024) | 稍低(937) |
| 最大频率 | 较低(~2.6 GHz) | 较高(~3.2 GHz) |
| 流水线宽度 | 较窄 | 更宽(更多执行单元) |
| 单线程绝对性能 | 中等 | 最高 |
代入归一化公式(假设 cpu5/6 max_freq=2600 MHz,cpu7 max_freq=3200 MHz):
bash
cpu5/6:raw = 1024 × 2600 = 2,662,400
cpu7: raw = 937 × 3200 = 2,995,200 ← 更高(频率优势弥补了 IPC 差距)
归一化后(以最大值为基准):
cpu7: 2995200 / 2995200 × 1024 = 1024 ← 最终是最强核
cpu5/6: 2662400 / 2995200 × 1024 ≈ 910
capacity-dmips-mhz 是内核计算的中间输入,不能直接比大小。调度器使用的是归一化后的 capacity_orig(= DMIPS/MHz × max_freq,再归一化)。cpu7 虽然每 MHz 效率低,但更高的最大频率让它最终成为算力最强的核,capacity_orig = 1024。PELT 时间域与跨核迁移补偿
15. 14.1 睡眠期间 se->avg 的更新行为
任务睡眠期间,se->avg 完全不会被更新——这是 PELT 刻意设计的懒惰求值,不是缺陷。
update_load_avg() 只在以下调度事件触发,睡眠中的任务不会触发任何一条:
bash
enqueue_entity() ← 进队列时
set_next_entity() ← 被选中即将运行时
put_prev_entity() ← 被抢占下来时
dequeue_entity() ← 离开队列时
唤醒时,enqueue_entity() 调用 update_load_avg(),对整段睡眠时间做一次性补算:
c
// 任务睡了 500ms 后唤醒:
delta = now - last_update_time ≈ 500ms → n ≈ 488 个周期
running = 0(睡眠期间不在 CPU 上)
decay_load() 处理大 n 的快速路径:
static u64 decay_load(u64 val, u64 n)
{
if (n >= LOAD_AVG_PERIOD * 63) // n >= 2016 周期 ≈ 2 秒
return 0; // 直接清零,O(1)
val >>= n / LOAD_AVG_PERIOD; // 每 32 周期右移一次(×0.5)
n %= LOAD_AVG_PERIOD;
return (val * runnable_avg_yN_inv[n]) >> 32; // 余数查表
}
| 睡眠时长 | 处理方式 | util_avg 结果 |
|---|---|---|
| < 32ms | 查表修正 | 衰减不多,仍有历史 |
| 32ms | 右移 1 位 | 减半 |
| ~160ms(5 个半衰期) | 右移 5 次 + 查表 | < 3%,接近消失 |
| ≥ 2 秒 | 直接 return 0 | 归零,O(1) |
16. 14.2 高负载任务长时间睡眠对整体负载的影响
cfs_rq->avg 在任务出队(进入睡眠)时立即下降,而非等待 PELT 慢慢衰减:
bash
dequeue_entity():
update_load_avg(cfs_rq, se, 0) // 先算清最后一段运行时间
detach_entity_load_avg(cfs_rq, se) // 立即从队列中减去贡献
→ cfs_rq->avg -= se->avg // ← 这一刻负载就降了
bash
多个高负载任务同时睡眠:
task_A util_avg=700 → detach → cfs_rq -= 700
task_B util_avg=600 → detach → cfs_rq -= 600
task_C util_avg=300 → detach → cfs_rq -= 300
睡前:cfs_rq->avg.util_avg = 1600(过载)
全部睡后:cfs_rq->avg.util_avg = 0 ← 立即降到 0
→ schedutil 立即降频,EAS 认为 CPU 空闲
| 数据 | 睡眠期间 | 说明 |
|---|---|---|
cfs_rq->avg |
立即下降(detach 时减去) | 只统计当前可运行任务 |
se->avg |
冻结,不更新,唤醒时一次性衰减 | 保留任务历史,供唤醒时选核参考 |
se->avg),整体 CPU 负载(cfs_rq->avg)始终只反映当前可运行任务,任务一睡就立刻摘除,不存在"幽灵负载拉高 cfs_rq"的问题。17. 14.3 队列等待任务的 util_avg 滞后
任务在队列中等待(runnable 但没在运行)时,se->avg 只在被调度事件触碰时才更新。如果因优先级低、竞争激烈而长时间未被调度,se->avg 就会冻结在上次更新的值。
但 cfs_rq->avg 不受影响,每个 tick 通过 entity_tick() → __update_load_avg_cfs_rq() 持续更新,使用的是 cfs_rq 级别的状态(load.weight、h_nr_running、curr != NULL),不依赖各个 se 是否最近被更新。
bash
now = T(当前时刻)
curr->avg.last_update_time = T - 4ms ← 每 tick 更新,非常新鲜
waiting_task->avg.last_update_time = T - 5分钟 ← 上次运行时设置,非常陈旧
cfs_rq->avg.last_update_time = T - 4ms ← 每 tick 更新,非常新鲜
等待任务最终被 set_next_entity() 选中时,update_load_avg() 对全部等待时间做一次性补算,running=0,大 delta 经 decay_load() 衰减到接近 0。
18. 14.4 tick 更新中 now 与 last_update_time 的归属
update_load_avg() 进入时取一次时间快照,se 和 cfs_rq 用同一个 now,但各自用自己的 last_update_time 计算 delta:
c
// kernel/sched/fair.c
static inline void update_load_avg(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags)
{
u64 now = cfs_rq_clock_pelt(cfs_rq); // ← 统一快照
__update_load_avg_se(now, cfs_rq, se); // delta = now - se->avg.last_update_time
update_cfs_rq_load_avg(now, cfs_rq); // delta = now - cfs_rq->avg.last_update_time
}
// ___update_load_sum() 内部(se 和 cfs_rq 都走这里)
delta = now - sa->last_update_time; // sa 是 &se->avg 或 &cfs_rq->avg
delta >>= 10;
sa->last_update_time += delta << 10; // 各自推进自己的时间戳
19. 14.5 cfs_rq_clock_pelt() 实现与三层时钟
c
// kernel/sched/fair.c
static inline u64 cfs_rq_clock_pelt(struct cfs_rq *cfs_rq)
{
if (unlikely(cfs_rq->removed.nr))
update_removed_cfs_rq(cfs_rq); // 组调度移除修正
return rq_clock_pelt(rq_of(cfs_rq)) - cfs_rq->removed.runnable_sum;
}
// kernel/sched/sched.h
static inline u64 rq_clock_pelt(struct rq *rq)
{
return rq->clock_pelt - rq->lost_idle_time; // 去掉空闲时间
}
rq->clock_pelt 由 update_rq_clock_pelt() 推进,按 CPU 算力缩放:
c
// kernel/sched/pelt.c
static inline void update_rq_clock_pelt(struct rq *rq, s64 delta)
{
if (unlikely(is_idle_task(rq->curr))) {
_update_idle_rq_clock_pelt(rq); // 空闲:clock_pelt 不推进
return;
}
// cap_scale(v, s) = v * s >> 10 = v * s / 1024
rq->clock_pelt += cap_scale(delta, arch_scale_cpu_capacity(cpu_of(rq)));
}
大核(capacity=1024):clock_pelt += delta × 1024/1024 = delta (全速)
中核(capacity=590) :clock_pelt += delta × 590/1024 ≈ 0.58×delta
小核(capacity=215) :clock_pelt += delta × 215/1024 ≈ 0.21×delta
19.1 完整调用链
bash
scheduler_tick()
→ update_rq_clock(rq) // core.c
delta = sched_clock_cpu() - rq->clock
rq->clock += delta
→ update_rq_clock_task(rq, delta)
rq->clock_task += delta
→ update_rq_clock_pelt(rq, delta)
if idle: lost_idle_time 增加,clock_pelt 不动
else: rq->clock_pelt += delta × capacity / 1024
19.2 三个时钟的关系
| 时钟 | 含义 | 特点 |
|---|---|---|
rq->clock |
原始单调时钟(纳秒) | 无论何种状态都推进 |
rq->clock_task |
去掉 IRQ/steal 的任务时钟 | 排除中断占用 |
rq->clock_pelt |
按算力缩放,空闲暂停 | PELT 使用,跨核可比 |
20. 14.6 last_update_time 使用缩放时间
last_update_time 存储的是 PELT 缩放时间,与 now 完全同一时间域。now 来自 cfs_rq_clock_pelt()(已缩放),last_update_time 每次更新都从同一个 now 写入:
bash
sa->last_update_time += delta << 10; // 存回缩放纳秒,与 now 同单位
last_update_time 本质上不是"几点几分",而是**“截至上次更新,这颗核累计做了多少等价工作量”**,和物理时间是不同的尺度。这保证了 delta = now - last_update_time 在同一颗 CPU 上始终正确。
21. 14.7 跨核迁移时间域修正:migrate_se_pelt_lag()
21.1 问题根源
bash
大核(capacity=1024):1ms 真实时间 → clock_pelt += 1.000ms
小核(capacity=215) :1ms 真实时间 → clock_pelt += 0.210ms
任务在大核最后更新时:last_update_time = 1000(大核时间域)
迁移到小核,小核当前:clock_pelt = 210(小核时间域)
直接相减:delta = 210 - 1000 = -790 ← 负数,错误
21.2 修正公式
bash
// 旧核上,从上次更新到迁移时刻的 lag(旧核时间域)
lag_old = now_old_pelt - se->avg.last_update_time
// 换算成真实时间(纳秒)
lag_real = lag_old * 1024 / capacity_old
// 换算成新核时间域
lag_new = lag_real * capacity_new / 1024
= lag_old * capacity_new / capacity_old ← 直接按算力比缩放
// 设置修正后的 last_update_time
se->avg.last_update_time = now_new_pelt - lag_new
21.3 完整迁移流程
bash
migrate_task_rq_fair(p, new_cpu) ← 在旧核上执行
│
├─ update_load_avg() // 算清旧核上最后一段运行时间
│ se->avg.last_update_time = now_old
│
├─ detach_entity_load_avg() // 从旧核 cfs_rq->avg 减去贡献
│
└─ migrate_se_pelt_lag(se) // 修正时间域
lag_old = now_old - lut_old
lag_new = lag_old × capacity_new / capacity_old
se->avg.last_update_time = now_new - lag_new
enqueue_entity() 在新核执行:
delta = now_new - (now_new - lag_new) = lag_new ← 正确
running = 0(迁移期间不在任何 CPU 上)
→ util_sum 按 lag_new 衰减,不累加运行时间
21.4 数值示例
bash
大核(capacity=1024)运行任务:
last_update_time = 5000(大核时间)
迁移时 now_old = 5100
lag_old = 100
迁移到小核(capacity=215):
lag_new = 100 × 215 / 1024 ≈ 21(小核时间)
小核当前 now_new = 1050(小核时间)
→ se->avg.last_update_time = 1050 - 21 = 1029
小核 enqueue 时:
delta = 1050 - 1029 = 21 ← 正确,不会突变
last_update_time 是 PELT 缩放时间域的时间戳,不同核的时间域不通用。跨核迁移必须按算力比(capacity_new / capacity_old)折算 lag,否则迁移后首次更新的 delta 会严重失真。
夜雨聆风