Linux Cpufreq框架
cpufreq动态调频
cpufreq概述
Linux Kernel主要通过三类机制来实现SMP(Symmetric Multiprocessing,对称多核)系统CPU core的电源管理:
- cpu hotplug: 根据应用场景来up/down CPU
- cpuidle framework: 当cpu上没有可执行任务时,就会进入空闲状态
- cpufreq framework: 根据使用场景和系统负荷来调整CPU的电压和频率
cpufreq framework的核心功能,是通过调整CPU core的电压或频率,兼顾系统的性能和功耗。在不需要高性能时,降低电压或频率,以降低功耗;在需要高性能时,提高电压或频率,以提高性能。
cpufreq framework中的几个重要概念:
- policy(策略):同一个power domain CPU动态调频策略,包含了当前使用的governor和cpufreq driver
- governor(调节器):决定如何计算合适的频率或电压
- cpufreq driver(调频驱动):实现真正的调频执行工作(与平台相关)
除此之外,cpufreq还包含cpufreq stats, cpufreq qos, cpufreq notifier等辅助模块,其主要功能如下:
- cpufreq stats:用于搜集cpufreq的一些统计数据,如CPU在每个频点下的运行时间,总的频率切换次数等
- cpufreq qos:用于cpufreq频率限制值发生改变时,向cpufreq模块发送一个通知,将频率限制值调整到新的值
- cpufreq notifer:对CPU频率切换或policy对应governor发生改变感兴趣的模块,可以向cpufreq注册一个通知,当以上事件发生时,cpufreq将会向其发送相关通知
常用的governor类型
- Performance:总是将CPU置于最高性能的状态,即硬件所支持的最高频率、电压
- Powersaving:总是将CPU置于最节能的状态,即硬件所支持的最低频率、电压
- Ondemand:设置CPU负载的阈值T,当负载低于T时,调节至一个刚好能够满足当前负载需求的最低频/最低压;当负载高于T时,立即提升到最高性能状态
- Conservative:跟Ondemand策略类似,设置CPU负载的阈值T,当负载低于T时,调节至一个刚好能够满足当前负载需求的最低频/最低压;但当负载高于T时,不是立即设置为最高性能状态,而是逐级升高主频/电压
- Userspace:将控制接口通过sysfs开放给用户,由用户进行自定义策略
- Schedutil:这是从Linux-4.7版本开始才引入的策略,其原理是根据调度器所提供的CPU利用率信息进行电压/频率调节,EAS使用schedutil进行调频
sysfs用户层接口,目录位于/sys/devices/system/cpu/cpufreq/policy
| 名称 | 说明 |
|---|---|
| cpuinfo_max_freq | 硬件所支持的最高频率 |
| cpuinfo_min_freq | 硬件所支持的最低频率 |
| affected_cpus | 该policy影响到哪些cpu(没显示offline状态的cpu) |
| related_cpus | 该policy影响到的所有cpu,包括offline状态的cpu |
| scaling_max_freq | 该policy支持调整的最高频率 |
| scaling_min_freq | 该policy支持调整的最低频率 |
| scaling_cur_freq | policy当前设置的频率 |
| scaling_available_governors | 当前系统支持的governor |
| scaling_available_frequencies | 支持的调频频率 |
| scaling_driver | 当前使用的调频驱动 |
| scaling_governor | 当前使用的governor |
| scaling_setspeed | 在userspace模式下才能使用,手动设置频率 |
cpufreq软件架构

