【cpufreq】cpufreq调频策略演进

传统CPU调频策略

传统 CPU 调频模块主要分为 3 个模块块:CPUFreq 核心模块CPUFreq 驱动和 CPUFreq Governor。核心模块主要是一些公共的逻辑和 API,CPUFreq 驱动是处理和平台相关的逻辑,比如设置 CPU 的频率和电压。而 Governor 就是我们今天要讲的主角,CPU 调频的策略。CPU 在什么样负载,什么样的场景下应该跑多少频率,都是通过 CPUFreq Governor 采取一定策略来决定的,然后调用 cpufreq_driver->target() 来设置要调整的频率。

那么传统 CPUFreq Governor 是如何选择当前 CPU 的频率的呢?performance 和 powersave 这两个 governor 就不说了,一个是让 CPU 一直跑在最高频率,另外一个是让 CPU 跑在最低频率,所有的动作都在初始化的时候做了,本身也没有什么策略。userspace 只是实现了 scaling_setspeed 节点,主要策略在用户态,也没什么可讲的。而 ondemand 和 conservation 两个 governor 则是开启一个 timer,定期去计算各个 CPU 的负载。当 CPU 负载超过 80% 时,ondemand 就会把 CPU 频率调到最高,其他情况则会根据当前负载按比例计算频率。而对于 conservation 而言,CPU 负载超过 80% 时,默认会以 5% 的步伐递增;当 CPU 负载少于 20% 的时候,默认会以 5% 的步伐递减

交互式调频策略

Interactive governor 并没有合入到 mainline,它是在 Android 中引入的。现在几乎所有的 Android 手机用的都在用这个 governor。所不同的是,它在每一个 CPU 上都注册了一个 idle notifier。当 CPU 退出 idle 状态时,interactive 就会缩减采样频率,从而可以快速响应负载变化。其他情况下,会根据当前 CPU 负载调整频率,这一点和 ondemand 类似

总结起来,对于像 ondemand,conservation,interactive 含有调频逻辑的 governor,都包含一个共同的部分 - 负载采样,需要每隔一定时间就计算一次 CPU 负载。而这个共同点,就是今天这篇文章的关键。有些人认为,对于 CPU 的负载,没有谁比调度器还清楚的了。所以 cpufreq governor 完全没必要自己去做负载采样,应该从内核调度器那里获取。而基于调度器的 cpufreq governor 就是这样引出来的。

基于调度器的调频策略

ARM公司在推出Cortex-A15之后市场反馈功耗有点过大,于是提出了大小核的概念即big.LITTLE模型,该模型主要目的是为了省电。目前大部分旗舰手机基本上都才有big.LITTLE模型,比较经典的配置是Cortex-A72+Cortex-A53,Cortex-A72是大核,Cortex-A53是小核。用通俗的话来概况big.LITTLE模型的话就是用大核干重活,用小核来干轻活。big.LITTLE模型在计算机术语上称为HMP(Heterogeneous Multi-Processing)。目前的Linux内核实现的CPU负载均衡算法是基于SMP模型的,并没有考虑到big.LITTLE模型,因此Linaro组织对big.LITTLE模型开发了全新的负载均衡调度器,称为HMP调度器。

Linux内核的CFS调度器和SMP负载均衡的缺点:

  1. 主要是为了服务器性能优先场景而考虑的,它们希望把任务都平均分配到系统所有可用的CPU上,最大限度地提高系统的吞吐量,这是主要为服务器设计的,没有考虑到系统的耗电问题,显然这不适合对功耗敏感的手机或者消费电子等设备中。
  2. 主要针对SMP系统,对于非SMP系统支持不足,比如说arm.big.little架构。
  3. 没有充分利用各个核的功耗,性能,频率差异来达到性能和功耗的最优平衡。

HMP模型

最早,针对arm big.litthle,高通提出了HMP调度器,直接在大小cluster上全部切换。

调度器演进

轮转-》完全公平CFS-》HMP-》EAS

 那么EAS如何工作?为什么仅在特定条件下如此有效?

功耗感知调度(EAS)需要使用功耗模型,就像上面提到的,EAS需要大量测试工作才能使其完美。EAS试图统一内核的三个不同核心部分,它们各自独立发挥作用,而功耗模型则有助于统一它们。

Linux调度程序(CFS,如上所述)

Linux cpuidle

Linux cpufreq

将调度器下的三个部分统一并且一起计算可以降低功耗,因为一起计算可以使它们尽可能高效。CPUIdle尝试确定CPU何时应进入空闲模式,而CPUFreq尝试确定何时升高或降低CPU频率。这两个模块的主要目标都是节能。不仅如此,它还将进程分为四个cgroup,即top-app,system-background,foreground和background。将要处理的任务放入这些类别之一,然后为该类别分配CPU资源,并在不同的CPU核上委派工作。top-app要求最优先地被完成,其次是foreground,background,然后是system-background。从技术上讲,background与system-background具有相同的优先级,但是system-background通常也可以访问更多的小核。实际上,EAS正在将Linux内核的核心部分整合到一个进程中。

