4.19 内核 EAS 调度器研究

荆雪

前情(不想看终端或者代码可以点小剪头隐藏↘️):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
Ran command: `adb shell "
uname -a
cat /proc/version
ls -la /dev/stune/
ls -la /dev/stune/top-app/
ls -la /dev/stune/foreground/
ls -d /sys/devices/system/cpu/cpu*/sched* 2>/dev/null
ls /proc/sys/kernel/sched*
"`
Ran command: `adb shell "
zcat /proc/config.gz | grep -E 'CONFIG_SCHED|ENERGY|SCHEDTUNE|PELT|WALT'
"`
Ran command: `adb shell "
cat /sys/devices/system/cpu/cpu*/topology/physical_package_id 2>/dev/null
cat /sys/devices/system/cpu/cpu*/topology/core_id 2>/dev/null
for i in 0 1 2 3 4 5 6 7; do
echo -n \"CPU\$i: max_freq=\$(cat /sys/devices/system/cpu/cpu\$i/cpufreq/scaling_max_freq) \"
echo -n \"min_freq=\$(cat /sys/devices/system/cpu/cpu\$i/cpufreq/scaling_min_freq) \"
echo -n \"gov=\$(cat /sys/devices/system/cpu/cpu\$i/cpufreq/scaling_governor) \"
echo -n \"cpu_capacity=\$(cat /sys/devices/system/cpu/cpu\$i/cpu_capacity 2>/dev/null) \"
echo \"\"
done
"`
Ran command: `adb shell "
for p in /sys/devices/system/cpu/cpufreq/policy*; do
echo \"=== \$p ===\"
ls \$p/schedutil/
cat \$p/schedutil/* 2>/dev/null
done
"`
Ran command: `adb shell "dmesg | grep -iE 'energy|eas|schedtune|capacity|sched:'"`
Ran command: `adb shell "grep -E 'find_busiest_group|select_task_rq|find_energy_efficient_cpu|schedtune' /proc/kallsyms"`
Checked task Check EAS kallsyms
Searched for files: *kernel*
Searched for files: *fair.c*
Listed directory kernel
Listed directory private
Searched for files: *
Listed directory msm-google
Ran command: `find port/ -name "fair.c"`
Ran command: `ls -la port/kernel-los/private/msm-google/kernel/sched/`
Viewed tune.c:1-100
Viewed tune.c:101-200
Ran command: `grep -rn "prefer_idle" port/kernel-los/private/msm-google/kernel/sched/`
Viewed fair.c:7170-7260
Viewed fair.c:7261-7380
Viewed fair.c:7370-7460
Viewed fair.c:7850-7980
Ran command: `grep -rn "uclamp_latency_sensitive" port/kernel-los/private/msm-google/kernel/sched/`
Viewed core.c:1390-1410
Viewed core.c:1380-1395
Viewed fair.c:8120-8280
Viewed fair.c:8000-8120
Ran command: `grep -rn "schedtune_task_margin" port/kernel-los/private/msm-google/kernel/sched/`
Viewed fair.c:6330-6370
Viewed fair.c:6280-6330
Ran command: `grep -rn "schedtune" port/kernel-los/private/msm-google/kernel/sched/cpufreq_schedutil.c`
Ran command: `adb shell "
ls -la /sys/devices/system/cpu/cpu0/
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_available_frequencies
cat /sys/devices/system/cpu/cpu6/cpufreq/scaling_available_frequencies
cat /sys/devices/system/cpu/cpu7/cpufreq/scaling_available_frequencies
"`
Ran command: `adb shell "
find /sys/firmware/devicetree/base/ -name '*energy*' 2>/dev/null
find /sys/firmware/devicetree/base/ -name '*opp*' 2>/dev/null
"`
Ran command: `adb shell "ls -la /sys/firmware/devicetree/base/cpus/"`
Ran command: `adb shell "ls -la /sys/firmware/devicetree/base/cpus/cpu-map/"`
Ran command: `grep -rn "em_register_perf_domain" port/kernel-los/private/msm-google/`
Viewed qcom-cpufreq-hw.c:355-385
Viewed qcom-cpufreq-hw.c:320-350
Ran command: `grep -rn "opp-microvolt" port/kernel-los/private/msm-google/arch/arm64/boot/dts/qcom/sm7250* 2>/dev/null`
Ran command: `find port/kernel-los/private/msm-google/ -name "*sm7250*.dts*"`
Ran command: `adb shell "ls -la /sys/firmware/devicetree/base/cpus/cpu@0/"`
Ran command: `adb shell "
for c in cpu@0 cpu@600 cpu@700; do
echo \"=== \$c ===\"
echo -n \"capacity-dmips-mhz: \"
xxd -p /sys/firmware/devicetree/base/cpus/\$c/capacity-dmips-mhz
echo -n \"dynamic-power-coefficient: \"
xxd -p /sys/firmware/devicetree/base/cpus/\$c/dynamic-power-coefficient
done
"`
Ran command: `adb shell "
cat /sys/kernel/debug/sched_features 2>/dev/null
for f in /proc/sys/kernel/sched*; do
if [ -f \"\$f\" ]; then
echo -n \"\$(basename \$f): \"
cat \"\$f\"
fi
done
"`
Ran command: `adb shell "mount -t debugfs none /sys/kernel/debug 2>/dev/null; cat /sys/kernel/debug/sched_features 2>/dev/null"`
Viewed features.h:1-110
Ran command: `adb shell "
for g in /dev/stune /dev/stune/*; do
if [ -d \"\$g\" ]; then
echo \"=== \$g ===\"
echo -n \"boost: \"; cat \"\$g/schedtune.boost\" 2>/dev/null
echo -n \"prefer_idle: \"; cat \"\$g/schedtune.prefer_idle\" 2>/dev/null
echo -n \"prefer_high_cap: \"; cat \"\$g/schedtune.prefer_high_cap\" 2>/dev/null
fi
done
"`
Ran command: `adb shell "
for g in /dev/cpuset /dev/cpuset/*; do
if [ -d \"\$g\" ]; then
echo \"=== \$g ===\"
echo -n \"cpus: \"; cat \"\$g/cpus\" 2>/dev/null
echo -n \"mems: \"; cat \"\$g/mems\" 2>/dev/null
fi
done
"`
Viewed powerhint.json:2-101
Ran command: `adb shell "head -n 100 /vendor/etc/powerhint.json"`
Ran command: `adb shell "grep -A 10 -B 2 'TASchedtune' /vendor/etc/powerhint.json"`

