设备能量模型

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 策略具有 1 对 1 的映射。性能域中的所有 CPU 都需要具有相同的微架构。不同性能域中的 CPU 可以具有不同的微架构。

为了更好地反映由于静态功耗(泄漏)引起的功率变化,EM 支持运行时修改功率值。该机制依赖于 RCU 来释放可修改的 EM perf_state 表内存。其用户,任务调度器,也使用 RCU 来访问此内存。EM 框架提供 API 用于为可修改的 EM 表分配/释放新内存。当给定的 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);

驱动程序必须提供一个回调函数,该函数为每个性能状态返回 <frequency, power> 元组。驱动程序提供的回调函数可以自由地从任何相关位置(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

使用框架辅助函数 cpufreq_register_em_with_opp() 注册“简单”EM。它实现了一个紧密结合数学公式的功率模型

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);

当没有其他子系统使用它时,例如 EAS,这将允许 EM 框架安全地删除内存。

为了在其他子系统(如散热,功耗限制)中使用功率值,需要调用保护读取器并提供 EM 表数据一致性的 API

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

与此级别关联的成本系数,在能量计算期间使用。等于:功率 * 最大频率 / 频率

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 *new_table)

更新设备的运行时 EM 表

参数

struct device *dev

要更新 EM 的设备

struct em_perf_table *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, const struct em_data_callback *cb, const cpumask_t *cpus, bool microwatts)

为设备注册能量模型 (EM)

参数

struct device *dev

要注册 EM 的设备

unsigned int nr_states

要注册的性能状态数

const struct em_data_callback *cb

提供能量模型数据的回调函数

const cpumask_t *cpus

指向 cpumask_t 的指针,在 CPU 设备的情况下是必需的。它可以从 i.e. ‘policy->cpus’ 中获取。对于其他类型的设备,应将其设置为 NULL。

bool microwatts

标志,指示功率值是否以微瓦或其他比例表示。必须正确设置它。

描述

使用 cb 中定义的回调函数为性能域创建能量模型表。

正确设置 **microwatts** 非常重要。某些内核子系统可能会依赖此标志并检查 EM 中的所有设备是否使用相同的比例。

如果多个客户端注册同一性能域,则除第一个注册外的所有注册都将被忽略。

成功时返回 0

void em_dev_unregister_perf_domain(struct device *dev)

注销设备的能量模型 (EM)

参数

struct device *dev

注册 EM 的设备

描述

注销指定的 **dev** 的 EM(但不是 CPU 设备)。

int em_dev_update_chip_binning(struct device *dev)

在新电压信息出现在 OPP 中之后更新能量模型。

参数

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(多个)“策略”对象注册 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, new_table, pd->nr_perf_states);
30            if (ret) {
31                    dev_warn(dev, "EM: compute costs failed %d\n", ret);
32                    em_table_free(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_table_free(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    }