唤醒设备时,EAS将选择处于最浅的空闲状态的核,从而将唤醒设备所需的功耗降至最低。这有助于减少使用设备时所需的功率,因为如果不需要,它不会唤醒大簇(big cluster)【big cluster的概念见下图】。负载跟踪也是EAS极其重要的一部分,有两种选择。“每实体负载跟踪”(Per-Entity Load Tracking PELT)通常用于负载跟踪,然后该信息用于确定频率以及如何在CPU上委派任务。也可以使用“窗口辅助的负载跟踪”(Window-Assisted Load Tracking WALT),它是Google Pixel上使用的。我们论坛上的许多EAS ROM(例如VertexOS)都选择使用WALT。许多ROM会发行WALT和PELT两个版本的内核,因此由用户决定。WALT更具突发性,CPU频率峰值很高,而PELT试图让频率保持连贯性。负载跟踪器实际上并不影响CPU频率,它只是告诉系统CPU使用率是多少。较高的CPU使用率需要较高的频率,因此PELT的连贯性意味着它将缓慢地调高或调低CPU频率。PELT确实会趋向于更高的CPU负载报告,因此它可能会以更高的耗s电来提供更高的性能。但是,由于两种负载跟踪方法都在不断地修补和完善,因此目前尚无法真正说出哪种负载跟踪系统更好。

基于调度器的 CPU 调频策略

内核调度器中的 CFS 调度类是通过 PELT(per entity load tracking) 来统计各个 Task 的负载(capacity),并映射到 0 ~ 1024(最大值可在编译时指定)。内核当中的负载均衡就是通过这些统计值来平衡各个 CPU 之间的任务。而基于调度器的 cpufreq governor 的主要原理就是把各个 CPU 的 capacity 映射到 CPU 频率,来完成调频动作,capacity 越高,当前 CPU 负载越高,所以频率也调的很高。

而当前内核社区中,已经有两个成形的方案。一个是 ARM 和 Linaro 主导的项目 - cpufreq_sched,属于 EAS 的一部分。而另外一个 Intel 主导的项目 - schedutil。

cpufreq_sched

cpufreq_sched本身逻辑比较简单,当 cfs, rt, deadline 3 个调度类中的 capacity 出现变化的时候,就调用 update_cpu_capacity_request() 来更新当前 policy 下 CPU 的频率。cpufreq 中的 policy 有可能包含多个 CPU,所以这里要选择其中最大的 capacity 来代表整个 policy 的负载。capacity 到 CPU 频率,是通过如下代码按比例转换的:

freq_new = capacity * policy->max >> SCHED_CAPACITY_SHIFT

SCHED_CAPACITY_SHIFT 一般是 10,即 capacity 的最大值 1024。假定当前 policy 允许的最大 CPU 频率是 1.2GHz,capacity 为 500,那么对应的频率是 586Mhz。如果我们直接把 CPU 设置在这个频率上,会导致一些性能上的下降。所以 cpufreq_sched 会在最终的 capacity 基础上,乘上 1.25,相当于在当前 capacity 的基础上增加 20%。

从 cpufreq_sched 的实现,我们可以看到整个调频动作,都是从调度器中直接设置下来的,cpufreq_sched 自身并没有去统计各个 CPU 的负载。而这种做法也让 CPU 的频率可以快速的响应负载变化,理论上讲,当前平台的 cpufreq 驱动最小调频间隔是多少,那么 cpufreq_sched 就可以做到多少。相比于 interactive 20ms 的调频间隔,cpufreq_sched 不到 1ms 的调频间隔简直是天壤之别。下图分别是 interactive 和 sched 在不同负载下 CPU 频率图:

  • Interactive:InteractiveInteractive
  • Cpufreq_sched:SchedSched

响应速度快,调频间隔短,固然是 cpufreq_sched 的优势,但是把整个调频动作都放到调度器里做,无疑会增加调度器的负担。调度器代码路径变长,也会增加调度器的延时。如果某个平台的 cpufreq 驱动在设置 CPU 频率的时候会导致系统睡眠,那么 cpufreq_sched 还需要在每一个 CPU 上额外开启一个线程,防止对调度器造成影响。

schedutil

在介绍 schedutil 之前,我们首先得介绍一个内核社区最近出现的新机制 - utilization update callback。其实就是一个各个 CPU 使用率变化时的一种回调机制。通过 cpufreq_set_update_util_data() 来注册回调函数,当 cfs, rt, deadline 3 个调度类的 capacity 出现变化时,调用 cpufreq_update_util() 来触发 hook,实现类似 notifier 的效果。

而 schedutil就是利用这个负载变化回调机制,通过 cpufreq_add_update_util_hook() 注册回调函数,当 CPU 负载出现变化的时候,就会触发 schedutil sugov_update 进行调频动作。而剩下的调频实现,其实跟 cpufreq_sched 大同小异。

目前来看,cpufreq_sched 好像已经被放弃,而 schedutil 有望在 Linux kenrel 4.7 版本中合入,到时候,内核 Cpufreq Governor 又要新添一名成员了。

posted @ 2022-09-26 11:40  zephyr~  阅读(1021)  评论(0编辑  收藏  举报