Pixel 5 (SM7250) Energy Aware Scheduling (EAS) 深度研究与架构解析

在 Android 与 Linux 异构多核(big.LITTLE / DynamIQ)体系中,EAS(能量感知调度) 是决定系统流畅度(帧率稳定性)能耗(发热续航)的核心中枢。

结合我们在 Pixel 5(高通骁龙 765G,Linux 4.19 内核)上的内核源码剖析(fair.ctune.cqcom-cpufreq-hw.c)以及实机设备树(DeviceTree)与调度器节点实测,对 EAS 的底层运行机制、配置逻辑以及此前引发微卡顿的深层成因进行完整复盘。


一、EAS 的诞生背景与核心目标

1. 传统 CFS 调度器的局限

传统的 Linux CFS(完全公平调度器)在设计时假设所有 CPU 核心是对等均质的(SMP 架构),其核心目标是:

  • CPU 间负载均衡(Load Balancing):尽量让每个核心上的任务数与权重保持一致。
  • 调度延迟公平(Fairness):按虚拟运行时间(vruntime)轮流推进。

但在现代移动端异构架构(如 SM7250 由 6 个低功耗小核 + 1 个大核 + 1 个超大核组成)下,CFS 的假设彻底崩溃:

  • 如果一个大任务被丢到小核,小核满载卡死,大核却在空转;
  • 如果系统把一堆后台轻量任务均衡到大核,会导致大核无法下电休眠,功耗暴涨、手机发烫。

2. EAS 的核心思想

EAS 的革命性改变在于将「CPU 选核(Placement)」与「CPU 调频(DVFS / Schedutil)」合二为一,并引入能量模型(Energy Model, EM)

  • 当一个任务唤醒(Wakeup)需要选择运行在哪个 CPU 时,调度器预先估算如果把任务放到 CPU $i$,整个 SoC(包括 CPU 功耗、跨簇 Cache 消耗等)会增加多少能耗;
  • 在满足该任务算力需求的前提下,优先挑选总体能耗增量最小的 CPU

二、Pixel 5 上的 EAS 物理与能量拓扑(实机实测)

通过提取设备树(/sys/firmware/devicetree/base/cpus/)与内核参数,Pixel 5 的 CPU 拓扑为标准的 1 + 1 + 6 三丛集(Tri-Cluster)

丛集(Cluster)物理核心微架构频点范围归一化算力 (capacity)动态功耗系数 (dynamic-power-coefficient)
Cluster 0 (Little)CPU 0–5 (6核)Cortex-A55576 MHz ~ 1.804 GHz452 / 1024100
Cluster 1 (Big)CPU 6 (1核)Cortex-A76652 MHz ~ 2.208 GHz942 / 1024341 (小核的 3.41 倍)
Cluster 2 (Prime)CPU 7 (1核)Cortex-A76806 MHz ~ 2.400 GHz1024 / 1024375 (小核的 3.75 倍)