cpufreq core(可以理解为对policy的操作):把一些公共的逻辑和接口代码抽象出来
cpufreq作为所有cpu设备的一个功能,注册到了cpu_subsys总线上- 对上以sysfs的形式向用户空间提供统一的接口,以notifier的形式向其他driver提供频率变化的通知
- 对下提供CPU频率和电压控制的驱动框架,方便底层driver的开发,同时提供governor框架,用于实现不同的频率调整机制
- 内部封装各种逻辑,主要围绕
struct cpufreq_policystruct cpufreq_driverstruct cpufreq_governor三个数据结构进行
kernel使用struct cpufreq_policy用来抽象cpufreq,它代表了一个CPU簇的cpufreq的属性
cpufreq_policy结构体
1 | struct cpufreq_cpuinfo { |
driver/cpufreq/cpufreq.c中定义了一个全局的percpu变量
1 | static DEFINE_PER_CPU(struct cpufreq_policy *, cpufreq_cpu_data); |
这里对应E2000 sysfs中3个policy文件夹,两个小核使用1个policy,另外两个大核分别对应1个policy
cpufreq初始化
内核配置
在kconfig中(CPU Power Management -> CPU Frequency scaling)可以对cpufreq进行配置,可以配置支持的governor及系统默认的governor,以及cpufreq调频driver,例如Phytium E2000 5.10内核的配置如下,默认使用schedutil governor,根据调度器所提供的CPU利用率信息进行电压/频率调节,EAS能源感知依赖该governor工作:
OPP表初始化
OPP表的定义:域中每个设备支持的电压和频率的离散元组的集合称为Operating Performance Points(OPP),内核设备树opp文档Documentation/devicetree/bindings/opp/opp.txt
假设一个CPU支持如下的电压和频率关系:
{300MHz at minimum voltage of 1V}
{800MHz at minimum voltage of 1.2V}
{1GHz at minimum voltage of 1.3V}
用OPP表示就可以用{Hz, uV}方式表示如下:
{300000000, 1000000}
{800000000, 1200000}
{1000000000, 1300000}
Linux内核使用opp layer库来管理opp table,具体的结构如下:
Linux内核使用struct dev_pm_opp结构表示设备的一OPP
1 | // drivers/opp/opp.h |
Linux内核opp layer库的结构如下
这里初始化的就是各个性能域(即不同cluster)的OPP表,在E2000平台中是通过SCMI的Performace domain management protocol协议获取PERFORMANCE_DESCRIBE_LEVELS这个参数表,具体的协议实现源码在drivers/firmware/arm_scmi/perf.c里面,perf.c实现了SCMI的Performance domain managment protocol,scmi cpufreq_drvier也是通过perf_ops函数集进行调频
1 | // include/linux/scmi_protocol.h |
在初始化阶段,scmi_perf_protocol_init只会将固件里面的perf domains信息保存到handle->perf_priv里面,此时还并没有将opp表注册到cpu设备上
接下来在scmi调频驱动初始化的过程中,会调用scmi的device_opps_add()接口初始化,即调用scmi_dvfs_device_opps_add(),在这个里面才会生成cpu的opp_table
1 | static int scmi_dvfs_device_opps_add(const struct scmi_handle *handle, |
详细看一下dev_pm_opp_add()的过程
1 | // drviers/opp/opp.h |
最终获取得到的OPP表如下
cpufreq初始化过程
cpufreq被注册cpu_subsys总线上
cpufreq的初始化从cpufreq_drvier注册开始,cpufreq_register_driver()函数为cpufreq驱动注册的入口,驱动程序通过调用该函数进行初始化,传入相关的struct cpufreq_driver,cpufreq_register_driver()会调用subsys_interface_register()最终执行回调函数cpufreq_add_dev,然后调用cpufreq_online()走初始化流程
1 | // cpufreq_drvier结构体 |
来看一下subsys_interface_register()
1 | // drivers/base/bus.c |
再来看看cpufreq_online()
1 | static int cpufreq_online(unsigned int cpu) |
cpufreq drviver初始化
在cpufreq_online()中调用全局变量cpufreq_driver->init(policy)进行调频驱动的初始化,下面是scmi调频驱动的初始化过程
1 | static int scmi_cpufreq_init(struct cpufreq_policy *policy) |
频率表初始化过程
1 | struct cpufreq_frequency_table { |
governor初始化过程
cpufreq governor的初始化过程,在cpufreq_init_policy(policy)中进行,这里以ondemand为例进行分析
1 | // include/linux/cpufreq.h |
启动governor中比较重要的是设置调频回调函数,该函数是真正调频时计算合适频率的函数
ondemand调节器
ondemand调节器也会根据当前的CPU负载来进行CPU频率计算,ondemand工作过程如下
1 | // include/linux/sched/cpufreq.h |
schedutil调节器

sugov(schedutil governor)作为一种内核调频策略模块,它主要是根据当前CPU的利用率进行调频。因此,sugov会注册一个callback函数(sugov_update_shared/sugov_update_single)到调度器负载跟踪模块,当CPU util发生变化的时候就会调用该callback函数,检查一下当前CPU频率是否和当前的CPU util匹配,如果不匹配,那么就进行升频或者降频。
sugov_tunables结构体,用来描述sugov的可调参数
1 | // sugov_tunables结构体 |
sugov_policy结构体,sugov为每个cluster构建了该数据结构,记录每个cluster的调频数据信息
1 | // sugov_policy结构体,为每个簇构建了该数据结构,记录每个簇的调频数据信息 |
sugov_cpu结构体,sugov为每个cpu构建了该数据结构,记录每个CPU的调频数据信息
1 | struct sugov_cpu { |
sugov初始化过程和ondemand初始化过程相似,当内核设定默认governor为sugov时,在cpufreq_init_governor(policy);中会调用sugov_init()初始化sugov,然后调用sugov_start()设置调频回调函数,每当CPU利用率发生变化的时候,调度器都会调用cpufreq_update_util()通知sugov,在cpufreq_update_util()被调用时,即任务调度后CPU当前的util发生变化,会调用sugov的回调函数进行调频,sugov_update_shared()当一个簇中有多个CPU调用该回调,遍历簇上的CPU找到当前最大util的CPU,然后根据该util映射到频率;sugov_update_single()即一个簇上单个CPU的情况直接根据该CPU util计算频率
调度事件的发生还是非常密集的,特别是在重载的情况下,很多任务可能执行若干个us就切换出去了。如果每次都计算CPU util看看是否需要调整频率,那么本身sugov就给系统带来较重的负荷,因此并非每次调频时机都会真正执行调频检查,sugov设置了一个最小调频间隔,小于这个间隔的调频请求会被过滤掉。
schedutil频率计算过程
1 | // sugov_start会遍历该sugov policy(cluster)中的所有cpu |
EAS能源感知调度
EAS整体框架

完全公平调度(Completely Fair Scheduler CFS)实现了面向吞吐量的的任务调度策略,EAS为这个调度器添加了一个基于能耗的调度策略,在优化CPU算力冗余的同时实现了节能,EAS在系统中、低度负载情况下工作,CFS在系统满负载情况下工作。
EAS在CPU调度领域,在为任务选核是起作用,目的是保证性能的情况下尽可能节省功耗,EAS涉及内核的几个子系统(任务调度、能源管理、CPU动态调频),EAS代码主要位于kernel/sched/fair.c,能源感知的任务调度需要调度器评估各个任务在CPU上运行带来的能耗影响
EAS全局控制开关/proc/sys/kernel/sched_energy_aware
CPU算力归一化过程
当前,Linux无法凭自身算出CPU算力,因此必须要有把这个信息传递给Linux的方式,它是从capacity-dmips-mhz CPU 设备树binding中衍生计算出来的
归一化CPU capacity,topology_normalize_cpu_scale()定义在drivers/base/arch_topology.c,这个capacity在schedutil调度中被sugov_get_util()函数读取
topology_normalize_cpu_scale()在CPU初始化parse_dt_topology()中被调用,capacity归一化的前提条件是需要在设备树中CPU节点设置capacity-dmips-mhz属性,该属性表示不同CPU的计算能力,内核读取该属性设置CPU的raw_capacity为capacity-dmips-mhz,参考内核文档Documentation/devicetree/bindings/arm/cpu-capacity.txt
ARM推荐的测试CPU的性能工具:Dhrystone 2.1以上版本,可以通过单核跑分成绩作为
capacity-dmips-mhz属性的参考,DMIPS: Dhrystone Million Instructions executed Per Second,表示了在Dhrystone这样一种测试方法下的MIPS,Dhrystone是一种整数运算测试程序。MIPS/MHz,就是说每MHz频率能产生多大的MIPS,CPU性能通常由每秒百万指令(Millions of Instructions Per Second,MIPS)表示,设备树里表示为dmips/mhz
CPU算力归一化公式,并不是简单的将capacity-dmips-mhz归一化到capacity,CPU的频率也参与到了计算中
capacity = (own(capacity-dmips-mhz) * own(max_freq)) / (max(capacity-dmips-mhz) * max(max_freq)) * 1024
根据测试部测试的E2000QCPU单核性能数据,E2000Q的capacity-dmips-mhz属性值可以设置为如下,放大1000倍:
1 | // 小核 |
实际经过CPU算力归一化到1024之后,对应的小核CPU算力为386,大核为1024
CPPC 调频驱动下的CPU算力归一化
在topology_init_cpu_capacity_cppc()中取得原始的CPU capacity数据,即 highest_perf,这个值是由固件描述的,之后还需要归一化到1024
1 | // arch/arm64/include/asm/topology.h |
查看 D3000M 的 highest_perf 值,可以看到固件那边并未设置为实际的 CPU 算力:
1 | root@Ubuntu:/sys/devices/system/cpu/cpu7/acpi_cppc# cat highest_perf |
CPPC 调频驱动下的 perf domian 建立过程
perf domain 的 debug fs 路径: /sys/kernel/debug/energy_model/
D3000M perf domain 打印
1 | [ 6.635761] freq: 400000, power: 7, cost: 7 |
EAS代码相关结构体
perf_domain结构表示一个CPU性能域,perf_domain和cpufreq_policy是一一对应的,性能域之间形成链,链表头存放在root_domain中
1 | // kernel/sched/sched.h |
perf_domain初始化
start_kernel() -> sched_init() -> init_defrootdomain()
1 | // kernel/sched/core.c |
E2000Q 5.10内核,perf_domain_debug 打印信息
1 | [ 2.574534] root_domain 0-3: pd3:{ cpus=3 nr_pstate=4 } |
root_domain的overload和overutilized说明:
- 对于一个 CPU 而言,其处于 overload 状态则说明其 rq 上有大于等于2个任务
- 对于一个 CPU 而言,其处于 overutilized 状态说明该 cpu 的 utility 超过其 capacity(缺省预留20%的算力,另外,这里的 capacity 是用于cfs任务的算力)
- 对于 root domain,overload 表示至少有一个 cpu 处于 overload 状态。overutilized 表示至少有一个 cpu 处于 overutilized 状态
- overutilized 状态非常重要,它决定了调度器是否启用EAS,只有在系统没有 overutilized 的情况下EAS才会生效。overload和newidle balance的频次控制相关,当系统在overload的情况下,newidle balance才会启动进行均衡。
EAS能量计算方法
CPU在某个performance state(ps)下的计算能力:
ps->cap = ps->freq * scale_cpu / cpu_max_freq (1)
CPU在该频点performace state(ps)下的能量消耗:
cpu_nrg = ps->power * cpu_util / ps->cap (2)
结合(1) (2)可以得出CPU在该ps下的能量消耗
cpu_nrg = ps->power * cpu_max_freq * cpu_util / ps->freq * scale_cpu (3)
其中 ps->power * cpu_max_freq / ps->freq 是一个固定数据存放在频点表的cost成员中
一个pd内的CPU,拥有相同的cost,所以一个pd内所有CPU的能量消耗可以表示为
pd_nrg = ps->cost * sum(cpu_util) / scale_cpu
EAS的调度过程
在任务被重新唤醒或者fork新建时,会通过select_task_rq_fair()将任务进行balance,达到充分利用CPU的目的。在select_task_rq_fair(),若任务是被重新唤醒就会调用find_energy_efficient_cpu()进行选核执行
1 | /* |
EAS Mainline
https://git.gitlab.arm.com/linux-arm/linux-power.git
计算 capacity-dmips-mhz
In order to calculate the right capacity-dmips-mhz, the following
test was performed:
- CPUFREQ governor was set to ‘performance’ on both clusters
- Ran dhrystone with 500000000 iterations for 10 times on each cluster
- Calculated the mean result for each cluster
- Calculated DMIPS/MHz: dmips_mhz = dmips_per_second / cpu_mhz
- Scaled results to 1024:
result_c0 = dmips_mhz_c0 / dmips_mhz_c1 * 1024
- CPU调频策略设置为 Performance
D3000M EAS初次测试
测试环境
- 开发板:D3000M TestA 板
- 固件版本:BL31: v2.3(release):D3000M-v0.70-p1-8-gaf62092
- 内核版本:linux v6.6
测试场景
- 空闲场景(idle):即待机场景
- 压力测试场景(stress):
stress-ng --cpu <cpu_num>,使所有CPU占用为100% - 模拟CPU占用场景(load):
lookbusy -c xx,模拟所有CPU占用xx百分比
测试方法
测试分为了三组,分别在上面的测试场景中测量处理器核心平均功耗:
- no eas: 未开启 EAS 功能
- eas: 开启 EAS 功能
- eas modify:将两组cluster中的大小核的 Efficiency Class 设置成一样,小核的Efficiency Class参数为1,大核的Efficiency Class参数为2,开启EAS
通过
/proc/sys/kernel/sched_energy_aware参数来开启和关闭 EAS 功能进行对比测试。
在对应的测试场景下,使用另一块开发板每隔50ms,通过检流模块分别测量 VDD_BIG1(第二组大核供电)、VDD_BIG0(第一组大核供电)、VDD_M0(第一组小核供电)、VDD_M1(第二组小核供电)的电流,并计算核心瞬时总功耗(并不代表处理器的总功耗,处理器还有其它几路电压,比如VDD_GPU, VDD_DMU未进行测量),每个测试场景测量 10 分钟,将数据进行保存,计算平均功耗。
测试结果

- 可能是功耗模型不准确,导致开启 EAS 后的功耗,在各个测试场景下会略高于未开启EAS的功耗
- 修改两组大小核的Efficiency Class为一致后,可以看到功耗会略微提高,印证了之前OS黄少波的说法
测试数据
测试代码