linux之调度管理(11)-cpu动态调频总体架构
一、cpufreq的背景
随着技术的发展,当前soc中的cpu主频一般都超过了1Ghz,而cpu的主频越高,其消耗的功耗也越大,这主要体现在以下两个方面:
(1)cpu的运行频率越高,则晶体管在单位时间内的开关次数就越多,与其相关的动态功耗也越高。
(2)为了保证数字电路的逻辑正确,cpu运行频率越高,则其所需的供电电压也越高
但是系统在负载较轻时可能并不需要工作在最高主频上,特别是对于手机等可移动设备,这种性能浪费会直接影响其续航能力。为此若能根据系统的负载情况,动态调整cpu频率和电压,就能减少这部分功率消耗。cpufreq就是内核为支持这种能力而定义的一套管理框架。
二、cpufreq总体架构
cpufreq主要功能为动态调节cpu的频率和电压,为了达到这一目标,该框架需要考虑以下几个问题:
(1)确定每个cpu的频率调节范围,包括硬件支持的最大频率、最小频率,以及软件可调节的最大频率和最小频率等。在cpufreq中它可通过policy来管理
(2)在当前负载下,确定cpu应该工作于哪个频率,如简单地使cpu工作在最高频率以提供最高性能,或者使其工作在最低频率以降低功耗。但系统的负载是动态变化的,因此cpufreq还支持通过特定算法,在负载变化时动态调节cpu的频率。它可通过其governor组件实现
(3)确定cpu频率调节的周期及触发时机,以使其既不对系统有太大影响,又能较快地跟踪系统的负载变化。它是通过内核调度器和cpufreq模块共同完成的
(4)当待调节频率确定后,如何调用频率设置接口,实现实际的频率调节工作。它是通过其driver组件实现的
除此以外,cpufreq还包含cpufreq stats,cpufreq qos,cpufreq notifier等辅助模块,其主要功能如下:
(1)cpufreq stats:用于搜集cpufreq的一些统计数据,如cpu在每个频点下的运行时间,总的切频次数等。
(2)cpufreq qos:该模块用于在cpufreq频率限制值发生改变时,向cpufreq模块发送一个通知,使其能及时调整到新的值
(3)cpufreq notifier:那些对cpu频率切换,或policy对应governor发生改变感兴趣的模块,可以向cpufreq注册一个通知。当以上事件发生时,cpufreq将会向其发送相关通知
最后由于cpu的工作电压越高,其功耗也越大,因此我们希望cpu在切换到某一频率时,其电压也相应地切换到其可稳定工作的最低电压。为了得到电压和频率的关系,一般soc都会提供一些频点组合,cpufreq只会在这些规定的频点中切换。这些系统预置的频点叫做operation performance points(opp),它可通过如下例所示的dts文件配置:
cpu0: cpu@0 {
...
operating-points-v2 = <&cluster0_opp>;
…
};
cpu1: cpu@1 {
...
operating-points-v2 = <&cluster0_opp>;
...
};
cluster0_opp: opp_table0 {
compatible = "operating-points-v2";
opp-shared;
opp00 {
opp-hz = /bits/ 64 <533000000>;
opp-microvolt = <700000>;
clock-latency-ns = <300000>;
};
opp01 {
opp-hz = /bits/ 64 <999000000>;
opp-microvolt = <800000>;
clock-latency-ns = <300000>;
};
opp02 {
opp-hz = /bits/ 64 <1402000000>;
opp-microvolt = <900000>;
clock-latency-ns = <300000>;
};
opp03 {
opp-hz = /bits/ 64 <1709000000>;
opp-microvolt = <1000000>;
clock-latency-ns = <300000>;
};
opp04 {
opp-hz = /bits/ 64 <1844000000>;
opp-microvolt = <1100000>;
clock-latency-ns = <300000>;
};
};
内核抽象出了一个opp组件,用于管理这些电压和频率组合。当cpufreq执行频率切换时,则从opp表中选择一个最合适的频率电压值,然后通过opp接口,调用clk和regulator操作函数,完成实际的调频调压操作。综上所述,内核cpufreq模块的总体架构如下:
三、主要数据结构
3.1 cufreq_policy结构
cpufreq_policy结构包含了调频策略相关的频率范围等参数。它们包括其对应的调频策略、最大最小频率限制值,以及cpufreq qos参数等。以下为其结构体定义:
struct cpufreq_policy {
cpumask_var_t cpus;
cpumask_var_t related_cpus;
cpumask_var_t real_cpus; (1)
…
unsigned int cpu; (2)
struct clk *clk;
struct cpufreq_cpuinfo cpuinfo; (3)
unsigned int min;
unsigned int max;
unsigned int cur; (4)
unsigned int suspend_freq; (5)
unsigned int policy; (6)
unsigned int last_policy;
struct cpufreq_governor *governor; (7)
void *governor_data;
char last_governor[CPUFREQ_NAME_LEN];
struct work_struct update;
struct freq_constraints constraints;
struct freq_qos_request *min_freq_req;
struct freq_qos_request *max_freq_req; (8)
struct cpufreq_frequency_table *freq_table;
enum cpufreq_table_sorting freq_table_sorted; (9)
struct list_head policy_list; (10)
…
struct cpufreq_stats *stats; (11)
void *driver_data; (12)
struct thermal_cooling_device *cdev; (13)
struct notifier_block nb_min;
struct notifier_block nb_max; (14)
};
(1)cpus、related_cpus和real_cpus都用于表示使用该policy的cpu mask,它们的区别在于cpu的运行状态不同。其中cpus只包含当前处于online状态的cpu,related_cpus包含online和offline状态的cpu,而real_cpus则包含present状态的cpu
(2)cpufreq中每一种policy都通过一个cpu进行管理,它们可以是以上cpus掩码中的任意一个。由于内核支持cpu hotplug,因此若该cpu被下线以后,则需要重新选择一个cpu作为管理该policy的cpu
(3)cpuinfo结构体定义如下,它用于定义硬件支持的cpu最高、最低频率,以及切频的延迟时间。该频率值在系统运行过程中不会改变
struct cpufreq_cpuinfo {
unsigned int max_freq;
unsigned int min_freq;
unsigned int transition_latency;
}
(4)min、max表示当前cpufreq支持切换的最低和最高频率,该值可以与cpuinfo中规定的硬件频率范围不同,且不能超过该范围。当cpufreq qos改变频率范围时,这两个值就可能在系统运行过程中发生改变。而cur表示cpu的当前运行频率
(5)在suspend流程中需要被设置为该频率
(6)内核支持两种调频方式,一种是通过governor调频,它通过target_index或target回调执行调频操作。另一种是通过setpolicy回调调频,这时其调频策略将由policy的值确定,其取值可为 CPUFREQ_POLICY_POWERSAVE和CPUFREQ_POLICY_PERFORMANCE
(7)若使用governor调频方式,则该成员用于指定控制调频策略的governor
(8)这几个成员用于cpufreq qos特性,主要用于修改该policy支持的最低、最高频率
(9)freq_table用于保存该policy支持的所有频点,这些频点数据是在cpufreq驱动初始化时,从cpu对应的opp表中读取的。而freq_table_sorted用于指定这些频点的排序方式
(10)它用于将该policy挂到全局的policy链表中
(11)该结构体用于保存policy的统计数据
(12)用于保存驱动的私有数据
(13)由于cpu调频可以改变系统的功耗,因此thermal模块可通过调频方式为系统降温。这种方式下,cpufreq就作为thermal模块的降温设备
(14)这两个通知将被注册到qos的通知链中,当cpufreq的最大、最小频率发生改变时,qos模块将分别调用这两个通知
3.2 cpufreq_governor结构体
cpufreq governor的主要功能是根据其相应的控制策略,为cpu选择一个合适的运行频率,该流程实际上是由governor的start接口向调度子系统注册一个回调函数,并由调度子系统触发调频操作时执行的。
cpufreq_governor结构体主要提供了一组回调函数,用于控制特定governor的相关流程:
struct cpufreq_governor {
char name[CPUFREQ_NAME_LEN]; (1)
int (*init)(struct cpufreq_policy *policy); (2)
void (*exit)(struct cpufreq_policy *policy); (3)
int (*start)(struct cpufreq_policy *policy); (4)
void (*stop)(struct cpufreq_policy *policy); (5)
void (*limits)(struct cpufreq_policy *policy); (6)
ssize_t (*show_setspeed) (struct cpufreq_policy *policy,
char *buf);
int (*store_setspeed) (struct cpufreq_policy *policy,
unsigned int freq); (7)
struct list_head governor_list; (8)
struct module *owner;
u8 flags; (9)
}
(1)该governor的名字
(2)初始化一个governor时的回调函数
(3)退出一个governor时的回调函数
(4)启动一个governor的回调函数
(5)停止一个governor的回调函数
(6)若一个policy的最高或最低频率值发生改变,会调用本函数,用于判断cpu当前运行频率是否已不在新的频率范围之内。如果是的话,就将其当前频率设置到新频率范围之内
(7)show_setspeed和store_setspeed分别为sysfs用于读取和修改频率的接口
(8)用于将该governor挂到全局governor链表的节点
(9)用于保存该governor的标志
3.3 cpufreq_driver结构
cpufreq_governor会根据governor特定的策略,计算得到cpu待设置频率对应的频点,而频率设置流程需要cpufreq 驱动完成。cpufreq驱动主要包含一组回调函数,而特定驱动函数只需要实现这些回调函数即可。以下为其定义:
struct cpufreq_driver {
char name[CPUFREQ_NAME_LEN]; (1)
u16 flags;
void *driver_data;
int (*init)(struct cpufreq_policy *policy); (2)
int (*verify)(struct cpufreq_policy_data *policy); (3)
int (*setpolicy)(struct cpufreq_policy *policy); (4)
int (*target)(struct cpufreq_policy *policy,
unsigned int target_freq,
unsigned int relation);
int (*target_index)(struct cpufreq_policy *policy, (5)
unsigned int index);
unsigned int (*fast_switch)(struct cpufreq_policy *policy,
unsigned int target_freq); (6)
void (*adjust_perf)(unsigned int cpu,
unsigned long min_perf,
unsigned long target_perf,
unsigned long capacity);
unsigned int (*get_intermediate)(struct cpufreq_policy *policy,
unsigned int index);
int (*target_intermediate)(struct cpufreq_policy *policy,
unsigned int index); (7)
unsigned int (*get)(unsigned int cpu); (8)
…
}
(1)驱动名
(2)init回调将在cpufreq policy初始化时调用,用于执行特定驱动相关的初始化流程
(3)verify回调用于校验cpu频率是否满足要求,如是否位于硬件频率范围,以及是否位于频点表的范围内等
(4)当使用非governor调频方式时,使用该回调执行调频流程。它与后面的target和target_index回调是互斥的
(5)target和target_index都是使用governor调频方式时,执行调频流程的回调函数。其中target接口已经废弃,因此应该尽量使用target_index接口
(6)fast_switch也被用于调频操作,但该接口位于调度器上下文中,因此其不能睡眠。而target_index会启动一个worker,并通过worker执行实际的调频操作,因此可以在worker中睡眠。该接口不是必须实现的
(7)这两个回调主要用于那些在切频时需要一个稳定中间频率过渡的情形,此时可通过get_intermediate获取中间频率的值,然后通过target_intermediate先将频率切换到该频率,最后再将频率切换到实际需要设置的频率上
(8)用于获取该cpu当前实际的时钟频率
四、cpufreq设备注册流程
cpufreq设备属于platform设备,内核可以通过platform_device_register_data()函数注册一个平台设备。我们以cpufreq-dt设备为例,其注册流程如下(drivers/cpufreq/cpufreq-dt-platdev.c):
即soc名称只要位于allowlist中,或cpu节点包含operating-points-v2属性,且该soc名称不在blocklist中即可注册cpufreq-dt设备。
因此若需要为一个新平台添加cpufreq设备,只需将其soc名加到以下allowlist列表中:
static const struct of_device_id allowlist[] __initconst = {
{ .compatible = "allwinner,sun4i-a10", },
{ .compatible = "allwinner,sun5i-a10s", },
…
}
或者在dts中通过operating-points-v2属性为cpu正确配置频点,且不要将soc名将入如下所示的blocklist列表中即可:
static const struct of_device_id blocklist[] __initconst = {
{ .compatible = "allwinner,sun50i-h6", },
{ .compatible = "arm,vexpress", },
…
}
若该设备需要实现intermediate或suspend、resume回调,则可通过platform设备的data成员传递,该私有数据的格式如下:
struct cpufreq_dt_platform_data {
bool have_governor_per_policy;
unsigned int (*get_intermediate)(struct cpufreq_policy *policy,
unsigned int index);
int (*target_intermediate)(struct cpufreq_policy *policy,
unsigned int index);
int (*suspend)(struct cpufreq_policy *policy);
int (*resume)(struct cpufreq_policy *policy);
}
五、cpufreq驱动初始化
5.1 cufreq_dt驱动probe流程
驱动初始化流程同样以cpufreq-dt为例(drivers/cpufreq/ cpufreq-dt.c),其主要流程如下:
以下为其代码:
static struct platform_driver dt_cpufreq_platdrv = {
.driver = {
.name = "cpufreq-dt",
},
.probe = dt_cpufreq_probe,
.remove = dt_cpufreq_remove,
};
module_platform_driver(dt_cpufreq_platdrv);
...
static int dt_cpufreq_probe(struct platform_device *pdev)
{
struct cpufreq_dt_platform_data *data = dev_get_platdata(&pdev->dev);
int ret, cpu;
/* Request resources early so we can return in case of -EPROBE_DEFER */
for_each_possible_cpu(cpu) {
ret = dt_cpufreq_early_init(&pdev->dev, cpu);---------------------(1)
if (ret)
goto err;
}
if (data) {----------------------------------------------------------------(2)
if (data->have_governor_per_policy)
dt_cpufreq_driver.flags |= CPUFREQ_HAVE_GOVERNOR_PER_POLICY;
dt_cpufreq_driver.resume = data->resume;
if (data->suspend)
dt_cpufreq_driver.suspend = data->suspend;
if (data->get_intermediate) {
dt_cpufreq_driver.target_intermediate = data->target_intermediate;
dt_cpufreq_driver.get_intermediate = data->get_intermediate;
}
}
ret = cpufreq_register_driver(&dt_cpufreq_driver);-------------------------(3)
if (ret) {
dev_err(&pdev->dev, "failed register driver: %d\n", ret);
goto err;
}
return 0;
err:
dt_cpufreq_release();
return ret;
}
(1)对每个cpu执行初始化流程,它的主要功能为根据dts中各cpu的opp配置,为对应cpu初始化频点相关的数据。这些数据最终将被保存到cpufreq_policy 的freq_table成员中。
(2)若该设备包含私有数据,则解析这些数据,并将其设置到驱动结构体的相应成员中
(3)调用核心层接口注册dt-cpufreq驱动,其中dt_cpufreq_driver用于定义该驱动对应的参数配置,它主要包括一些回调函数的实现,以下为其定义:
static struct cpufreq_driver dt_cpufreq_driver = {
.flags = CPUFREQ_NEED_INITIAL_FREQ_CHECK |
CPUFREQ_IS_COOLING_DEV,
.verify = cpufreq_generic_frequency_table_verify,
.target_index = set_target,
.get = cpufreq_generic_get,
.init = cpufreq_init,
.exit = cpufreq_exit,
.online = cpufreq_online,
.offline = cpufreq_offline,
.name = "cpufreq-dt",
.attr = cpufreq_dt_attr,
.suspend = cpufreq_generic_suspend,
}
地平线板子上的系统:是使用scmi。硬件系统管理接口(System Control and Management Interface,SCMI)
源码路径:kernel/drivers/cpufreq/scmi-cpufreq.c
static const struct scmi_device_id scmi_id_table[] = {
{ SCMI_PROTOCOL_PERF, "cpufreq" },
{ },
};
MODULE_DEVICE_TABLE(scmi, scmi_id_table);
static struct scmi_driver scmi_cpufreq_drv = {
.name = "scmi-cpufreq",
.probe = scmi_cpufreq_probe,
.remove = scmi_cpufreq_remove,
.id_table = scmi_id_table,
};
module_scmi_driver(scmi_cpufreq_drv);
...
static struct cpufreq_driver scmi_cpufreq_driver = {
.name = "scmi",
.flags = CPUFREQ_STICKY | CPUFREQ_HAVE_GOVERNOR_PER_POLICY |
CPUFREQ_NEED_INITIAL_FREQ_CHECK |
CPUFREQ_IS_COOLING_DEV,
.verify = cpufreq_generic_frequency_table_verify,
.attr = cpufreq_generic_attr,
.target_index = scmi_cpufreq_set_target,
.fast_switch = scmi_cpufreq_fast_switch,
.get = scmi_cpufreq_get_rate,
.init = scmi_cpufreq_init,
.exit = scmi_cpufreq_exit,
};
5.2 subsys_interface机制介绍
内核中有些子系统,可能需要向该子系统中所有设备动态地添加或移除特定功能。为了支持该特性,内核抽象出了subsys_interface机制,该机制支持动态地向subsys注册或注销一个subsys_interface。其中注册流程如下:源码目录-kernel/drivers/base/bus.c
int subsys_interface_register(struct subsys_interface *sif)
{
struct bus_type *subsys;
struct subsys_dev_iter iter;
struct device *dev;
if (!sif || !sif->subsys)
return -ENODEV;
subsys = bus_get(sif->subsys);-----------------(1)
if (!subsys)
return -EINVAL;
mutex_lock(&subsys->p->mutex);
list_add_tail(&sif->node, &subsys->p->interfaces);------(2)
if (sif->add_dev) {
subsys_dev_iter_init(&iter, subsys, NULL, NULL);----(3)
while ((dev = subsys_dev_iter_next(&iter)))----------(4)
sif->add_dev(dev, sif);
subsys_dev_iter_exit(&iter);--------------------------(5)
}
mutex_unlock(&subsys->p->mutex);
return 0;
}
EXPORT_SYMBOL_GPL(subsys_interface_register);
它的原理很简单,就是遍历subsys上所有的设备,并分别对它们调用add_dev回调。以下为其详细分析:
(1)获取该接口对应的subsys
(2)将该接口加入到subsys的interface链表中
(3)初始化subsys的iter,开始遍历挂载该总线上的设备
(4)逐个遍历设备,并分别对每个设备执行add_dev回调
(5)设备遍历完成
其中add_dev回调的定义位于如下所示的subsys_interface结构体中:
struct subsys_interface {
const char *name;
struct bus_type *subsys;
struct list_head node;
int (*add_dev)(struct device *dev, struct subsys_interface *sif);
void (*remove_dev)(struct device *dev, struct subsys_interface *sif);
}
由该结构体可知,其主要包含了该interface所属的subsys,以及add_dev和remove_dev回调函数。
5.3 cpufreq_interface添加流程
由于cpufreq是cpu相关的子功能,因此它作为subsys_interface被添加到每个cpu节点中。其中该interface的定义如下:
//kernel/drivers/cpufreq/cpufreq.c
static struct subsys_interface cpufreq_interface = {
.name = "cpufreq",
.subsys = &cpu_subsys,
.add_dev = cpufreq_add_dev,
.remove_dev = cpufreq_remove_dev,
}
它将通过cpufreq_register_driver() --> subsys_interface_register()被注册到系统中。由上一节可知,该流程主要是对每个cpu分别执行cpufreq_add_dev函数。该函数主要为每个cpu初始化相关的cpufreq_policy结构体,以及为其创建cpufreq相关的sysfs属性文件。其代码实现如下:
static int cpufreq_add_dev(struct device *dev, struct subsys_interface *sif)
{
struct cpufreq_policy *policy;
unsigned cpu = dev->id;
int ret;
dev_dbg(dev, "%s: adding CPU%u\n", __func__, cpu);
if (cpu_online(cpu)) {-----------------------------(1)
ret = cpufreq_online(cpu);
if (ret)
return ret;
}
/* Create sysfs link on CPU registration */
policy = per_cpu(cpufreq_cpu_data, cpu);
if (policy)
add_cpu_dev_symlink(policy, cpu, dev);--------(2)
return 0;
}
(1)若该cpu当前处于online状态,则调用cpufreq_online将该cpu的cpufreq模块初始化为online状态。其主要流程是初始化policy结构体与调用驱动初始化相关的回调函数
(2)为该cpu对应的policy创建sysfs文件
接下来我们重点看一下cpufreq_online的流程:
static int cpufreq_online(unsigned int cpu)
{
…
policy = per_cpu(cpufreq_cpu_data, cpu);
if (policy) {
if (!policy_is_inactive(policy))
return cpufreq_add_policy_cpu(policy, cpu);
new_policy = false;
down_write(&policy->rwsem);
policy->cpu = cpu;
policy->governor = NULL;
up_write(&policy->rwsem);
} else {
new_policy = true;
policy = cpufreq_policy_alloc(cpu); (1)
…
}
if (!new_policy && cpufreq_driver->online) {
ret = cpufreq_driver->online(policy);
…
cpumask_copy(policy->cpus, policy->related_cpus);
} else {
cpumask_copy(policy->cpus, cpumask_of(cpu)); (2)
ret = cpufreq_driver->init(policy); (3)
…
ret = cpufreq_table_validate_and_sort(policy); (4)
…
cpumask_copy(policy->related_cpus, policy->cpus); (5)
}
down_write(&policy->rwsem);
cpumask_and(policy->cpus, policy->cpus, cpu_online_mask);
if (new_policy) {
for_each_cpu(j, policy->related_cpus) {
per_cpu(cpufreq_cpu_data, j) = policy;
add_cpu_dev_symlink(policy, j); (6)
}
policy->min_freq_req = kzalloc(2 * sizeof(*policy->min_freq_req),
GFP_KERNEL);
…
ret = freq_qos_add_request(&policy->constraints,
policy->min_freq_req, FREQ_QOS_MIN,
policy->min);
…
policy->max_freq_req = policy->min_freq_req + 1;
ret = freq_qos_add_request(&policy->constraints,
policy->max_freq_req, FREQ_QOS_MAX,
policy->max); (7)
…
blocking_notifier_call_chain(&cpufreq_policy_notifier_list,
CPUFREQ_CREATE_POLICY, policy); (8)
}
if (cpufreq_driver->get && has_target()) {
policy->cur = cpufreq_driver->get(policy->cpu); (9)
…
}
if ((cpufreq_driver->flags & CPUFREQ_NEED_INITIAL_FREQ_CHECK)
&& has_target()) {
…
ret = cpufreq_frequency_table_get_index(policy, old_freq);
if (ret == -EINVAL) {
ret = __cpufreq_driver_target(policy, old_freq - 1,
CPUFREQ_RELATION_L); (10)
}
}
if (new_policy) {
ret = cpufreq_add_dev_interface(policy); (11)
…
cpufreq_stats_create_table(policy); (12)
write_lock_irqsave(&cpufreq_driver_lock, flags);
list_add(&policy->policy_list, &cpufreq_policy_list); (13)
write_unlock_irqrestore(&cpufreq_driver_lock, flags);
}
ret = cpufreq_init_policy(policy); (14)
…
kobject_uevent(&policy->kobj, KOBJ_ADD); (15)
if (cpufreq_driver->ready)
cpufreq_driver->ready(policy);
if (cpufreq_thermal_control_enabled(cpufreq_driver))
policy->cdev = of_cpufreq_cooling_register(policy); (16)
…
}
它主要分为以下几种情况:
(a)若policy已经被创建,则该policy当前处于online状态,则将该cpu添加到policy的cpu mask中即可
(b)若该policy已被创建,但policy本身处于offline状态,则表明其cpu mask当前不包含任何cpu。因此可将当前cpu设置为该policy的管理cpu,并为其设置governor
(c)若该policy未被创建,则需要为其分配一个新的policy结构体,并执行完整的policy初始化流程
接下来将主要看一下新分配policy的初始化流程:
(1)分配一个policy结构体
(2)将该cpu设置到policy的cpu mask中
(3)调用特定驱动的初始化接口,如在cufreq-dt驱动中,该接口会向policy设置动私有数据、cpu时钟指针、该cpu的频点列表等信息。其相应源码如下:
static int cpufreq_init(struct cpufreq_policy *policy)
{
…
priv = cpufreq_dt_find_data(policy->cpu);
…
cpu_dev = priv->cpu_dev;
cpu_clk = clk_get(cpu_dev, NULL);
…
transition_latency = dev_pm_opp_get_max_transition_latency(cpu_dev);
if (!transition_latency)
transition_latency = CPUFREQ_ETERNAL;
cpumask_copy(policy->cpus, priv->cpus);
policy->driver_data = priv;
policy->clk = cpu_clk;
policy->freq_table = priv->freq_table;
policy->suspend_freq = dev_pm_opp_get_suspend_opp_freq(cpu_dev) / 1000;
policy->cpuinfo.transition_latency = transition_latency;
policy->dvfs_possible_from_any_cpu = true;
if (policy_has_boost_freq(policy)) {
ret = cpufreq_enable_boost_support();
…
cpufreq_dt_attr[1] = &cpufreq_freq_attr_scaling_boost_freqs;
}
dev_pm_opp_of_register_em(cpu_dev, policy->cpus);
…
}
(4)由于已经为该policy设置频点列表,因此可根据频点设置计算该policy的调频范围,以及频点表的排序方式
(5)初始化policy的related_cpus掩码,它包括使用该policy的online和offine cpu
(6)在sysfs的cpu目录下为其创建cpufreq目录
(7)分别向qos模块发送添加cpufreq qos最小频率和最大频率的请求
(8)向关心cpufreq policy创建的模块发送相关通知
(9)读取当前cpu的实际运行频率
(10)若当前cpu并未运行在opp设置的任一频点上,则将cpu切换到与当前频率最接近的频点上
(11)在cpu的cpufreq目录下创建sysfs属性文件,如scaling_driver 、cpuinfo_cur_freq 、scaling_cur_freq等
(12)初始化该policy的统计数据,其统计数据可包括如下内容:
如time_in_state表示cpu在各个频点总的运行时间,total_trans表示总的切频次数,而trans_table则更加详细地列出了不同频点间切换的次数,其示例如下:
从上面数据,可以看出,稳定运行在 CPU max MHz: 2000.0000;并没有进行切换。
(13)将该policy添加到全局的policy链表中
目前地平线的板子上:2个策略
(14)为该policy设置合适的governor,并且调用governor的启动函数。该函数执行完成后,cpufreq governor将开始执行调频操作
(15)向用户空间发送uevent事件
(16)若使能了cpufreq的thermal控制特性,则将其注册为cooling设备
六、cpufreq governor
cpufreq governor主要功能是根据特定的governor算法,计算cpu待切换频率对应的频点。当前内核支持performance、powersave、userspace、ondemand、conservative和schedutil六种governor。
其中前三种方式非常简单,如performance governor直接将cpu频率设置为最高值,powersave governor将频率设置为最低值,而userspace governor则将其设置为用户空间传入的频率值。目前地平线板子上的策略是:
其它三种governor都会根据负载变化,动态调节cpu的频率,其特点分别如下:
(1)ondemand:当负载超过80%时,会将cpu的频率调到最高。否则就会按负载比例调节频率。它的调频方式比较激进,如负载升高时能快速升频,而频率负载降低时也会较快的降频。因此其可能导致cpu频繁进行升降频操作,造成性能抖动
(2)conservative:这种方式相对于ondemand,其调频方式更加柔和。它当负载超过80%时会以5%的幅度升频,而只有当负载低于20%时,才会以5%的幅度降频。因此其调频幅度较平滑,且降频时留有足够的缓冲,cpu不会频繁执行升降频操作。但它也使得cpu频率对负载的跟踪不够及时,特别是降频流程比较保守,从而可能会影响节电效果
(3)schedutil:以上两种方式中,负载都是由cpufreq模块自己计算的,它计算的主要依据为cpu idle时间。而实际上cpu在不同时间段idle时间,对当前负载的贡献是不同的,如1s前的负载显然比10s前负载贡献更大。
而实际上调度器本身就会跟踪cpu的负载,而且其负载跟踪算法(pelt)更加准确。因此若将调度器的负载值用作cpu调频依据,则不仅使cpu调频更加准确,而且还减少了cpufreq governor的负载计算流程,提高调频效率。schedutil就是使用这种方式实现调频的。
6.1 governor初始化流程
除了与具体governor无关的通用结构体cpufreq_governor之外,cpufreq还定义了一个与特定governor相关的结构体dbs_governor,每个governor可以通过该结构体实现其自身的回调函数以及保存其相关的私有数据。其定义如下:
struct dbs_governor {
struct cpufreq_governor gov;
struct kobj_type kobj_type;
struct dbs_data *gdbs_data;
unsigned int (*gov_dbs_update)(struct cpufreq_policy *policy);
struct policy_dbs_info *(*alloc)(void);
void (*free)(struct policy_dbs_info *policy_dbs);
int (*init)(struct dbs_data *dbs_data);
void (*exit)(struct dbs_data *dbs_data);
void (*start)(struct cpufreq_policy *policy);
}
如对于ondemand类型的governor,可按如下方式初始化:
//kernel/drivers/cpufreq/cpufreq_ondemand.c
static struct dbs_governor od_dbs_gov = {
.gov = CPUFREQ_DBS_GOVERNOR_INITIALIZER("ondemand"),
.kobj_type = { .default_attrs = od_attributes },
.gov_dbs_update = od_dbs_update,
.alloc = od_alloc,
.free = od_free,
.init = od_init,
.exit = od_exit,
.start = od_start,
};
其中CPUFREQ_DBS_GOVERNOR_INITIALIZER用于初始化通用governor结构体的相关成员,其实现如下:
#define CPUFREQ_DBS_GOVERNOR_INITIALIZER(_name_) \
{ \
.name = _name_, \
.flags = CPUFREQ_GOV_DYNAMIC_SWITCHING, \
.owner = THIS_MODULE, \
.init = cpufreq_dbs_governor_init, \
.exit = cpufreq_dbs_governor_exit, \
.start = cpufreq_dbs_governor_start, \
.stop = cpufreq_dbs_governor_stop, \
.limits = cpufreq_dbs_governor_limits, \
}
接下来我们以ondemand governor为例,看一下其初始化流程:
由于governor的调频操作将由调度子系统触发,为了防止在调频过程中引起睡眠,从而影响调度器的正常工作。故governor的实际调频流程,将通过工作队列执行。为此,在governor初始化流程中,需要为其初始化相应的工作队列。此外,还需要为其初始化一些其它相关参数,如调频周期,governor相关属性等。
以下为governor初始化主流程cpufreq_dbs_governor_init的代码实现:
//kernel/drivers/cpufreq/cpufreq_governor.c
int cpufreq_dbs_governor_init(struct cpufreq_policy *policy)
{
struct dbs_governor *gov = dbs_governor_of(policy);
struct dbs_data *dbs_data;
struct policy_dbs_info *policy_dbs;
int ret = 0;
/* State should be equivalent to EXIT */
if (policy->governor_data)
return -EBUSY;
policy_dbs = alloc_policy_dbs_info(policy, gov);------------------(1)
if (!policy_dbs)
return -ENOMEM;
/* Protect gov->gdbs_data against concurrent updates. */
mutex_lock(&gov_dbs_data_mutex);
dbs_data = gov->gdbs_data;
if (dbs_data) {
if (WARN_ON(have_governor_per_policy())) {
ret = -EINVAL;
goto free_policy_dbs_info;
}
policy_dbs->dbs_data = dbs_data;
policy->governor_data = policy_dbs;------------------------(2)
gov_attr_set_get(&dbs_data->attr_set, &policy_dbs->list);
goto out;
}
dbs_data = kzalloc(sizeof(*dbs_data), GFP_KERNEL);-------------------(3)
if (!dbs_data) {
ret = -ENOMEM;
goto free_policy_dbs_info;
}
gov_attr_set_init(&dbs_data->attr_set, &policy_dbs->list);
ret = gov->init(dbs_data);---------------------------------------------(4)
if (ret)
goto free_policy_dbs_info;
/*
* The sampling interval should not be less than the transition latency
* of the CPU and it also cannot be too small for dbs_update() to work
* correctly.
*/
dbs_data->sampling_rate = max_t(unsigned int,
CPUFREQ_DBS_MIN_SAMPLING_INTERVAL,
cpufreq_policy_transition_delay_us(policy));(5)
if (!have_governor_per_policy())
gov->gdbs_data = dbs_data;
policy_dbs->dbs_data = dbs_data;
policy->governor_data = policy_dbs;
gov->kobj_type.sysfs_ops = &governor_sysfs_ops;
ret = kobject_init_and_add(&dbs_data->attr_set.kobj, &gov->kobj_type,
get_governor_parent_kobj(policy),
"%s", gov->gov.name);--------------------------(6)
if (!ret)
goto out;
/* Failure, so roll back. */
pr_err("initialization failed (dbs_data kobject init error %d)\n", ret);
kobject_put(&dbs_data->attr_set.kobj);
policy->governor_data = NULL;
if (!have_governor_per_policy())
gov->gdbs_data = NULL;
gov->exit(dbs_data);
kfree(dbs_data);
free_policy_dbs_info:
free_policy_dbs_info(policy_dbs, gov);
out:
mutex_unlock(&gov_dbs_data_mutex);
return ret;
}
EXPORT_SYMBOL_GPL(cpufreq_dbs_governor_init);
(1)分配并初始化policy_dbs结构体,其主要功能为初始化相关的工作队列。以下为其代码实现:
static struct policy_dbs_info *alloc_policy_dbs_info(struct cpufreq_policy *policy,
struct dbs_governor *gov)
{
…
policy_dbs = gov->alloc(); (a)
…
policy_dbs->policy = policy;
mutex_init(&policy_dbs->update_mutex);
atomic_set(&policy_dbs->work_count, 0);
init_irq_work(&policy_dbs->irq_work, dbs_irq_work);
INIT_WORK(&policy_dbs->work, dbs_work_handler); (b)
for_each_cpu(j, policy->related_cpus) {
struct cpu_dbs_info *j_cdbs = &per_cpu(cpu_dbs, j);
j_cdbs->policy_dbs = policy_dbs; (c)
}
return policy_dbs;
}
(a)调用特定governor的alloc回调函数,分配一个policy_dbs结构体
(b)初始化两个工作队列,其中irq_work可以由中断上下文调用,而work可以由进程上下文调用
(c)将policy_dbs结构体保存到每个使用该policy的cpu相关的全局变量中
(2)如governor的dbs_data已分配,则将其保存到policy的相应成员中
(3)若governor的dbs_data未分配,则分配该结构体,并初始化相关的成员
(4)调用特定governor的init回调函数,如对于ondemand类型governor,则为od_init
(5)计算调频周期
(6)添加governor相关的sysfs属性
在governor被初始化完成之后,就可以调用启动流程使其开始执行调频操作。
6.2 governor启动流程
governor启动的主要工作包括初始化负载计算相关参数,以及将调频相关的回调函数注册到调度子系统中。以下为governor启动的主要流程:
其中cpufreq_dbs_governor_start的实现如下:
int cpufreq_dbs_governor_start(struct cpufreq_policy *policy)
{
…
policy_dbs->is_shared = policy_is_shared(policy);
policy_dbs->rate_mult = 1;
sampling_rate = dbs_data->sampling_rate;
ignore_nice = dbs_data->ignore_nice_load;
io_busy = dbs_data->io_is_busy;
for_each_cpu(j, policy->cpus) { (1)
struct cpu_dbs_info *j_cdbs = &per_cpu(cpu_dbs, j);
j_cdbs->prev_cpu_idle = get_cpu_idle_time(j, &j_cdbs->prev_update_time, io_busy);
j_cdbs->prev_load = 0;
if (ignore_nice)
j_cdbs->prev_cpu_nice = kcpustat_field(&kcpustat_cpu(j), CPUTIME_NICE, j);
}
gov->start(policy); (2)
gov_set_update_util(policy_dbs, sampling_rate); (3)
return 0;
}
(1)为该policy对应的cpu分别计算负载计算相关的参数,如prev的cpu idle时间,prev的负载值等。它们将在下一次调频操作被触发后,用于辅助计算cpu的负载
(2)调用特定governor对应的启动回调函数,如对于ondemand类型,则该函数为od_start
(3)将该policy的回调函数注册到调度子系统中,它将在governor启动后由调度子系统触发,实现调频操作。
其中gov_set_update_util的实现如下:
static void gov_set_update_util(struct policy_dbs_info *policy_dbs,
unsigned int delay_us)
{
struct cpufreq_policy *policy = policy_dbs->policy;
int cpu;
gov_update_sample_delay(policy_dbs, delay_us); (1)
policy_dbs->last_sample_time = 0;
for_each_cpu(cpu, policy->cpus) {
struct cpu_dbs_info *cdbs = &per_cpu(cpu_dbs, cpu);
cpufreq_add_update_util_hook(cpu, &cdbs->update_util,
dbs_update_util_handler); (2)
}
}
(1)设置调频周期
(2)向调度子系统设置实际的调频回调函数,该回调函数为dbs_update_util_handler
void cpufreq_add_update_util_hook(int cpu, struct update_util_data *data,
void (*func)(struct update_util_data *data, u64 time,
unsigned int flags))
{
…
data->func = func; (1)
rcu_assign_pointer(per_cpu(cpufreq_update_util_data, cpu), data); (2)
}
(1)设置回调函数
(2)将hook信息设置到percpu的全局变量cpufreq_update_util_data中
该hook设置完成后,调度子系统将会通过cpufreq_update_util()调用该回调,其实现如下:
static inline void cpufreq_update_util(struct rq *rq, unsigned int flags)
{
struct update_util_data *data;
data = rcu_dereference_sched(*per_cpu_ptr(&cpufreq_update_util_data,
cpu_of(rq))); (1)
if (data)
data->func(data, rq_clock(rq), flags); (2)
}
(1)获取该cpu对应的cpufreq_update_util_data值
(2)调用该其对应的回调函数
6.3 governor调频触发流程
governor的调频操作由调度子系统触发,如cfs调度器在向cfs就绪队列中添加进程时,就会触发该流程。以下为ondemand governor下,cfs调度器在进程入队时触发调频操作的流程图:
它会通过cpufreq_update_util函数调用cpu调频的回调函数dbs_update_util_handler,该函数会通过工作队列处理实际的调频流程。其相关的处理函数为dbs_work_handler,它最终会调用特定governor的gov_dbs_update回调,执行实际的调频流程。