一个 Linux 调度器优化,让 Android 多耗 20% 的电,传音工程师如何发现问题?

一段代码之前是好的,但是经过多年迭代之后,虽然没动过,但是可能就变成了问题,这就是这次要聊的话题:

一个曾经为了改善 Android UI 流畅度而引入的调度器优化,几年后反而让 SoC 功耗最高增加 20%

事情的起因是 Linux 调度器里的 cpu_util() boost 机制 ,最近传音控股的工程师 Hongyan Xia 在 LKML 提交了一个 patch:

bash 复制代码
[PATCH] sched/fair: Revert boost in cpu_util()

这个 patch 主要是把 cpu_util() 里基于 runnable_avg 的 boost 逻辑撤掉,虽然看起来像是一个调度器的小改动,但背后其实牵扯到了 PELT、schedutil、Android 图形管线、JankBench、ADPF、厂商功耗策略,以及 Linux 主线内核和 Android 真实设备之间的验证偏差等问题。

大概情况是,传音的工程师在升级 Linux 内核版本后,发现多款 Android 手机的 SoC 功耗出现明显上升,而且在多个真实工作负载里都出现了类似事情,不局限某个 App 或者芯片,他们测试的场景包括:

  • YouTube 1080p 60 播放
  • 手机游戏,例如无尽对决、原神
  • 多种真实手机工作负载

而出现问题的情况是:

  • CPU 频率更容易维持在高档位
  • 功耗却明显增加
  • 部分场景 SoC 功耗增加约 20%

于是他们开始做 git bisect,最终定位到 Linux CPU 调度器和 schedutil CPU 频率调整路径里的 boost 逻辑,这个 boost 逻辑的关键点是:

过去 schedutil 主要看 util_avg ,后来为了更积极响应 CPU contention,又额外看 runnable_avg,如果 runnable_avg 暗示"排队工作很多",就提高 CPU 频率。

也就是说,系统看到很多任务处于 runnable 状态,就认为 CPU 可能不够用,于是更积极地拉高频率,但是传音的工程师实测结果却是,这种处理逻辑在现在的 Android 上很不合理,它让 CPU 积极提频,但没有换来对应的性能收益

这个问题就需要从 Linux 调度器聊起,Linux 里有一个很核心的负载追踪机制,叫 PELT,全称是 Per-Entity Load Tracking,它大致会追踪两个东西:

  • util_avg:任务真正拿到 CPU 执行的时间
  • runnable_avg:任务处于 runnable 状态的时间,包括正在运行,也包括排队等待 CPU

这两个指标在 CPU 竞争场景下差异很大,举个例子,比如一个 CPU 上同时有 4 个任务争抢:

arduino 复制代码
Task A
Task B
Task C
Task D

如果它们都很想跑,但每个任务只能拿到 25% 的 CPU 时间,那么每个任务的 util_avg 看起来都不高,从单个任务角度看:

arduino 复制代码
Task A 实际只运行 25%
Task B 实际只运行 25%
Task C 实际只运行 25%
Task D 实际只运行 25%

于是 util_avg 会显得偏低,但真实情况是 CPU 已经被抢满了,这就是 boost 机制当初想解决的问题。

如果调度器只看「任务实际跑了多久」,就可能低估 CPU 需求,而如果把「任务排队等了多久」也算进去,就能更早发现 CPU contention,所以这个设计的初衷是:

util_avg 反应慢,那就引入 runnable_avg。 如果 runnable_avg 明显高于 util_avg,说明有任务在排队。 任务在排队,可能说明 CPU 频率不够。 那就让 schedutil 更积极提频。

但是在 Android 上就有点不一样,Android 手机上常见的 CPU 调频路径一般可以简化成:

  • 任务运行/唤醒/迁移
  • 调度器更新 PELT 信号
  • schedutil 读取 CPU utilization
  • 计算目标频率
  • cpufreq 驱动切到对应 OPP

这里主要是 schedutil :每次调度器负载追踪更新时,比如任务唤醒、任务迁移、时间推进,都会调用 schedutil 去更新硬件 DVFS 状态

也就是说 schedutil 本质上是一个「调度器驱动的 CPU 调频 governor 」,它根据 CPU runqueue 上的 utilization 估算当前需要多少频率,利用率越高目标频率越高,所以 cpu_util() 里多加一个 boost,在 Android 上就不是一个无关紧要的小数值了,它会直接影响 CPU 频率选择

比如在当前主线代码里 sugov_get_util() 就会调用:

ini 复制代码
util += cpu_util_cfs_boost(sg_cpu->cpu);
util = effective_cpu_util(...);
util = max(util, boost);
sg_cpu->util = sugov_effective_cpu_perf(...);

