设备能量模型¶
1. 概述¶
能量模型 (EM) 框架充当驱动程序(了解设备在各种性能级别下的功耗)与内核子系统(希望使用该信息做出节能决策)之间的接口。
关于设备功耗的信息来源可能因平台而异。在某些情况下,可以使用设备树数据来估计这些功耗。在其他情况下,固件会更了解。或者,用户空间可能是最佳选择。依此类推。为了避免每个客户端子系统都重新实现对每个可能的信息来源的支持,EM 框架作为一个抽象层进行干预,该抽象层标准化内核中功耗表格式,从而避免冗余工作。
功率值可以以微瓦为单位或以“抽象尺度”表示。多个子系统可能会使用 EM,系统集成商应检查是否满足功率值尺度类型的要求。一个例子可以在节能调度器的文档 节能调度 中找到。对于某些子系统,如热管理或功率限制,以“抽象尺度”表示的功率值可能会导致问题。这些子系统更关注过去使用的功率估计,因此可能需要真实的微瓦值。这些要求的一个例子可以在 电源分配器调谐参数 中的智能电源分配中找到。内核子系统可能会实现自动检测以检查已注册的 EM 设备是否具有不一致的尺度(基于 EM 内部标志)。需要记住的重要一点是,当功率值以“抽象尺度”表示时,无法导出微焦耳的真实能量。
下图描述了一个驱动程序(此处特定于 Arm,但该方法适用于任何架构)向 EM 框架提供功耗,以及感兴趣的客户端从中读取数据的示例
+---------------+ +-----------------+ +---------------+
| Thermal (IPA) | | Scheduler (EAS) | | Other |
+---------------+ +-----------------+ +---------------+
| | em_cpu_energy() |
| | em_cpu_get() |
+---------+ | +---------+
| | |
v v v
+---------------------+
| Energy Model |
| Framework |
+---------------------+
^ ^ ^
| | | em_dev_register_perf_domain()
+----------+ | +---------+
| | |
+---------------+ +---------------+ +--------------+
| cpufreq-dt | | arm_scmi | | Other |
+---------------+ +---------------+ +--------------+
^ ^ ^
| | |
+--------------+ +---------------+ +--------------+
| Device Tree | | Firmware | | ? |
+--------------+ +---------------+ +--------------+
对于 CPU 设备,EM 框架管理系统中每个“性能域”的功耗表。“性能域”是一组性能一起缩放的 CPU。性能域通常与 CPUFreq 策略具有一对一的映射。性能域中的所有 CPU 都必须具有相同的微架构。不同性能域中的 CPU 可以具有不同的微架构。
为了更好地反映静态功耗(泄漏)引起的功率变化,EM 支持运行时修改功率值。该机制依赖于 RCU 来释放可修改的 EM perf_state 表内存。其用户(任务调度器)也使用 RCU 来访问此内存。EM 框架提供用于为可修改的 EM 表分配/释放新内存的 API。当给定的 EM 运行时表实例不再有所有者时,旧内存会使用 RCU 回调机制自动释放。这是使用 kref 机制跟踪的。提供运行时新 EM 的设备驱动程序应在不再需要时调用 EM API 安全地释放它。EM 框架会在可能的情况下处理清理。
想要修改 EM 值的内核代码使用互斥锁来防止并发访问。因此,设备驱动程序代码在尝试修改 EM 时必须在睡眠上下文中运行。
通过运行时可修改的 EM,我们从“单一且在整个运行时静态 EM”(系统属性)设计转变为“可以根据例如工作负载在运行时更改的单一 EM”(系统和工作负载属性)设计。
也可以修改每个 EM 性能状态的 CPU 性能值。因此,可以根据例如工作负载或系统属性更改完整的功率和性能曲线(指数曲线)。
2. 核心 API¶
2.1 配置选项¶
必须启用 CONFIG_ENERGY_MODEL 才能使用 EM 框架。
2.2 性能域的注册¶
注册“高级”EM¶
之所以称之为“高级”EM,是因为允许驱动程序提供更精确的功率模型。它不限于框架中实现的某些数学公式(就像在“简单”EM 的情况下一样)。它可以更好地反映为每个性能状态执行的实际功率测量。因此,如果考虑 EM 静态功耗(泄漏)很重要,则应首选此注册方法。
驱动程序应通过调用以下 API 将性能域注册到 EM 框架中
int em_dev_register_perf_domain(struct device *dev, unsigned int nr_states,
struct em_data_callback *cb, cpumask_t *cpus, bool microwatts);
驱动程序必须为每个性能状态提供一个返回 <频率,功率> 元组的回调函数。驱动程序提供的回调函数可以自由地从任何相关位置(DT、固件等)以及通过任何认为必要的方式获取数据。仅对于 CPU 设备,驱动程序必须使用 cpumask 指定性能域的 CPU。对于 CPU 以外的其他设备,最后一个参数必须设置为 NULL。最后一个参数 “microwatts” 设置正确值非常重要。使用 EM 的内核子系统可能依赖此标志来检查所有 EM 设备是否使用相同的尺度。如果存在不同的尺度,这些子系统可能会决定返回警告/错误、停止工作或崩溃。有关实现此回调的驱动程序示例,请参见第 3 节,或有关此 API 的更多文档,请参见第 2.4 节
使用 DT 注册 EM¶
也可以使用 OPP 框架和 DT “operating-points-v2” 中的信息来注册 EM。DT 中的每个 OPP 条目都可以扩展一个属性“opp-microwatt”,其中包含微瓦功率值。此 OPP DT 属性允许平台注册反映总功率(静态 + 动态)的 EM 功率值。这些功率值可能直接来自实验和测量。
注册“人工”EM¶
可以选择为缺少每个性能状态的功率值详细知识的驱动程序提供自定义回调。.get_cost() 回调是可选的,提供 EAS 使用的“成本”值。这对于仅提供 CPU 类型之间相对效率信息的平台很有用,在这种情况下,可以使用该信息来创建抽象功率模型。但是,即使是抽象功率模型,有时也很难适应,因为输入功率值大小的限制。.get_cost() 允许提供反映 CPU 效率的“成本”值。这将允许提供与 EM 内部计算“成本”值所强制的不同的 EAS 信息。要为此类平台注册 EM,驱动程序必须将标志“microwatts”设置为 0,提供 .get_power() 回调并提供 .get_cost() 回调。EM 框架将在注册期间正确处理此类平台。为此类平台设置了标志 EM_PERF_DOMAIN_ARTIFICIAL。使用 EM 的其他框架应特别注意测试并正确处理此标志。
注册“简单”EM¶
“简单”EM 使用框架辅助函数 cpufreq_register_em_with_opp() 注册。它实现了一个紧密结合数学公式的功率模型
Power = C * V^2 * f
使用此方法注册的 EM 可能无法正确反映真实设备的物理特性,例如当静态功耗(泄漏)很重要时。
2.3 访问性能域¶
有两个 API 函数提供对能量模型的访问:em_cpu_get()
,它将 CPU id 作为参数,以及 em_pd_get()
,它将设备指针作为参数。这取决于子系统将要使用哪个接口,但在 CPU 设备的情况下,这两个函数都返回相同的性能域。
对 CPU 的能量模型感兴趣的子系统可以使用 em_cpu_get()
API 检索它。能量模型表在创建性能域时分配一次,并在内存中保持不变。
可以使用 em_cpu_energy()
API 估计性能域消耗的能量。假设在 CPU 设备的情况下使用了 schedutil CPUfreq 调速器进行估计。目前,不为其他类型的设备提供此计算。
有关上述 API 的更多详细信息,请参见 <linux/energy_model.h>
或第 2.5 节
2.4 运行时修改¶
希望在运行时更新 EM 的驱动程序应使用以下专用函数来分配修改后的 EM 的新实例。API 如下所示
struct em_perf_table __rcu *em_table_alloc(struct em_perf_domain *pd);
这允许分配一个结构体,其中包含新的 EM 表以及 EM 框架所需的 RCU 和 kref。 ‘struct em_perf_table
’ 包含一个数组 ‘struct em_perf_state
state[]’,它是一个按升序排列的性能状态列表。该列表必须由想要更新 EM 的设备驱动程序填充。频率列表可以从现有的 EM(在启动期间创建)中获取。 ‘struct em_perf_state
’ 中的内容也必须由驱动程序填充。
这是使用 RCU 指针交换执行 EM 更新的 API。
int em_dev_update_perf_domain(struct device *dev,
struct em_perf_table __rcu *new_table);
驱动程序必须提供指向已分配和初始化的新 EM ‘struct em_perf_table
’ 的指针。该新 EM 将在 EM 框架内部安全地使用,并且对内核中的其他子系统(热管理、功率限制)可见。此 API 的主要设计目标是快速,并避免在运行时进行额外的计算或内存分配。当设备驱动程序中存在预先计算好的 EM 时,应该可以简单地重用它们,而不会产生很高的性能开销。
为了释放驱动程序先前提供的 EM(例如,在卸载模块时),需要调用 API。
void em_table_free(struct em_perf_table __rcu *table);
这将允许 EM 框架在没有其他子系统(例如 EAS)使用它时安全地删除内存。
要在其他子系统(如热管理、功率限制)中使用功率值,需要调用 API,该 API 保护读取器并提供 EM 表数据的一致性。
struct em_perf_state *em_perf_state_from_pd(struct em_perf_domain *pd);
它返回 ‘struct em_perf_state
’ 指针,它是一个按升序排列的性能状态数组。此函数必须在 RCU 读取锁区域中调用(在 rcu_read_lock()
之后)。当不再需要 EM 表时,需要调用 rcu_real_unlock()。这样,EM 可以安全地使用 RCU 读取区域并保护用户。它还允许 EM 框架管理内存并释放它。有关如何使用它的更多详细信息,请参见示例驱动程序中的第 3.2 节。
有一个专门的 API 供设备驱动程序计算 em_perf_state::cost 值。
int em_dev_compute_costs(struct device *dev, struct em_perf_state *table,
int nr_states);
EM 中的这些 “cost” 值在 EAS 中使用。新的 EM 表应与条目数和设备指针一起传递。当成本值的计算正确完成后,该函数的返回值为 0。该函数还负责为每个性能状态正确设置效率低下值。它会相应地更新 em_perf_state::flags。然后,可以将这样准备好的新 EM 传递给 em_dev_update_perf_domain()
函数,这将允许使用它。
有关上述 API 的更多详细信息,请参阅 <linux/energy_model.h>
或第 3.2 节,其中包含一个示例代码,展示了设备驱动程序中更新机制的简单实现。
2.5 此 API 的详细描述¶
-
struct em_perf_state¶
性能域的性能状态
定义:
struct em_perf_state {
unsigned long performance;
unsigned long frequency;
unsigned long power;
unsigned long cost;
unsigned long flags;
};
成员
performance
在给定频率下的 CPU 性能(容量)
frequency
以 KHz 为单位的频率,与 CPUFreq 保持一致
power
在此级别消耗的功率(由 1 个 CPU 或注册设备消耗)。它可以是总功率:静态和动态。
cost
与此级别关联的成本系数,在能量计算期间使用。等于:power * max_frequency / frequency
flags
请参阅下面的 “em_perf_state flags” 说明。
-
struct em_perf_table¶
性能状态表
定义:
struct em_perf_table {
struct rcu_head rcu;
struct kref kref;
struct em_perf_state state[];
};
成员
rcu
用于安全访问和销毁的 RCU
kref
用于跟踪用户的引用计数器
state
按升序排列的性能状态列表
-
struct em_perf_domain¶
性能域
定义:
struct em_perf_domain {
struct em_perf_table __rcu *em_table;
int nr_perf_states;
int min_perf_state;
int max_perf_state;
unsigned long flags;
unsigned long cpus[];
};
成员
em_table
指向运行时可修改的 em_perf_table 的指针
nr_perf_states
性能状态的数量
min_perf_state
允许的最小性能状态索引
max_perf_state
允许的最大性能状态索引
flags
请参阅 “em_perf_domain flags”
cpus
覆盖域的 CPU 的 Cpumask。这是出于性能原因,以避免在调度器中进行能量计算时出现潜在的缓存未命中,并简化该内存区域的分配/释放。
描述
对于 CPU 设备,一个“性能域”表示一组性能同步伸缩的 CPU。性能域的所有 CPU 必须具有相同的微架构。性能域通常与 CPUFreq 策略具有 1 对 1 的映射。对于其他设备,cpus 字段未使用。
-
int em_pd_get_efficient_state(struct em_perf_state *table, struct em_perf_domain *pd, unsigned long max_util)¶
从 EM 获取有效的性能状态
参数
struct em_perf_state *table
按升序排列的性能状态列表
struct em_perf_domain *pd
必须执行此操作的性能域
unsigned long max_util
要与 EM 映射的最大利用率
描述
它是从调度器代码中频繁调用的,因此不实现任何检查。
返回
一个有效的性能状态 ID,高到足以满足 max_util 要求。
-
unsigned long em_cpu_energy(struct em_perf_domain *pd, unsigned long max_util, unsigned long sum_util, unsigned long allowed_cpu_cap)¶
估算性能域的 CPU 消耗的能量
参数
struct em_perf_domain *pd
必须估算能量的性能域
unsigned long max_util
域中 CPU 的最高利用率
unsigned long sum_util
域中所有 CPU 的利用率之和
unsigned long allowed_cpu_cap
pd 的最大允许 CPU 容量,可能反映降低的频率(由于热管理)
描述
此函数必须仅用于 CPU 设备。没有验证,即 EM 是否为 CPU 类型且已分配 cpumask。它是从调度器代码中频繁调用的,这就是为什么没有检查的原因。
返回
假设满足域的最大利用率的容量状态,该域的 CPU 消耗的能量总和。
-
int em_pd_nr_perf_states(struct em_perf_domain *pd)¶
获取性能域的性能状态数
参数
struct em_perf_domain *pd
必须执行此操作的性能域
返回
性能域表中的性能状态数
-
struct em_perf_state *em_perf_state_from_pd(struct em_perf_domain *pd)¶
获取性能域的性能状态表
参数
struct em_perf_domain *pd
必须执行此操作的性能域
描述
要使用此函数,应保持 rcu_read_lock()
。在性能状态表的使用完成后,应调用 rcu_read_unlock()
。
返回
指向性能域的性能状态表的指针
-
int em_dev_update_perf_domain(struct device *dev, struct em_perf_table __rcu *new_table)¶
更新设备的运行时 EM 表
参数
struct device *dev
要更新 EM 的设备
struct em_perf_table __rcu *new_table
从现在开始要使用的新 EM 表
描述
使用提供的 table 更新 dev 的 EM 运行时可修改表。
此函数使用互斥锁来序列化写入器,因此不能从非睡眠上下文调用。
成功时返回 0,失败时返回错误代码。
-
struct em_perf_domain *em_pd_get(struct device *dev)¶
返回设备的性能域
参数
struct device *dev
要查找性能域的设备
描述
返回 dev 所属的性能域,如果不存在,则返回 NULL。
-
struct em_perf_domain *em_cpu_get(int cpu)¶
返回 CPU 的性能域
参数
int cpu
要查找性能域的 CPU
描述
返回 cpu 所属的性能域,如果不存在,则返回 NULL。
-
int em_dev_register_perf_domain(struct device *dev, unsigned int nr_states, struct em_data_callback *cb, cpumask_t *cpus, bool microwatts)¶
为设备注册能量模型 (EM)
参数
struct device *dev
要注册 EM 的设备
unsigned int nr_states
要注册的性能状态数
struct em_data_callback *cb
提供能量模型数据的回调函数
cpumask_t *cpus
指向 cpumask_t 的指针,如果是 CPU 设备,则必须使用此指针。 可以从例如 ‘policy->cpus’ 中获取。 对于其他类型的设备,应将其设置为 NULL。
bool microwatts
标志指示功率值是否以微瓦为单位或以其他刻度为单位。 必须正确设置。
描述
使用 cb 中定义的回调为性能域创建能量模型表。
microwatts 设置正确的值非常重要。 一些内核子系统可能依赖于此标志,并检查 EM 中的所有设备是否都使用相同的刻度。
如果多个客户端注册相同的性能域,则将忽略除第一次注册外的所有注册。
成功时返回 0
参数
struct device *dev
已注册 EM 的设备
描述
取消注册指定 dev 的 EM(但不是 CPU 设备)。
参数
struct device *dev
必须更新能量模型的设备。
描述
此函数允许使用 OPP 框架和 DT 中提供的新值轻松更新 EM。 它可以在设备驱动程序正确验证芯片并且调整了“芯片分级”的电压后使用。
-
int em_update_performance_limits(struct em_perf_domain *pd, unsigned long freq_min_khz, unsigned long freq_max_khz)¶
使用性能限制信息更新能量模型。
参数
struct em_perf_domain *pd
具有必须更新的 EM 的性能域。
unsigned long freq_min_khz
此设备允许的新最小频率。
unsigned long freq_max_khz
此设备允许的新最大频率。
描述
此函数允许使用有关可用性能级别的信息更新 EM。 它以 kHz 为单位获取最小和最大频率,并进行内部转换以获得性能级别。 成功时返回 0,失败时返回 -EINVAL。
3. 示例¶
3.1 具有 EM 注册的示例驱动程序¶
CPUFreq 框架支持用于为给定 CPU(s) ‘policy’ 对象注册 EM 的专用回调:cpufreq_driver::register_em()。 必须为给定驱动程序正确实现该回调,因为框架会在设置期间在正确的时间调用它。 本节提供了一个简单的 CPUFreq 驱动程序示例,该驱动程序使用(伪)‘foo’ 协议在能量模型框架中注册性能域。 该驱动程序实现了一个 est_power() 函数,该函数将提供给 EM 框架
-> drivers/cpufreq/foo_cpufreq.c
01 static int est_power(struct device *dev, unsigned long *mW,
02 unsigned long *KHz)
03 {
04 long freq, power;
05
06 /* Use the 'foo' protocol to ceil the frequency */
07 freq = foo_get_freq_ceil(dev, *KHz);
08 if (freq < 0);
09 return freq;
10
11 /* Estimate the power cost for the dev at the relevant freq. */
12 power = foo_estimate_power(dev, freq);
13 if (power < 0);
14 return power;
15
16 /* Return the values to the EM framework */
17 *mW = power;
18 *KHz = freq;
19
20 return 0;
21 }
22
23 static void foo_cpufreq_register_em(struct cpufreq_policy *policy)
24 {
25 struct em_data_callback em_cb = EM_DATA_CB(est_power);
26 struct device *cpu_dev;
27 int nr_opp;
28
29 cpu_dev = get_cpu_device(cpumask_first(policy->cpus));
30
31 /* Find the number of OPPs for this policy */
32 nr_opp = foo_get_nr_opp(policy);
33
34 /* And register the new performance domain */
35 em_dev_register_perf_domain(cpu_dev, nr_opp, &em_cb, policy->cpus,
36 true);
37 }
38
39 static struct cpufreq_driver foo_cpufreq_driver = {
40 .register_em = foo_cpufreq_register_em,
41 };
3.2 具有 EM 修改的示例驱动程序¶
本节提供了一个修改 EM 的简单热驱动程序示例。 该驱动程序实现了一个 foo_thermal_em_update() 函数。 该驱动程序会定期被唤醒以检查温度并修改 EM 数据
-> drivers/soc/example/example_em_mod.c
01 static void foo_get_new_em(struct foo_context *ctx)
02 {
03 struct em_perf_table __rcu *em_table;
04 struct em_perf_state *table, *new_table;
05 struct device *dev = ctx->dev;
06 struct em_perf_domain *pd;
07 unsigned long freq;
08 int i, ret;
09
10 pd = em_pd_get(dev);
11 if (!pd)
12 return;
13
14 em_table = em_table_alloc(pd);
15 if (!em_table)
16 return;
17
18 new_table = em_table->state;
19
20 rcu_read_lock();
21 table = em_perf_state_from_pd(pd);
22 for (i = 0; i < pd->nr_perf_states; i++) {
23 freq = table[i].frequency;
24 foo_get_power_perf_values(dev, freq, &new_table[i]);
25 }
26 rcu_read_unlock();
27
28 /* Calculate 'cost' values for EAS */
29 ret = em_dev_compute_costs(dev, table, pd->nr_perf_states);
30 if (ret) {
31 dev_warn(dev, "EM: compute costs failed %d\n", ret);
32 em_free_table(em_table);
33 return;
34 }
35
36 ret = em_dev_update_perf_domain(dev, em_table);
37 if (ret) {
38 dev_warn(dev, "EM: update failed %d\n", ret);
39 em_free_table(em_table);
40 return;
41 }
42
43 /*
44 * Since it's one-time-update drop the usage counter.
45 * The EM framework will later free the table when needed.
46 */
47 em_table_free(em_table);
48 }
49
50 /*
51 * Function called periodically to check the temperature and
52 * update the EM if needed
53 */
54 static void foo_thermal_em_update(struct foo_context *ctx)
55 {
56 struct device *dev = ctx->dev;
57 int cpu;
58
59 ctx->temperature = foo_get_temp(dev, ctx);
60 if (ctx->temperature < FOO_EM_UPDATE_TEMP_THRESHOLD)
61 return;
62
63 foo_get_new_em(ctx);
64 }