Linux 内核调度进阶 负载均衡与 Android 调度框架
1. 目录
PELT —— Per-Entity Load Tracking
· 为什么需要 PELT?
· 基本公式
· 半衰期的理解
· util_avg:归一化到 [0, 1024]
· 递推更新(O(1) 实现)
· uptime load average vs util_avg
负载均衡(Load Balancing)
· 为什么需要负载均衡?
· CPU 拓扑:sched_domain 树
· 触发机制:三条路径
· 核心算法:sched_balance_rq()
· sched_balance_domains():逐层扫描
· avg_vruntime_update():防止 min_vruntime 漂移
· 整体数据流
NUMA 架构与调度
· 从 UMA 到 NUMA:为什么产生
· 跨节点延迟的来源
· NUMA 节点的多种物理形态
· Linux NUMA 框架
· 调度器中的 NUMA
· NUMA 自动均衡(AutoNUMA)
· 手机芯片的 NUMA 状况
Android 调度框架:高通 vs MTK
· 共同基础:EAS
· EAS 工作原理
· 高通平台:WALT
· MTK 平台:FPSGO
· MTK vs 高通核心差异
· Android Cgroup 任务分组
· GKI 时代:上游内核 + vendor hook
· Android/嵌入式方向学习路线
关键数据结构与文件速查
· 关键源码文件
· 关键数据结构速查
· 常用调试命令
PELT —— Per-Entity Load Tracking
2. 为什么需要 PELT?
负载均衡需要比较各 CPU 的繁忙程度,但"繁忙程度"不能只看当前是否有任务运行,必须考虑近期历史。一个刚结束密集计算、暂时休眠的任务,仍然是"重任务",不应该和空闲 CPU 等同对待。
PELT 的核心思路:用指数衰减加权和把过去的运行历史折算成当前负载,近期权重高,远期自动淡化。
3. 基本公式
PELT 内核实际维护两套独立的累加流水线:
|
累加内容 |
归一化后得到 |
是否含 nice 权重 |
util_sum |
任务在 CPU 上运行的时间 |
util_avg |
否 |
load_sum |
任务可运行(运行+等待队列)的时间 |
load_avg |
是(乘以调度权重) |
两套 sum 的累加形式相同,区别仅在 time_i 的含义;权重只在最后归一化时才乘入 load_avg:
bash
/* kernel/sched/pelt.c: ___update_load_avg() */
sa->util_avg = sa->util_sum / LOAD_AVG_MAX; // 纯时间,无权重
sa->load_avg = (weight × sa->load_sum) / LOAD_AVG_MAX; // 乘调度权重
3.1 util_sum 累加公式
bash
util_sum = Σ (run_i × y^i) i = 0, 1, 2, ...
其中:
i = 距离现在的周期数(i=0 是当前周期,i=1 是 1ms 前,…)
run_i = 第 i 个周期内任务在 CPU 上的实际运行时间(0 ~ 1024 μs)
y = 衰减底数,y^32 ≈ 0.5(半衰期约 32ms)
y ≈ 0.97857(= 0.5^(1/32))
每个周期的长度为 1024 微秒(≈1ms),选 2^10 是为了方便位运算。
3.2 展开示意
bash
util_sum = run_0 × 1.000 ← 当前周期,权重最高
+ run_1 × 0.979 ← 1ms 前
+ run_2 × 0.958 ← 2ms 前
+ run_3 × 0.937 ← 3ms 前
+ ... ← 越旧权重越低
4. 半衰期的理解
4.1 衰减公式推导
① 基本定义
PELT 把时间划分为等长的基本周期(1024 μs ≈ 1ms)。设第 i 个周期前任务的实际运行时长为 run_i(取值 0~1024μs),y 为每周期衰减因子(y ≈ 0.97857)。
当前时刻 T 的负载累加值定义为:
bash
load_sum(T) = run_0·y⁰ + run_1·y¹ + run_2·y² + ...
= Σ run_i · yⁱ (i=0 为最近一个周期,越旧权重越低)
② 递推关系(O(1) 更新的来源)
时间推进一个周期,新周期的运行时长为 run_new:
bash
load_sum(T+1) = run_new·y⁰ + run_0·y¹ + run_1·y² + ...
= run_new + y·(run_0·y⁰ + run_1·y¹ + ...)
= run_new + y · load_sum(T)
这就是 PELT 的核心递推式:新值 = 本周期贡献 + 旧值×y。每个周期只需一次乘法和加法,时间复杂度 O(1),无需遍历历史。
③ 停跑 n 个周期的衰减
任务停止运行(run = 0)连续 n 个周期,递推式退化为:
bash
load_sum(T+1) = 0 + y · load_sum(T) = y · load_sum(T)
load_sum(T+2) = 0 + y · load_sum(T+1) = y² · load_sum(T)
...
load_sum(T+n) = yⁿ · load_sum(T)
即每停跑一个周期乘以 y,停跑 n 个周期后原来的历史贡献整体缩小为 yⁿ 倍。
④ 极值:LOAD_AVG_MAX
若任务始终满载运行(run_i = 1024 恒成立),load_sum 趋向上限:
bash
load_sum_max = Σ 1024·yⁱ (i=0..∞)
= 1024 × 1/(1-y)
≈ 1024 × 47.07
≈ 47742 ← 即内核常量 LOAD_AVG_MAX
util_avg 即用 util_sum / LOAD_AVG_MAX 归一化到 [0, 1024],满载任务 util_avg = 1024。
4.2 为什么选 y³² ≈ 0.5(32ms 半衰期)?
① 与显示帧率对齐
32ms ≈ 2帧(60Hz),调度器对负载的感知时间窗口和帧间隔同一量级——太短随瞬时抖动乱跳频,太长响应迟滞。
| 帧率 |
每帧时长 |
32ms 约等于 |
| 60 Hz |
16.7 ms |
2 帧 |
| 120 Hz |
8.3 ms |
4 帧 |
② 有效历史窗口约 160ms
几何级数无穷求和:
bash
Σ y^i (i=0..∞) = 1/(1-y) ≈ 47 个周期 = LOAD_AVG_MAX
经过 5 个半衰期(160ms),旧贡献衰减到 < 3%,相当于调度器只记得最近 160ms 的行为,与任务迁移、频率决策的响应周期吻合。
③ 实现上的工程便利
32 是 2 的幂,内核预计算 runnable_avg_yN_inv[32] 存储 y⁰~y³¹ 的定点近似值。每攒满 32 个周期,直接右移 1 位(等效 ×0.5),无需浮点运算:
c
/* kernel/sched/pelt.c */
static u64 decay_load(u64 val, u64 n)
{
if (n >= LOAD_AVG_PERIOD * 63)
return 0;
val >>= n / LOAD_AVG_PERIOD; // 整除部分:每 32 周期右移一次
n %= LOAD_AVG_PERIOD;
return (val * runnable_avg_yN_inv[n]) >> 32; // 余数:查表修正
}
4.3 衰减速率一览
| 停跑时长 |
衰减倍数 |
效果 |
| 32ms(32 个周期) |
y^32 ≈ 0.5 |
减半 |
| 64ms(64 个周期) |
y^64 ≈ 0.25 |
剩 1/4 |
| 96ms(96 个周期) |
y^96 ≈ 0.125 |
剩 1/8 |
| 160ms(160 个周期) |
y^160 ≈ 0.03 |
接近消失 |
5. util_avg:归一化到 [0, 1024]
原始 load_avg 没有上界,无法直接比较。内核对其乘以归一化系数 (1-y):
bash
任务以利用率 u 稳定运行(每周期运行 u×1024 μs):
load = u×1024 × Σ y^i = u×1024 × 1/(1-y) (等比级数)
util_avg = load × (1-y)
= u×1024 / (1-y) × (1-y)
= u × 1024 ← (1-y) 约掉了
🔑 关键:物理含义:util_avg ≈ CPU利用率 × 1024
• util_avg = 1024:任务一直在跑(100% CPU)
• util_avg = 512:跑 50% 时间
• util_avg = 0:从不运行
结果与衰减系数 y 无关,仅由真实利用率决定。
6. 递推更新(O(1) 实现)
内核不存储所有历史 run_i,只保留一个累计值,每次更新时:
bash
// pelt.c: decay_load()
new_load = old_load × y^n + 当前周期新贡献
实现:
val >>= n / 32; // 每 32 个周期右移一位(×0.5)
val = mul_u64_u32_shr(val, table[n%32], 32); // 查表乘余数衰减
7. 7.6 uptime load average vs util_avg
|
uptime load average |
PELT util_avg |
| 测量对象 |
可运行® + 不可中断睡眠(D) 的任务数量 |
单个任务/CPU 的利用率 |
| 数值范围 |
0 到 ∞(超过 CPU 数说明有积压) |
0 到 1024 |
| 时间常数 |
1 分钟 / 5 分钟 / 15 分钟 |
约 32ms 半衰期 |
| 更新频率 |
每 5 秒(LOAD_FREQ = 5*HZ+1) |
每次调度事件 |
| 作用 |
给人看,系统整体繁忙程度 |
给调度器用,负载均衡/cpufreq |
| 代码位置 |
kernel/sched/loadavg.c |
kernel/sched/pelt.c |
uptime load average 的公式(loadavg.c:25):
bash
avenrun[n] = avenrun[n] * exp_n + nr_active * (1 - exp_n)
常数(固定点,基数 2048):
EXP_1 = 1884 // 1884/2048 ≈ 0.920,每5秒衰减一次,对应约1分钟半衰期
EXP_5 = 2014 // 对应约5分钟
EXP_15 = 2037 // 对应约15分钟
负载均衡(Load Balancing)
8. 8.1 为什么需要负载均衡?
单 CPU 的 CFS/EEVDF 保证了本 CPU 内的公平,但 SMP 系统每个 CPU 有独立的 cfs_rq,它们之间天然不通信:
bash
CPU0: task1, task2, task3, task4 ← 4个任务排队
CPU1: (空闲) ← 全程空转
负载均衡的目标:**在不破坏 cache 局部性的前提下,让各 CPU 的负载尽量相等。**迁移任务有代价(cache 失效、NUMA 延迟),需要在"均衡收益"和"迁移代价"之间权衡。
9. 8.2 CPU 拓扑:sched_domain 树
内核把 CPU 组织成树形拓扑,每层叫一个 sched_domain(include/linux/sched/topology.h:88):
bash
典型 4核2NUMA 系统:
sched_domain[NUMA] ← 跨 NUMA 节点(代价最高,最少做)
sched_domain[MC] ← 跨物理核(中等代价)
sched_domain[SMT] ← 同一物理核的超线程(代价最低,最常做)
CPU0, CPU1 ← 超线程兄弟
sched_domain[SMT]
CPU2, CPU3
9.1 关键数据结构
c
// include/linux/sched/topology.h:88
struct sched_domain {
struct sched_domain *parent; // 向上(更大范围)
struct sched_domain *child; // 向下(更小范围)
struct sched_group *groups; // 本层 CPU 分组(循环链表)
unsigned long min_interval; // 最小均衡间隔(ms)
unsigned long max_interval; // 最大均衡间隔(ms)
unsigned int imbalance_pct; // 不均衡阈值(125 = 超25%才均衡)
int flags; // SD_LOAD_BALANCE, SD_BALANCE_WAKE...
unsigned long last_balance; // 上次均衡时间(jiffies)
unsigned int imb_numa_nr; // 允许NUMA不均衡的任务数阈值
};
// kernel/sched/sched.h:2158
struct sched_group {
struct sched_group *next; // 循环链表
struct sched_group_capacity *sgc; // 容量信息
unsigned long cpumask[]; // 该 group 覆盖的 CPU 集合
};
**关键设计:**越底层的 domain 均衡越频繁(间隔短),越顶层越稀疏(NUMA 均衡间隔可达数百 ms)。代价与频率成反比。
10. 8.3 触发机制:三条路径
bash
路径1(周期性):
timer tick
→ scheduler_tick() [core.c]
→ sched_balance_trigger() [fair.c:13284]
→ raise_softirq(SCHED_SOFTIRQ) ← jiffies >= next_balance 才触发
→ sched_balance_softirq() [fair.c:13261]
→ sched_balance_domains() [fair.c:12503]
→ sched_balance_rq() [fair.c:12047] ← 核心
路径2(newidle):
CPU 变空闲,pick_next_task() 找不到任务
→ sched_balance_newidle() ← 立刻拉任务,不等 softirq
路径3(NOHZ idle):
CPU 关闭 tick(idle 省电)
→ 其他 CPU 的 tick 代劳触发
→ nohz_idle_balance()
11. 8.4 核心算法:sched_balance_rq()
位于 fair.c:12047,是负载均衡的大脑。主干逻辑:
c
static int sched_balance_rq(int this_cpu, struct rq *this_rq,
struct sched_domain *sd, ...)
{
struct lb_env env = { .dst_cpu = this_cpu, .dst_rq = this_rq, ... };
redo:
if (!should_we_balance(&env)) // ① 我该负责均衡吗?
goto out_balanced;
group = sched_balance_find_src_group(&env); // ② 哪个 group 最忙?
if (!group) goto out_balanced;
busiest = sched_balance_find_src_rq(&env, group);// ③ 该 group 哪个 CPU 最忙?
if (!busiest) goto out_balanced;
detach_tasks(&env); // ④ 从 busiest 摘任务,加入 env.tasks 列表
attach_tasks(&env); // 挂到 this_rq
if (failed && active_balance)
kick_active_load_balance(); // ⑤ 亲和性受限 → 主动均衡(IPI)
}
11.1 ① should_we_balance():谁来主导
每个 domain 内只有一个 CPU 主导均衡(通常是 group 中 idle 最多或编号最小的 CPU),避免多个 CPU 重复迁移同一任务。
11.2 ② sched_balance_find_src_group():找最忙的 group
遍历本 domain 的所有 sched_group,计算加权平均负载,找出超过本地 group 超过 imbalance_pct(默认 125%)的那个,计算 env.imbalance(需要转移多少负载)。
三种不均衡类型:
-
migrate_util:利用率不均(cpufreq 感知场景)migrate_task:任务数不均(空闲 CPU 拉任务)
11.3 ③ sched_balance_find_src_rq():找最忙的 CPU
在最忙 group 内选出负载最高的那个 rq(busiest rq)。
11.4 ④ detach_tasks() + attach_tasks():实际迁移
bash
detach_tasks(&env):
遍历 busiest->cfs_rq 任务列表:
if (task_hot(p, env)) continue; // cache 热任务跳过
if (!can_migrate_task(p, env)) continue; // CPU 亲和性检查
detach_task(p, env); // 从 busiest rq 摘下
if (env.imbalance <= 0) break; // 够了就停
attach_tasks(&env):
将 env.tasks 中的任务逐一挂到 dst_rq,触发重调度
task_hot 判断(fair.c:9544):任务最近运行过(exec_start 距今 < cache_nice_tries × sysctl_sched_migration_cost),认为 cache 还热,不迁移。
11.5 ⑤ 主动均衡(Active Load Balancing)
所有任务因 CPU 亲和性限制无法迁移,但确实存在不均衡时,向 busiest CPU 发 IPI,让它主动把正在运行的任务推出来(active_load_balance_cpu_stop())。
12. 8.5 sched_balance_domains():逐层扫描
c
// fair.c:12503
static void sched_balance_domains(struct rq *rq, enum cpu_idle_type idle)
{
for_each_domain(cpu, sd) { // 从底层 SMT 向上到 NUMA 遍历
interval = get_sd_balance_interval(sd, busy);
if (time_after_eq(jiffies, sd->last_balance + interval)) {
sched_balance_rq(cpu, rq, sd, idle, ...);
sd->last_balance = jiffies;
}
}
}
每层 domain 有独立的 last_balance 时间戳,越底层(SMT)间隔越短(几 ms),越顶层(NUMA)越长(几百 ms)。
13. 8.6 avg_vruntime_update():防止 min_vruntime 漂移
min_vruntime 推进 δ 时,avg_vruntime 需要同步校正(fair.c:676):
c
void avg_vruntime_update(struct cfs_rq *cfs_rq, s64 delta)
{
// min_vruntime 推进 delta → 所有 key_i 都缩小 delta
// 直接在 avg_vruntime 字段上反映:
cfs_rq->avg_vruntime -= cfs_rq->avg_load * delta;
}
14. 8.7 整体数据流
bash
scheduler_tick()
│
sched_balance_trigger()
│ jiffies >= next_balance
SCHED_SOFTIRQ
│
sched_balance_softirq()
│
├─ nohz_idle_balance() (代替 idle CPU 做均衡)
└─ sched_balance_domains()
│
for_each_domain (SMT→MC→NUMA)
│ 按时间间隔逐层
sched_balance_rq()
│
┌─────────┼─────────────┐
should_we find_src find_src
_balance? _group _rq
│
detach_tasks() + attach_tasks()
│ 失败(亲和性)
kick_active_lb() (IPI)
NUMA 架构与调度
15. 9.1 从 UMA 到 NUMA:为什么产生
多 CPU 共用一条内存总线(UMA)时,CPU 越多总线争用越严重。解决办法:把内存控制器集成进 CPU Die,每颗 CPU 直连自己的 DRAM,CPU 之间用高速互连(Intel QPI/UPI,AMD Infinity Fabric)相连。
bash
UMA(旧): NUMA(新):
CPU0 CPU1 CPU2 CPU3 ┌─Socket0──────┐ ┌─Socket1──────┐
└─────┴─────┘ │ Core0-3 │ │ Core4-7 │
共享内存总线 │ 内存控制器 │◄►│ 内存控制器 │
│ │ DRAM 0 │ │ DRAM 1 │
DRAM └───────────────┘ └───────────────┘
扩展差,总线瓶颈 本地访问快,跨节点慢
16. 9.2 跨节点延迟的来源
CPU0 访问 DRAM 1(远端内存)的完整路径:
bash
CPU0 发出读请求 → cache miss
→ Socket0 内存控制器发送请求包 (+20~40ns QPI传输)
→ Socket1 内存控制器接收请求
→ 访问 Socket1 DRAM (+60~80ns 内存延迟)
→ 数据通过 QPI 传回 Socket0 (+20~40ns QPI传输)
→ CPU0 收到数据
总计:~140~160ns vs 本地 ~60~80ns(约 2×)
| 访问类型 |
延迟 |
| 本地 DRAM(同节点) |
60~80 ns |
| 远端 DRAM(1跳) |
120~150 ns(约 2×) |
| 远端 DRAM(2跳,4-socket) |
200+ ns(约 3×) |
| L3 Cache 命中 |
10~40 ns |
17. 9.3 NUMA 节点的多种物理形态
NUMA 节点的本质是"访问延迟不均匀的分组",物理实现不限于外接 DIMM:
| 形态 |
例子 |
特点 |
| 外接 DIMM(多 Socket) |
双路服务器 |
最常见,每 Socket 独立 DIMM |
| 同封装内多节点 |
AMD EPYC(多 CCD) |
单 Socket 内多个 NUMA 节点 |
| 封装内堆叠 HBM |
Intel Sapphire Rapids |
HBM=Node0(高带宽),DDR=Node1 |
| GPU 显存 |
NVLink 连接 H100 |
GPU HBM 作为独立 NUMA 节点 |
🔑 关键:核心规律:Linux 只关心 node_distances[][] 矩阵(ACPI SLIT),不关心物理形态。距离越大,代价越高。
18. 9.4 Linux NUMA 框架
c
系统
└── NUMA 节点 × N → struct pglist_data (pg_data_t)
├── 内存区域 → struct zone (DMA/Normal/Highmem)
│ └── 物理页帧 → struct page
└── 节点 CPU 集合 → node_to_cpumask_map[]
c
// include/linux/mmzone.h
typedef struct pglist_data {
int node_id; // 节点编号
struct zone node_zones[MAX_NR_ZONES]; // 该节点的内存区域
unsigned long node_start_pfn; // 物理页起始帧号
unsigned long node_present_pages; // 实际内存页数
} pg_data_t;
extern struct pglist_data *node_data[MAX_NUMNODES]; // 全局节点数组
节点间距离(典型 2-socket):
bash
Node0 Node1
Node0 10 21 ← 本地=10,远端=21(约 2.1×)
Node1 21 10
查看方式:numactl --hardware
19. 9.5 调度器中的 NUMA
NUMA 节点作为 sched_domain 树的最高层,带 imb_numa_nr 约束:
bash
sched_domain[NUMA] ← 跨节点,最贵,最少均衡(per_cpu: sd_numa)
sched_domain[MC] ← 跨物理核
sched_domain[SMT] ← 超线程
imb_numa_nr 的意义:跨 NUMA 迁移代价极高,允许一定程度的 CPU 不均衡来换取内存局部性。
任务迁移的两难:
bash
场景:Node0 有 8 个任务,Node1 有 2 个任务,任务 T 的内存全在 Node0
选项A:迁移 T 到 Node1 → 负载均衡了,但每次内存访问跨节点(慢 2×)
选项B:T 留在 Node0 → 负载不均,但内存访问快
imb_numa_nr 设定阈值:差异足够大才迁移。
20. 9.6 NUMA 自动均衡(AutoNUMA)
bash
1. 把任务的内存页标记为 PROT_NONE(故意触发缺页)
2. 任务访问时产生缺页中断 → task_numa_fault() 记录访问模式
3. 发现任务经常访问远端内存:
→ 把内存页迁移到本地节点(migrate_pages)
→ 或把任务迁移到内存所在节点(migrate_task_to)
目标:任务和它的内存尽量在同一 NUMA 节点
21. 9.7 手机芯片的 NUMA 状况
高通骁龙、MTK Dimensity 等手机 SoC 属于单节点 UMA:
-
- 内存(LPDDR5X)PoP 叠封装,访问延迟基本均匀
- Linux 看到 1 个 NUMA 节点,NUMA 均衡无意义
- 手机调度真正的挑战是异构 CPU 算力差异(大/中/小核),不是 NUMA
Android 调度框架:高通 vs MTK
22. 10.1 共同基础:EAS
两者都建立在 EAS(Energy Aware Scheduling)之上:
bash
ARM DynamIQ 硬件(大/中/小核异构)
│
EAS(Energy Aware Scheduling) ← 两者共用,建在 PELT/util_avg 上
│
schedutil governor ← 两者共用,util_avg → CPU 频率
│
各自私有扩展层 ← 差异在这里
23. 10.2 EAS 工作原理
入口函数:find_energy_efficient_cpu()(fair.c)
核心逻辑:在性能够用的前提下,选择能耗最低的那颗核
bash
任务 util_avg = 200(轻任务)
→ 小核算力 300 够用 → 放小核,省电 ✓
任务 util_avg = 800(重任务)
→ 小核 300 不够 → 放大核,保性能 ✓
决策依据:
util_avg(PELT)+ arch_scale_cpu_capacity()(CPU算力表)+ energy_model(能耗表)
24. 10.3 高通平台:WALT
高通用 WALT(Window Assisted Load Tracking)替换 PELT:
bash
PELT(上游): WALT(高通):
指数衰减(~32ms 半衰期) 固定窗口(默认 20ms)
平滑,对突发不敏感 响应快,对突发敏感
util = 加权历史平均 util = 窗口内运行时间/窗口长度
|
PELT |
WALT |
| 时间窗口 |
指数衰减(~32ms 半衰期) |
固定窗口(默认 20ms) |
| 响应速度 |
较慢(平滑) |
更快(突发感知强) |
| 数值特点 |
平滑,不易抖动 |
跳动较大 |
| 使用场景 |
上游 Linux / GKI |
高通 downstream kernel |
| 代码位置 |
kernel/sched/pelt.c |
kernel/sched/walt/(BSP kernel) |
WALT 对突发任务的优势:
bash
游戏场景突然变重:
PELT:util 缓慢上升 → 调频滞后 → 可能掉帧
WALT:util 立刻跳高 → 调频及时 → 响应快,功耗略高
25. 10.4 MTK 平台:FPSGO
MTK 的独特优势:FPSGO(Frame Per Second Governor),高通无对应机制。
25.1 FPSGO 解决的问题
EAS 只看 CPU 利用率,不知道当前在渲染哪一帧、帧率目标是多少,导致调频策略和实际帧渲染需求脱节。
25.2 FPSGO 工作原理
bash
渲染线程完成一帧
│
FPSGO 记录帧耗时
│
├── 帧耗时 > 目标(如 16.7ms @ 60fps)→ 拉升 CPU/GPU 频率
│
└── 帧耗时 < 目标(有余量)→ 降低频率,省电
目标:刚好达标,不浪费
25.3 FPSGO vs EAS 的信息对比
| 信息 |
EAS 是否知道 |
FPSGO 是否知道 |
| 目标帧率(60/90/120/144 fps) |
❌ |
✅ |
| 每帧的 Render Thread 耗时 |
❌ |
✅ |
| GPU 渲染耗时 |
❌ |
✅ |
| 帧与帧间的 jank(卡顿) |
❌ |
✅ |
| CPU 利用率(util_avg) |
✅ |
✅ |
代码位置(MTK BSP kernel):
bash
drivers/misc/mediatek/fpsgo_v3/
├── fpsgo_main.c ← 主控逻辑
├── fbt/ ← Frame Budget Tracker(CPU 频率控制)
├── fstb/ ← Frame Stabilizer(帧率稳定)
└── composer/ ← 与 SurfaceFlinger 交互
26. 10.5 MTK vs 高通核心差异
|
高通(Snapdragon) |
MTK(Dimensity) |
| 负载跟踪 |
WALT(替换 PELT) |
改良 PELT(保留并扩展) |
| 时间窗口 |
固定窗口(默认 20ms) |
指数衰减(32ms 半衰期) |
| 帧调度 |
依赖 Android 框架层 |
FPSGO(内核内置,功能强) |
| Boost 机制 |
Qualcomm Boost(perfmgr) |
MTK perfmgr |
| Thermal |
BCL + QMI thermal |
MTK thermal |
| 私有代码 |
kernel/sched/walt/ |
drivers/misc/mediatek/ |
27. 10.6 Android Cgroup 任务分组
Android 把所有进程按优先级分组,用 cpuset 限制可用 CPU:
bash
/dev/cpuset/
top-app/ ← 前台应用:所有大核可用,最高优先级
foreground/ ← 普通前台:部分核
background/ ← 后台:只允许小核
system-background/ ← 系统后台服务
restricted/ ← 严格受限
这直接决定每个应用能用哪些 CPU、能拿多少 CPU 时间,是 Android 性能管理的基础。
28. 10.7 GKI 时代:上游内核 + vendor hook
Android 12+ 要求遵守 GKI(Generic Kernel Image)规范:
bash
GKI 核心(上游 Linux + PELT + EAS) ← 两者共用,不能改
│
vendor hook(预留的扩展点) ← 两者各自挂自己的代码
│
├── 高通 vendor module:WALT、Boost
└── MTK vendor module:FPSGO、perfmgr
29. 10.8 Android/嵌入式方向学习路线
| 优先级 |
主题 |
入口 |
说明 |
| ★★★ |
EAS 深入 |
find_energy_efficient_cpu() |
直接延伸 util_avg,最高优先级 |
| ★★★ |
schedutil |
kernel/sched/cpufreq_schedutil.c |
util_avg → 调频决策 |
| ★★☆ |
Cgroup 分组 |
kernel/sched/fair.c 组调度部分 |
Android top-app 等分组机制 |
| ★★☆ |
CFS 带宽控制 |
fair.c 搜索 cfs_bandwidth |
后台任务 CPU 配额限制 |
| ★★☆ |
WALT |
高通 BSP kernel/sched/walt/ |
需要高通 BSP 源码 |
| ★★☆ |
FPSGO |
MTK BSP drivers/misc/mediatek/fpsgo_v3/ |
需要 MTK BSP 源码 |
| ★☆☆ |
RT 调度 |
kernel/sched/rt.c |
音视频低延迟场景 |
| ★☆☆ |
Binder 优先级继承 |
drivers/android/binder.c |
Android IPC 与调度交互 |
关键数据结构与文件速查
30. A.1 关键源码文件
| 文件 |
内容 |
| kernel/sched/fair.c |
CFS/EEVDF、负载均衡主体(sched_balance_rq、detach_tasks)、EAS |
| kernel/sched/pelt.c |
PELT 实现:decay_load()、update_load_avg() |
| kernel/sched/loadavg.c |
uptime load average(avenrun[]、calc_global_load) |
| kernel/sched/topology.c |
sched_domain 树构建 |
| kernel/sched/cpufreq_schedutil.c |
schedutil governor,util_avg → CPU 频率 |
| include/linux/sched/topology.h |
struct sched_domain、struct sched_domain_topology_level |
| include/linux/mmzone.h |
pg_data_t(NUMA节点)、struct zone |
| kernel/sched/energy.c |
EAS 能耗模型 |
31. A.2 关键数据结构速查
| 结构体 |
字段 |
含义 |
| struct sched_avg |
util_avg |
CPU 利用率,0~1024 |
| struct sched_avg |
load_avg |
含权重的负载,用于均衡决策 |
| struct sched_avg |
runnable_avg |
可运行状态时间占比 |
| struct sched_domain |
imbalance_pct |
不均衡触发阈值(默认 125) |
| struct sched_domain |
imb_numa_nr |
允许 NUMA 不均衡的任务数 |
| struct lb_env |
imbalance |
需要迁移的负载量 |
| struct lb_env |
src_rq / dst_rq |
迁移来源/目标 rq |
| pg_data_t |
node_id |
NUMA 节点编号 |
| pg_data_t |
node_zones[] |
该节点的内存区域 |
32. A.3 常用调试命令
bash
# 查看 NUMA 拓扑和距离
numactl --hardware
# 查看 NUMA 节点内存信息
cat /sys/devices/system/node/node0/meminfo
# 查看 schedstat(负载均衡统计)
cat /proc/schedstat
# 查看 CPU 拓扑
lscpu | grep -E "NUMA|CPU\(s\)|Core|Thread"
# 查看 cgroup cpuset(Android)
cat /dev/cpuset/top-app/cpus
# 实时查看 PELT util_avg(需要 trace)
echo 1 > /sys/kernel/debug/tracing/events/sched/sched_load_avg_task/enable