从事Android 开发过程中,我们其实经常会遇到关于cpu 频率的疑问
- 为什么这段时间 CPU 频率突然飙高?
- 为什么有时任务持续 running,但频率却不升?
- 为什么有些 task 刚唤醒就能直接拉满频率?
- 为什么有时一些计算密集型任务没有调度到大核?
- 为什么有时cluster 空闲频率还没掉?
........
本文先整理一些关于调频的QA,理解有限难免可能会有偏差的地方,还希望多指正,后续有空再基于Code 详细讨论
Q: 调频的颗粒度是cluster 吗?
A:
手机 CPU 被划分为多个 cluster,每个 cluster 用一个独立的调频 policy 控制频率。
shell 下可以看到类似如下节点:
js
policy0/
policy4/
policy7/
每个policy 下有一些参数如scaling_min_freq 等
Q: 一般什么事件会触发sugov 更新?
A:
(摘自www.wowotech.net/process_man...)
sugov是调度事件(进程切换、入队、出队、tick等)驱动调频的,因此调频会更及时。具体驱动调频的时机包括:
(1)实时线程(rt或者deadline)的入队出队
(2)Cpu上的cfs util发生变化
(3)处于Iowait的任务被唤醒
sugov会注册一个callback函数(sugov_update_shared/sugov_update_single)到调度器负载跟踪模块,当CPU util发生变化的时候就会调用该callback函数,检查一下当前CPU频率是否和当前的CPU util匹配,如果不匹配,那么就进行提频或者降频。
Q: 怎么计算cpu 的util?
A:
上面提到sugov 在调度事件触发后会回调sugov_update_shared。
回调触发后的大致调用链
rust
sugov_next_freq_shared -> sugov_get_util -> schedutil_cpu_util -> uclamp_rq_util_with
各家同样一般会客制化,做文章一般是在schedutil_cpu_util 上,比如mtk 上hook 后名字是mtk_cpu_util,主要作用是计算一个CPU 的util,供频率调节 过程大致是:
- 获取基础 util(CFS + RT)
- 判断是否需要 boost、uclamp 调整
- 判断 deadline 带宽(DL)是否要加上
- 修正 IRQ 时间带来的误差
- 得出一个最终的 util 值,用来决定频率
注:各家hook 这个接口,可以配合上层做一些feature,比如mtk 上SBB boost,在这个接口中就加入了
c
if (sbb_trigger)
util = util * sbb_data->boost_factor;
相当于让这个task "变大"
最终是通过uclamp_rq_util_with 中调用的 clamp(util, min_util, max_util)
做最后实际真正的钳制
Q: 怎么算出cluster的util ?
A:
以原生(sugov_next_freq_shared) 为例
C
for_each_cpu(j, policy->cpus) {
j_util = sugov_get_util(j_sg_cpu);
j_max = j_sg_cpu->max;
if (j_util * max > j_max * util) {
util = j_util;
max = j_max;
}
}
找出一个 cluster 里"最忙的 CPU",然后用它的负载值(utility)来决定整个 cluster 的频率。
Q: util 是如何映射到freq 的
A:
核心函数 map_util_freq()
是原生调频逻辑的关键,各家厂商通常会通过 hook 做定制。
原生实现中,调度器会在一个 cluster 内找出 util 最大的 CPU ,以此计算目标频率 next_freq
:
next_freq = C × max_freq × util / max
C = 1.25:是个"保险系数",表示预留 20% 的性能空间(不是刚好够用,而是多一点,避免频率刚刚好不够用),但最终的频率还是要匹配到底层硬件支持的档位(比如 1.2GHz、1.4GHz 这种),由驱动(cpufreq_driver_resolve_freq)挑一个最近的设定上去。
Q:PELT 半衰期指的是什么?越短意味着什么?
A:
PELT 半衰期决定了系统感知负载变化的速度。越短表示系统对负载上升反应更快,更容易拉高频率、调度到大核,响应更快,但能耗也更高。
具体半衰期多少,可以查看sched_pelt_multiplier 参数 cat proc/sys/kernel/sched_pelt_multiplier
比如我手头的机器上打印出来是4,这说明衰减速度是4倍速,原生默认是32ms,x4 意味着半衰期是8ms。
那么这个数值越高越好吗?
不一定,前提一般不会小于tick 周期,越短也越容易发生cluster 迁移带来cache miss,也更容易受最近noise 影响,总之有利有弊,一般不去动这个数值。
Q: 为什么Android上会经常使用uclamp 钳制task?
A:
boosting: better interactive response for small tasks which are affecting the user experience.
capping: increase energy efficiency for background tasks not directly affecting the user experience.
uclamp 允许用户空间限制任务负载范围,解决PELT 调度器对任务轻重判断不符合人们"预期"的问题,比如像UI 相关线程,有时候PELT 算出来的util 很小,频率自然也会很低。还有像一些后台对用户不重要但是比较忙的task,限制其uclamp max。
比如垫个底的话,给到sugov 的util 就是垫过的,频率不至于会落得很低,主要是为了应对突然"变重"的情况,因为PELT 的提升util 总是滞后的。
不过这种写死的做法并不是最佳实践,可移植性将是一个问题。 比如在 100、200 或 1024 时可以完成多少工作,对于不同平台都是不同的。
基于数据反馈动态调节uclamp 或许更好,厂商一般都有自己的实现如MTK 的Fpsgo。比如游戏场景下可以使用 util 钳制与其感知的实际每秒帧数(FPS)形成反馈回路,动态钳制uclamp min,以确保不会丢帧。
Q: Util Clamp 如何设置?
A:
三种方式:Per-task、Cgroup、System 全局 以最常用的Per-task 设置为例,上层通过 sched_setattr
或kernel 中使用sched_setattr_nocheck
。
两个可调参数:
- UCLAMP_MIN,设置下限。
- UCLAMP_MAX,设置上限。
可以这样理解,无论一些所谓的"boost" 策略多么复杂,或许你会看的晕头转向,但最终你会看到这个熟悉的身影:sched_setattr 或sched_setattr_nocheck,因为也没别的接口可选。
举个源码中的使用例子 frameworks/native/services/surfaceflinger/SurfaceFlinger.cpp
C++
status_t SurfaceFlinger::setSchedAttr(bool enabled) {
static const unsigned int kUclampMin =
base::GetUintProperty<unsigned int>("ro.surface_flinger.uclamp.min"s, 0U);
if (!kUclampMin) {
// uclamp.min set to 0 (default), skip setting
return NO_ERROR;
}
sched_attr attr = {};
attr.size = sizeof(attr);
attr.sched_flags = (SCHED_FLAG_KEEP_ALL | SCHED_FLAG_UTIL_CLAMP);
attr.sched_util_min = enabled ? kUclampMin : 0;
attr.sched_util_max = 1024;
if (syscall(__NR_sched_setattr, 0, &attr, 0)) {
return -errno;
}
return NO_ERROR;
}
Q: 什么时候会发生钳制?
A:
既然已经知道通过什么接口钳制了,那么紧接着问题是:什么时候钳制?
仅在需要时才会发生钳制,例如:当任务唤醒并且调度器需要选择合适的CPU 以在其上运行时。
选核时用到的util 已经是clamp 后的,所以会影响选核流程。
比如钳制task uclamp max 到较低数值,调度器选核时会将其看做"小任务",从而更容易选到小核上。
Q: 多个task 挂在一个rq 上,rq uclamp 如何计算?
A: 摘取内核文档中的例子:
js
p0->uclamp[UCLAMP_MIN] = 300
p0->uclamp[UCLAMP_MAX] = 900
p1->uclamp[UCLAMP_MIN] = 500
p1->uclamp[UCLAMP_MAX] = 500
假设p0和p1都入队到同一个rq,UCLAMP_MIN和UCLAMP_MAX都会变为
js
rq->uclamp[UCLAMP_MIN] = max(300, 500) = 500
rq->uclamp[UCLAMP_MAX] = max(900, 500) = 900
最大聚合策略一个明显的局限性: 比如我们期望p1 max 不要太高(省电),但是受p0 max 影响,最终rq max 并未满足p1 max 的预期。
注:不光会受其它task uclamp 影响,还会受Cgroup、System 全局设置范围限制,不过本文重点讨论task 级别的uclamp。
Q: 为什么有些task 刚唤醒运行就能直接将频率拉的很高?
A:
这种很常见,特别是一些比较重的密集型task,睡了一会后再唤醒时能够直接将频率拉满 用的是预估负载util_est.enqueued,可以看到用到了PELT 算出来的util_avg。
util_est.enqueued = max(util_avg, util_est.ewma);
这是调频(sugov)真正会使用的估算值。
为何不用util_avg?
util_avg 其实更多用于选核上,util_est 更能反应该task 最近实际util,特别是最近突增的情况,响应更快。
Q: 为什么钳制task util 还会影响选核?
A:
选核时判断该task util 是否fit 这个cpu,如果不fit 的话,就会跳过这个cpu,通常是和该cpu 当前实际算力做比较。比如钳制了clamp max,那么在调度器眼中的util 就是缩小后的,也就更容易选小核上(通常更节能,也不绝对)。
还有一种情况,这个task 正running 中被钳制了min 比如垫高到当前cpu 算力无法满足了,那么此时会被标记为"misfit", 进而触发主动负载均衡迁移,迁移到更高算力的cpu 上,不过这种情况只是我从理论上分析得出的结论。
Q: Cgroup 限定的使用率是如何转换为util 的?
A:
js
req.util = req.percent << SCHED_CAPACITY_SHIFT;
req.util = DIV_ROUND_CLOSEST_ULL(req.util, UCLAMP_PERCENT_SCALE);
这里SCHED_CAPACITY_SHIFT 是1024,目的是归一到1024 单位上去,这种转换思路在一些厂商的客制化feature 中也可以看到,比如mtk Fpsgo 的perf idx (0~100) 也是会换算到1024 单位上去。
Q: 如何查看某个线程是否被钳制uclamp?
A:
方法有多种,可以命令查询或Ftrace(打开sched 相关event)都可以看出 比如查询节点方式
bash
proc/<pid>/task/<tid>/sched
比如cat /proc/12345/task/12345/sched | grep uclamp
参考文献:
lwn.net/Articles/76...