Linux | 关于CPU 调频的一些QA

从事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...

相关推荐
顾林海1 分钟前
深度解析HashMap工作原理
android·java·面试
normaling35 分钟前
十,软件包管理
linux
V少年1 小时前
深入浅出DiskLruCache原理
android
鱼洗竹1 小时前
协程的挂起与恢复
android
派阿喵搞电子1 小时前
在Ubuntu下交叉编译 Qt 应用程序(完整步骤)
linux·运维·ubuntu
知北游天1 小时前
Linux:基础IO---软硬链接&&动静态库前置知识
linux·运维·服务器
云途行者1 小时前
GitLab 17.x 在 Ubuntu 24.04 上安装配置
linux·ubuntu·gitlab
汤姆和杰瑞在瑞士吃糯米粑粑2 小时前
【操作系统学习篇-Linux】进程
linux·运维·学习
清风~徐~来2 小时前
【Linux】进程创建、进程终止、进程等待
android·linux·运维
binary思维2 小时前
LibreOffice与Microsoft Word对比分析
linux·word