1. 前言
本文以ARM64平台下的cpuidle driver为例,说明怎样在cpuidle framework的框架下,编写cpuidle driver。另外,本文在描述cpuidle driver的同时,会涉及到CPU hotplug的概念,因此也可作为CPU hotplug的引子。
2. arm_idle_init
ARM generic CPU idle driver的代码位于“drivers/cpuidle/cpuidle-arm.c”中,它的入口函数是arm_idle_init,如下:
/*
* arm_idle_init - Initializes arm cpuidle driver
*
* Initializes arm cpuidle driver for all CPUs, if any CPU fails
* to register cpuidle driver then rollback to cancel all CPUs
* registration.
*/
static int __init arm_idle_init(void)
{
int cpu, ret;
struct cpuidle_driver *drv;
struct cpuidle_device *dev;
// 遍历所有可能的 CPU
for_each_possible_cpu(cpu) {
// 调用 arm_idle_init_cpu 函数对每个 CPU 初始化 cpuidle 驱动
ret = arm_idle_init_cpu(cpu);
if (ret)
goto out_fail; // 如果初始化失败,则跳转到 out_fail 标签进行回滚操作
}
return 0; // 初始化成功,返回 0
out_fail:
// 回滚操作,取消已注册的 cpuidle 驱动
while (--cpu >= 0) {
// 获取当前 CPU 对应的 cpuidle 设备和驱动
dev = per_cpu(cpuidle_devices, cpu);
drv = cpuidle_get_cpu_driver(dev);
// 注销 cpuidle 驱动并释放相关资源
cpuidle_unregister(drv);
kfree(drv);
}
return ret; // 返回初始化失败的错误码
}
// 在设备初始化时调用 arm_idle_init 函数
device_initcall(arm_idle_init);
arm_idle_init 函数是 ARM 架构的 cpuidle 驱动的初始化函数。该函数负责对所有可能的 CPU 初始化 cpuidle 驱动,并在任何一个 CPU 初始化失败时执行回滚操作。
使用 for_each_possible_cpu 循环遍历系统中所有可能的 CPU。
对每个 CPU 调用 arm_idle_init_cpu 函数,该函数可能执行特定于 ARM 架构的 cpuidle 驱动的初始化。如果初始化失败(返回非零值),则跳转到标签 out_fail 进行回滚操作。
如果所有 CPU 成功初始化,函数返回 0 表示成功。
如果有任何一个 CPU 初始化失败,通过标签 out_fail 执行回滚操作。在回滚时,使用 per_cpu 宏获取每个 CPU 对应的 cpuidle 设备和驱动,然后使用 cpuidle_unregister 注销 cpuidle 驱动,并使用 kfree 释放相关资源。
device_initcall(arm_idle_init) 将 arm_idle_init 函数注册为设备初始化函数,确保在系统启动时调用该函数进行 cpuidle 驱动的初始化。
总体而言,这段代码是 ARM 架构中用于初始化 cpuidle 驱动的代码,它通过遍历所有可能的 CPU 来初始化每个 CPU 上的 cpuidle 驱动,同时处理初始化失败的回滚操作
/*
* arm_idle_init_cpu
*
* Registers the arm specific cpuidle driver with the cpuidle
* framework. It relies on core code to parse the idle states
* and initialize them using driver data structures accordingly.
*/
static int __init arm_idle_init_cpu(int cpu)
{
int ret;
struct cpuidle_driver *drv;
// 通过 kmemdup 复制 arm_idle_driver 结构体,分配内存并初始化 cpuidle 驱动
drv = kmemdup(&arm_idle_driver, sizeof(*drv), GFP_KERNEL);
if (!drv)
return -ENOMEM; // 内存分配失败,返回错误码 -ENOMEM
// 设置 cpumask,指定对应 CPU
drv->cpumask = (struct cpumask *)cpumask_of(cpu);
/*
* Initialize idle states data, starting at index 1. This
* driver is DT only, if no DT idle states are detected (ret
* == 0) let the driver initialization fail accordingly since
* there is no reason to initialize the idle driver if only
* wfi is supported.
*/
// 使用 dt_init_idle_driver 初始化驱动的空闲状态数据,从索引 1 开始
ret = dt_init_idle_driver(drv, arm_idle_state_match, 1);
if (ret <= 0) {
ret = ret ? : -ENODEV; // 如果没有检测到 DT 空闲状态,设置错误码为 -ENODEV
goto out_kfree_drv; // 跳转到标签 out_kfree_drv 进行错误处理
}
/*
* Call arch CPU operations in order to initialize
* idle states suspend back-end specific data
*/
// 调用 arm_cpuidle_init 初始化 CPU 的空闲状态挂起后端的特定数据
ret = arm_cpuidle_init(cpu);
/*
* Allow the initialization to continue for other CPUs, if the
* reported failure is a HW misconfiguration/breakage (-ENXIO).
*
* Some platforms do not support idle operations
* (arm_cpuidle_init() returning -EOPNOTSUPP), we should
* not flag this case as an error, it is a valid
* configuration.
*/
// 如果初始化失败且错误码不是 -EOPNOTSUPP,则打印错误信息,但继续初始化其他 CPU
if (ret) {
if (ret != -EOPNOTSUPP)
pr_err("CPU %d failed to init idle CPU ops\n", cpu);
ret = ret == -ENXIO ? 0 : ret; // 如果错误码是 -ENXIO,则设置为 0,表示继续初始化其他 CPU
goto out_kfree_drv; // 跳转到标签 out_kfree_drv 进行错误处理
}
// 注册 cpuidle 驱动
ret = cpuidle_register(drv, NULL);
if (ret)
goto out_kfree_drv; // 注册失败,跳转到标签 out_kfree_drv 进行错误处理
// 注册 cpuidle 驱动的冷却设备
cpuidle_cooling_register(drv);
return 0; // 初始化成功,返回 0
out_kfree_drv:
// 错误处理,释放已分配的内存
kfree(drv);
return ret; // 返回错误码
}
arm_idle_init_cpu 函数用于初始化 ARM 架构特定的 cpuidle 驱动,注册到 cpuidle 框架中。
使用 kmemdup 复制 arm_idle_driver 结构体,该结构体包含 ARM 架构的 cpuidle 驱动信息。
设置 cpumask,指定对应 CPU。
使用 dt_init_idle_driver 初始化驱动的空闲状态数据,从索引 1 开始。如果没有检测到 DT 空闲状态,设置错误码为 -ENODEV。
调用 arm_cpuidle_init 初始化 CPU 的空闲状态挂起后端的特定数据。
允许初始化继续进行其他 CPU,如果报告的失败是硬件配置错误(-ENXIO)。
如果初始化失败且错误码不是 -EOPNOTSUPP,则打印错误信息,但继续初始化其他 CPU。如果错误码是 -ENXIO,则设置为 0,表示继续初始化其他 CPU。
注册 cpuidle 驱动,如果注册失败,则跳转到标签 out_kfree_drv 进行错误处理。
注册 cpuidle 驱动的冷却设备。
如果所有步骤成功,则返回 0 表示初始化成功。如果发生错误,通过标签 out_kfree_drv 进行错误处理,释放已分配的内存并返回相应的错误码。
static struct cpuidle_driver arm_idle_driver __initdata = {
.name = "arm_idle",
.owner = THIS_MODULE,
/*
* State at index 0 is standby wfi and considered standard
* on all ARM platforms. If in some platforms simple wfi
* can't be used as "state 0", DT bindings must be implemented
* to work around this issue and allow installing a special
* handler for idle state index 0.
*/
.states[0] = {
.enter = arm_enter_idle_state,
.exit_latency = 1,
.target_residency = 1,
.power_usage = UINT_MAX,
.name = "WFI",
.desc = "ARM WFI",
}
};
arm_idle_driver 是一个静态的 struct cpuidle_driver 结构体,用于定义 ARM 架构的 cpuidle 驱动。
__initdata 标识符表示该结构体是用于初始化过程的数据,且在初始化完成后可以被丢弃。
name 字段设置了 cpuidle 驱动的名称为 "arm_idle"。
owner 字段设置了 cpuidle 驱动的所有者为当前模块(THIS_MODULE 是指当前编写的内核模块)。
states[0] 定义了 cpuidle 的第一个状态(索引为 0),通常代表的是 "standby wfi" 状态。这个状态在所有 ARM 平台上被认为是标准状态。注释中提到,如果在某些平台上简单的 wfi 不能用作 "state 0",则需要实现 DT 绑定来解决此问题,并允许为空闲状态索引 0 安装特殊处理程序。
对于 "state 0",定义了以下参数:
enter: 进入空闲状态时调用的函数,此处为 arm_enter_idle_state。
exit_latency: 退出空闲状态的延迟,设置为 1。
target_residency: 目标停留时间,设置为 1。
power_usage: 预计功耗,设置为 UINT_MAX(表示无限大)。
name: 状态的名称,设置为 "WFI"。
desc: 状态的描述,设置为 "ARM WFI"。
这个结构体定义了 ARM 架构的 cpuidle 驱动的基本特性和参数。
关于 sysfs 节点,这些变量和结构体主要体现在 /sys/devices/system/cpu/cpuidle/ 目录下。具体来说,以下节点是相关的:
/sys/devices/system/cpu/cpuidle/driver[arm_idle]/:包含有关 arm_idle 驱动的信息,例如驱动的名称、所有者等。
/sys/devices/system/cpu/cpuidle/current_driver:指示当前正在使用的 cpuidle 驱动的名称。
/sys/devices/system/cpu/cpuidle/current_governor:指示当前正在使用的 cpuidle 调度器(governor)的名称。
/sys/devices/system/cpu/cpuidle/ 下的其他子目录和文件可能包含有关空闲状态的信息,例如功耗、延迟等。具体的节点结构和名称可能因实际系统和内核配置而异。
3. dt_init_idle_driver
dt_init_idle_driver函数用于从DTS中解析出cpuidle states的信息,并初始化arm64_idle_driver中的states数组。在分析这个函数之前,我们先来看一下cpuidle相关的DTS源文件是怎么写的:
cpus {
#address-cells = <1>;
#size-cells = <0>;
idle-states {
entry-method = "psci";
cpu_pd_wait: cpu-pd-wait {
compatible = "arm,idle-state";
arm,psci-suspend-param = <0x0010033>;
local-timer-stop;
entry-latency-us = <1000>;
exit-latency-us = <700>;
min-residency-us = <2700>;
wakeup-latency-us = <1500>;
};
};
A53_0: cpu@0 {
device_type = "cpu";
compatible = "arm,cortex-a53";
reg = <0x0>;
clock-latency = <61036>;
clocks = <&clk IMX8MP_CLK_ARM>;
enable-method = "psci";
i-cache-size = <0x8000>;
i-cache-line-size = <64>;
i-cache-sets = <256>;
d-cache-size = <0x8000>;
d-cache-line-size = <64>;
d-cache-sets = <128>;
next-level-cache = <&A53_L2>;
nvmem-cells = <&cpu_speed_grade>;
nvmem-cell-names = "speed_grade";
operating-points-v2 = <&a53_opp_table>;
#cooling-cells = <2>;
cpu-idle-states = <&cpu_pd_wait>;
};
A53_1: cpu@1 {
device_type = "cpu";
compatible = "arm,cortex-a53";
reg = <0x1>;
clock-latency = <61036>;
clocks = <&clk IMX8MP_CLK_ARM>;
enable-method = "psci";
i-cache-size = <0x8000>;
i-cache-line-size = <64>;
i-cache-sets = <256>;
d-cache-size = <0x8000>;
d-cache-line-size = <64>;
d-cache-sets = <128>;
next-level-cache = <&A53_L2>;
operating-points-v2 = <&a53_opp_table>;
#cooling-cells = <2>;
cpu-idle-states = <&cpu_pd_wait>;
};
A53_2: cpu@2 {
device_type = "cpu";
compatible = "arm,cortex-a53";
reg = <0x2>;
clock-latency = <61036>;
clocks = <&clk IMX8MP_CLK_ARM>;
enable-method = "psci";
i-cache-size = <0x8000>;
i-cache-line-size = <64>;
i-cache-sets = <256>;
d-cache-size = <0x8000>;
d-cache-line-size = <64>;
d-cache-sets = <128>;
next-level-cache = <&A53_L2>;
operating-points-v2 = <&a53_opp_table>;
#cooling-cells = <2>;
cpu-idle-states = <&cpu_pd_wait>;
};
A53_3: cpu@3 {
device_type = "cpu";
compatible = "arm,cortex-a53";
reg = <0x3>;
clock-latency = <61036>;
clocks = <&clk IMX8MP_CLK_ARM>;
enable-method = "psci";
i-cache-size = <0x8000>;
i-cache-line-size = <64>;
i-cache-sets = <256>;
d-cache-size = <0x8000>;
d-cache-line-size = <64>;
d-cache-sets = <128>;
next-level-cache = <&A53_L2>;
operating-points-v2 = <&a53_opp_table>;
#cooling-cells = <2>;
cpu-idle-states = <&cpu_pd_wait>;
};
A53_L2: l2-cache0 {
compatible = "cache";
cache-level = <2>;
cache-size = <0x80000>;
cache-line-size = <64>;
cache-sets = <512>;
};
};
idle-states:这是一个 Device Tree 中的节点,用于描述 CPU 空闲状态的配置信息。
entry-method = "psci";:指定了空闲状态的进入方法为 PSCI(Power State Coordination Interface)。
cpu_pd_wait:这是具体的 CPU 空闲状态的描述符,可以根据实际需要定义多个不同的空闲状态。
compatible = "arm,idle-state";:指定了这个节点兼容的设备或驱动类型,表示这是一个 ARM 架构的空闲状态。
arm,psci-suspend-param = <0x0010033>;:指定了 PSCI 中的挂起参数,用于在进入空闲状态时传递给 PSCI 接口。在这里,参数值为 0x0010033。
local-timer-stop;:指定在进入空闲状态时停止本地定时器。
entry-latency-us = <1000>;:定义了进入空闲状态的延迟,单位是微秒,这里设置为 1000 微秒。
exit-latency-us = <700>;:定义了退出空闲状态的延迟,单位是微秒,这里设置为 700 微秒。
min-residency-us = <2700>;:定义了最小停留时间,单位是微秒,这里设置为 2700 微秒。
wakeup-latency-us = <1500>;:定义了唤醒空闲状态的延迟,单位是微秒,这里设置为 1500 微秒。
总体而言,这段 DTS 描述了一个名为 cpu-pd-wait 的 CPU 空闲状态,采用 PSCI 进行挂起,停止本地定时器,具有特定的延迟和参数设置。这样的信息对于系统调度和功耗管理非常重要,操作系统可以根据这些信息有效地进行空闲状态的管理和调度.
结合上面的DTS信息,dt_init_idle_driver函数的执行过程如下。
/**
* dt_init_idle_driver() - Parse the DT idle states and initialize the
* idle driver states array
* @drv: Pointer to CPU idle driver to be initialized
* @matches: Array of of_device_id match structures to search in for
* compatible idle state nodes. The data pointer for each valid
* struct of_device_id entry in the matches array must point to
* a function with the following signature, that corresponds to
* the CPUidle state enter function signature:
*
* int (*)(struct cpuidle_device *dev,
* struct cpuidle_driver *drv,
* int index);
*
* @start_idx: First idle state index to be initialized
*
* If DT idle states are detected and are valid the state count and states
* array entries in the cpuidle driver are initialized accordingly starting
* from index start_idx.
*
* Return: number of valid DT idle states parsed, <0 on failure
*/
int dt_init_idle_driver(struct cpuidle_driver *drv,
const struct of_device_id *matches,
unsigned int start_idx)
{
struct cpuidle_state *idle_state;
struct device_node *state_node, *cpu_node;
const struct of_device_id *match_id;
int i, err = 0;
const cpumask_t *cpumask;
unsigned int state_idx = start_idx;
if (state_idx >= CPUIDLE_STATE_MAX)
return -EINVAL; // 如果开始的索引大于等于 CPUIDLE_STATE_MAX,返回无效参数错误
/*
* We get the idle states for the first logical cpu in the
* driver mask (or cpu_possible_mask if the driver cpumask is not set)
* and we check through idle_state_valid() if they are uniform
* across CPUs, otherwise we hit a firmware misconfiguration.
*/
// 获取驱动掩码(或者如果驱动掩码没有设置,则使用 cpu_possible_mask)的第一个逻辑 CPU 的空闲状态
cpumask = drv->cpumask ? : cpu_possible_mask;
cpu_node = of_cpu_device_node_get(cpumask_first(cpumask));
for (i = 0; ; i++) {
// 获取 CPU 状态节点
state_node = of_get_cpu_state_node(cpu_node, i);
if (!state_node)
break; // 如果没有更多的状态节点,退出循环
// 在 matches 中查找匹配的设备节点
match_id = of_match_node(matches, state_node);
if (!match_id) {
err = -ENODEV; // 如果没有匹配的设备节点,返回设备不存在错误
break;
}
// 检查设备节点是否可用
if (!of_device_is_available(state_node)) {
of_node_put(state_node);
continue; // 如果设备节点不可用,继续下一个循环
}
// 检查空闲状态是否有效
if (!idle_state_valid(state_node, i, cpumask)) {
pr_warn("%pOF idle state not valid, bailing out\n",
state_node);
err = -EINVAL; // 如果空闲状态无效,返回无效参数错误
break;
}
if (state_idx == CPUIDLE_STATE_MAX) {
pr_warn("State index reached static CPU idle driver states array size\n");
break; // 如果状态索引达到静态 CPU 空闲驱动状态数组的大小,退出循环
}
// 初始化空闲状态节点
idle_state = &drv->states[state_idx++];
err = init_state_node(idle_state, match_id, state_node);
if (err) {
pr_err("Parsing idle state node %pOF failed with err %d\n",
state_node, err);
err = -EINVAL; // 如果初始化失败,返回无效参数错误
break;
}
of_node_put(state_node);
}
of_node_put(state_node);
of_node_put(cpu_node);
if (err)
return err; // 如果发生错误,返回错误码
/* Set the number of total supported idle states. */
// 设置总支持的空闲状态数量
drv->state_count = state_idx;
/*
* Return the number of present and valid DT idle states, which can
* also be 0 on platforms with missing DT idle states or legacy DT
* configuration predating the DT idle states bindings.
*/
// 返回存在且有效的 DT 空闲状态的数量,也可能是 0,表示缺少 DT 空闲状态或者遗留的 DT 配置先于 DT 空闲状态绑定
return state_idx - start_idx;
}
EXPORT_SYMBOL_GPL(dt_init_idle_driver);
dt_init_idle_driver 函数用于解析设备树(Device Tree)中的空闲状态并初始化 cpuidle 驱动的状态数组。
参数:
drv:指向要初始化的 CPU 空闲驱动的指针。
matches:of_device_id 结构体数组,用于匹配设备节点。
start_idx:空闲状态数组的起始索引。
函数首先检查起始索引是否超过了数组的最大索引值,如果是,则返回无效参数错误。
通过获取驱动的掩码(或者使用 cpu_possible_mask 如果掩码未设置),获得第一个逻辑 CPU 的设备节点。
在循环中,获取每个 CPU 状态节点,然后检查匹配和有效性。如果没有更多的状态节点,则退出循环。
初始化空闲状态节点,包括从设备节点中解析状态信息,并设置到 cpuidle 驱动的状态数组中。
如果发生错误,返回相应的错误码。否则,设置 cpuidle 驱动支持的总空闲状态数量。
最后返回存在且有效的 DT 空闲状态的数量,可能为 0。
这个函数主要用于从设备树中读取空闲状态信息,因此相关的 sysfs 节点和文件通常是通过设备树提供的。
4.arm_enter_idle_state
/*
* arm_enter_idle_state - Programs CPU to enter the specified state
*
* dev: cpuidle device
* drv: cpuidle driver
* idx: state index
*
* Called from the CPUidle framework to program the device to the
* specified target state selected by the governor.
*/
static __cpuidle int arm_enter_idle_state(struct cpuidle_device *dev,
struct cpuidle_driver *drv, int idx)
{
/*
* Pass idle state index to arm_cpuidle_suspend which in turn
* will call the CPU ops suspend protocol with idle index as a
* parameter.
*/
return CPU_PM_CPU_IDLE_ENTER(arm_cpuidle_suspend, idx);
}
arm_enter_idle_state 函数是 ARM 架构中用于进入指定空闲状态的函数。它是由 CPUidle 框架调用的,目的是将设备设置到由 governor 选择的特定目标状态。
参数:
dev:指向 cpuidle 设备的指针。
drv:指向 cpuidle 驱动的指针。
idx:空闲状态的索引。
函数内部调用了 CPU_PM_CPU_IDLE_ENTER 宏,将空闲状态的索引传递给 arm_cpuidle_suspend 函数。该宏用于执行与 CPU 操作挂起协议相关的操作,其中传递了空闲状态的索引作为参数。
返回值是函数调用的结果,通常表示进入空闲状态的成功与否。这个返回值将被 CPUidle 框架用于进一步的处理,以通知系统是否成功进入了指定的空闲状态。
总体而言,这个函数负责将设备设置到指定的空闲状态,它通过调用底层的 arm_cpuidle_suspend 函数来实现挂起协议的执行,以便进入空闲状态。