内核实现特点:Google 主线 EAS vs 高通 WALT

  • 实测配置CONFIG_SCHED_WALT is not setCONFIG_SCHED_TUNE=yCONFIG_ENERGY_MODEL=y
  • 重要技术区别:大多数国产品牌的高通机型使用高通专有的 WALT(Window-Assisted Load Tracking)(配合 sched_boostsched_upmigrate 等节点);而 Google Pixel 坚持使用 Linux 主线标准 CFS + PELT(Per-Entity Load Tracking) 结合 Arm/Linaro 设计的 EAS + SchedTune

三、EAS 核心选核算法剖析(源码级追踪)

在内核源码 kernel/sched/fair.c 中,任务唤醒的主入口为 select_task_rq_fair $\to$ find_energy_efficient_cpu $\to$ find_best_target

1
2
3
4
5
6
7
8
9
10
11
12
flowchart TD
A[任务唤醒 ttwu] --> B[select_task_rq_fair]
B --> C{是否启用 EAS?}
C -->|否| D[常规 CFS 慢速负载均衡]
C -->|是| E[find_energy_efficient_cpu]
E --> F[find_best_target: 预选候选核]
F --> G{prefer_idle == 1?}
G -->|是| H{prefer_high_cap == 1?}
H -->|是| I[锁定大核/超大核: CPU 6-7]
H -->|否| J[小核陷阱: 寻找算力最小的空闲小核 CPU 0-5]
G -->|否| K[计算能耗: compute_energy]
K --> L[选择全 SoC 能耗增量最小的核心]

1. 致命的小核陷阱(Little Core Trap)

fair.cfind_best_target 中,有一段关键逻辑(行 7196–7384):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* 若同时设置了 prefer_idle 和 prefer_high_cap,目标算力从 0 开始往大找 */
if (prefer_idle && prefer_high_cap)
target_capacity = 0;

/* 在核心遍历循环中 */
if (prefer_idle) {
if (idle_cpu(i)) {
/* 如果 prefer_high_cap 为 0,寻找能效最高的核(即 capacity_orig 最小的小核) */
if (!prefer_high_cap && capacity_orig > target_capacity)
continue;

target_capacity = capacity_orig;
best_idle_cpu = i;
continue;
}
}
  • 问题根源
    当 Android 系统默认配置 schedtune.prefer_idle = 1prefer_high_cap = 0 时:
    • target_capacity 初始为 ULONG_MAX
    • 调度器遍历 CPU 0–5(小核,算力 452),发现空闲小核,将 target_capacity 赋为 452best_idle_cpu 记为小核;
    • 继续遍历到 CPU 6(大核,算力 942)时,capacity_orig (942) > target_capacity (452) 成立,大核被直接 continue 跳过抛弃
    • 遍历到 CPU 7(超大核,1024)同样被跳过!
    • 最终结果:UI 线程被 100% 绑定到 Cortex-A55 小核上!

2. 能耗模型在突发 UI 场景下的数学失效

如果 prefer_idle = 0,EAS 会调用 compute_energy(p, cpu, pd) 进行能量比较。
SoC 的动态功耗公式为:
$$P = C \cdot V^2 \cdot f$$
其中 $C$ 是 dynamic-power-coefficient。由前表可知:

  • Cortex-A55 小核:$C = 100$
  • Cortex-A76 大核:$C = 341$

Android UI 线程的物理特征是「短周期突发(Burst)」
在 90Hz 下,UI 线程每 11.1ms 唤醒一次,处理触摸事件和 View 绘制,通常只要几毫秒,其余时间处于休眠。

  • PELT 积分滞后:由于大部分时间在休眠,PELT 统计出的平均利用率(util_avg)只有 15% ~ 25%(约 150–250 / 1024)
  • EAS 的能耗计算误判
    • EAS 评估:负载仅 200,小核算力 452 足以容纳;
    • 放在小核跑,能量模型给出的功耗预估只有大核的 **$1/4 \sim 1/3$**;
    • 调度器判定:将 UI 线程分配到小核是最优节能选择
  • 现实灾难
    在小核(低 IPC、小缓存、分支预测弱)且缺少 AOT 机器码(ART Nterp 解释执行)的环境下,单帧 View 树绘制直接拉长到 18 ~ 21 ms,严重突破 11.1ms 的 VSYNC 截止线,产生肉眼可见的卡顿!

四、SchedTune 的桥梁作用与调频联动