这意味着 CFS 的 boost util 会进入 schedutil 的频率决策链路,一旦 cpu_util_cfs_boost() 给出的值偏高,CPU 就更容易被推到高频。

但是这套机制在过去的 Android 其实是有用的,因为当时 Android UI 的体验是经常「慢半拍」,早期 Android 的调度问题里:

  • 任务刚醒来时 PELT 还没反应过来
  • schedutil 觉得 CPU 不忙
  • CPU 频率还没拉起来
  • 结果 UI 线程 / RenderThread 错过帧预算

当年很多 Android 性能优化的思路都偏向「宁可早点提频,也不要掉帧」,JankBench 这类工具也正是在这种背景下被用来验证 UI 流畅度,它关注的是 Android Graphics Pipeline,也就是用户滑动、列表渲染、动画等场景下的 jank。

所以在当年看来, boost 逻辑有它的历史合理性,因为 PELT 慢、UI 负载短而急、schedutil 提频慢,所以用户看到卡顿,所以用 runnable_avg 提前补一脚油门。

但是问题在于,时代变了,这也是这次传音发现的结论:

  • CPU 频率确实更高了
  • 但性能没有明显更好
  • 功耗却明显上升

也就是现在 runnable_avg boost 场景在现在的 Android 手机上不成立,因为它的假设是:

复制代码
runnable_avg 高
  ↓
CPU contention 高
  ↓
CPU 频率不够
  ↓
提高 CPU 频率能改善体验

但是现在测试下来并不是,比如 runnable 多,不一定是 CPU 频率不够,任务排队可能是 CPU 忙,也可能是锁竞争、线程唤醒风暴、binder 调度、GPU 等待、内存带宽压力、thermal 限制,甚至是应用自身线程模型不合理。

这时候调度器看到的是有很多 runnable task ,但系统真正的问题场景可能是:

  • GPU 忙
  • 内存带宽不够
  • 某个锁被占着
  • RenderThread 等 SurfaceFlinger
  • 游戏主线程在等 GPU fence
  • 视频播放受解码和显示链路限制

所以如果瓶颈不在 CPU 频率,提高 CPU 频率其实并不会明显提升性能。

另外现在 Android 已经有更直接的 performance hint ,过去系统需要靠调度器猜,但 Android 后来已经逐步引入了更明确的机制,比如 ADPF(Android Dynamic Performance Framework),它支持游戏和性能敏感 App 更直接地与 Android 的功耗、温控和 CPU 管理系统交互,而不是让调度器只靠 runnable/util 盲猜 。

所以从这个角度看,runnable_avg boost 是一个比较粗的旧时代启发式规则。

最后现在厂商一般啊自己也有一堆调度和提频策略,毕竟 Android 手机不是裸 Linux,SoC 厂商、系统厂商、游戏模式、Power HAL、thermal governor、GPU governor 都可能参与性能决策。

所以如果厂商已经对前台 App、游戏线程、SurfaceFlinger、RenderThread 做了 hint 或 boost,Linux 调度器再根据 runnable_avg 来一层 boost,就很容易出现多层策略叠加后,导致"过度提频"。

传音这次看到的现象很像这种过度提频,CPU 更常待在高频,SoC 功耗显著增加,但是用户可见性能没有明显变化

所以这个其实不算是 Bug,而是当年的优化在现在成了负担,现在系统更成熟了,方案也多了,所以 boost 自然就时代的 Bug ,而且 Linux 主线希望机制通用,而 Android 设备希望整机体验和续航最优,这些年 Android 发展太快了,所以 Linux 层面没跟上也算正常。

所以代码不是一开始能跑,就代表了一直能跑,过去优化在现在也可能是负债。

链接

lore.kernel.org/lkml/202605...

相关推荐
kyriewen111 小时前
开源|Image Harvest v1.0.5:AI 智能标签 + Eagle 导出,设计师和开发者的图片工作流神器
前端·javascript·人工智能
Kapaseker1 小时前
一个圆屏逼得我好好学习 Compose MeasurePolicy
android·kotlin
步十人1 小时前
【Vue】认识单文件组件与模板语法
前端·javascript·vue.js
__Witheart__1 小时前
RK Android OTA U盘升级指南
android
__Witheart__1 小时前
RK Android OTA U盘升级包编译指南
android
AIFQuant1 小时前
贵金属投资 APP 开发:实时报价、图表、提醒与交易数据全链路
开发语言·前端·websocket·金融·web app
爱吃羊的老虎1 小时前
【JAVA】python转java:Spring Boot 如何处理 Web 请求
java·前端·spring boot·http
shuoshuohaohao1 小时前
《JavaScript》
开发语言·前端·javascript
步步为营DotNet1 小时前
洞悉.NET 11:ASP.NET Core 10 在构建实时协作 Web 应用的技术实践
前端·asp.net·.net