为了解决突发负载被能耗模型「压制」的问题,Linux 4.19 / Android 引入了 SchedTune(定义在 tune.c):

1. SPC 信号比例补偿算法(Signal Proportional Compensation)

当在 cgroup 中写入 schedtune.boost 时,内核通过 schedtune_margin 进行负载放大计算:
$$M = \frac{\text{Boost}}{100} \times (\text{Capacity} - S)$$
放大后的感知负载:
$$S’ = S + M$$

  • Boost = 0 时:$M = 0$,$S’ = S$(无提速);
  • Boost = 30 时:即使任务刚唤醒时利用率 $S \approx 0$,放大后的感知负载直接达到 $0.3 \times 1024 \approx 307$!

2. 双向联动机制

  1. 联动 EAS 选核:$S’ = 307$ 使得任务更难被小核消化,迫使 EAS 将其迁移至大核;
  2. 联动 Schedutil 快速升频
    cpufreq_schedutil.c 中:
    1
    util += schedtune_cpu_margin_with(util, cpu, p);
    Schedutil 计算目标频率的公式为:
    $$f_{\text{target}} = 1.25 \times f_{\max} \times \frac{\text{util}}{\text{capacity}}$$
    有了 margin 加成,CPU 频率在任务唤醒的 第一个调度周期(500微秒内) 就能瞬间拉满,而无需等待数个 VSYNC 周期的 PELT 积分上升。

五、我们采取的联合治理策略与技术原理

在前面的攻坚中,我们针对上述机制实施了系统级重构,彻底闭环了微卡顿:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
┌────────────────────────────────────────────────────────┐
│ Android 用户交互 (90Hz) │
└───────────────────────────┬────────────────────────────┘

┌──────────────┴──────────────┐
▼ ▼
┌─────────────────────────┐ ┌─────────────────────────┐
│ 算力隔离 (Cpuset) │ │ 调度加速 (SchedTune) │
│ foreground / top-app │ │ prefer_high_cap = 1 │
│ cpus: 6-7 │ │ prefer_idle = 0 │
│ │ │ boost = 30 │
└────────────┬────────────┘ └────────────┬────────────┘
│ │
└──────────────┬──────────────┘

┌────────────────────────────────────────────────────────┐
│ Linux EAS (find_best_target) │
│ 1. 物理屏蔽 CPU 0-5 (A55 小核) │
│ 2. target_capacity 向上寻找大核 (CPU 6) / 超大核 (CPU 7) │
│ 3. Schedutil 500us 内瞬间拉升大核频率至 1.4GHz+ │
└───────────────────────────┬────────────────────────────┘

┌────────────────────────────────────────────────────────┐
│ 单帧 View#draw 耗时由 21ms 骤降至 0.5ms,0 掉帧达成 │
└────────────────────────────────────────────────────────┘
  1. 算力硬隔离(Hard Core Affinity)
    直接将 /dev/cpuset/foreground/cpustop-app/cpus 锁死为 **6-7**。从根源上截断 EAS 考虑小核 0–5 的可能性,无论能耗模型如何计算,前台交互线程只被允许在大核与超大核之间分配。
  2. 调度策略翻转(Prefer High Cap & Boost 30)
    • prefer_idle 设为 0,消除小核优先唤醒;
    • prefer_high_cap 设为 1,指令 EAS 优先选大核;
    • schedtune.boost 锁定为 30,消除唤醒升频迟滞。
  3. 固化 PowerHAL 回拨逻辑
    修改 /vendor/etc/powerhint.json,将默认索引固定为 30,阻断 libperfmgr 在手势松手后将 boost 降回 20。

六、EAS 的技术演进:从 SchedTune 到 UCLAMP

了解当前体系后,也可以清晰理解 Android 内核架构的发展脉络:

  • Linux 4.14 / 4.19(当前 Pixel 5 所处时代)
    采用 SchedTune。SchedTune 作为独立的 cgroup 子系统,粒度较粗(按 cgroup 进程组粒度配置),且无法针对线程级进行精细化动态钳位。
  • Linux 5.4+ / 5.10+ / 6.1+(Android GKI 时代)
    上游正式废弃 SchedTune,引入 UCLAMP(Utilization Clamping)
    • 直接在 CFS 调度实体 sched_entity 中原生嵌入 uclamp_minuclamp_max
    • 允许进程通过 sched_setattr 系统调用直接对特定线程(如只对 RenderThread)设置利用率下限;
    • 在我们目前的 Pixel 5 内核代码中,已经看到了向 UCLAMP 过渡的打桩接口(如 uclamp_latency_sensitive 封装了 schedtune_prefer_idle),正是这一演进时期的典型产物。