电子说
之前章节介绍的电源管理都都直接下电,不用电当然能节能,但是还有比较温柔的方法就是通过调节电压频率。比如经常的一个说法:CPU太热了跑不动了,快给降频下。频率就干活的速度,干活太快,CPU都要烧了,太热了,费电啊。但是在户外的设备,环境温度过高还是要考虑遮阳、通风来应对,但降频也可以降低温度,但是会引起卡顿啊。
一般电压和频率是成对出现的,也叫OPP(Operating Performance Points),对其进行调节也叫DVFS(Dynamic Voltage and Frequency Scaling),下面就来揭开这些技术的神秘面纱。
DVFS(Dynamic Voltage and Frequency Scaling)即动态电压频率调整。这项技术可以根据芯片运行的应用程序的计算需求制定策略,动态调整电压和频率:
DVFS技术利用了CMOS芯片的特性:CMOS芯片的能量消耗正比于电压的平方和时钟频率:
CPUFreq系统流程:
DVFS调节策略 一味的降频降压当然是不能降低功耗的,因为低频下运行可能使系统处理任务的时长增加,从而整体上可能增加了功耗。所以DVFS的核心是动态调整的策略,其目的是根据当时的系统负载实时调整,从而提供满足当时性能要求的最低功率,也就达到了最低功耗。
需要统计出这些模块的负载情况,基本的策略当然是工作负载增加则升频升压,工作负载降低则降频降压。工作负载的粗略模型是在一个时间窗口内,统计模块工作的时间长度,设定不同阈值,高阈值对应高电压高频率,低阈值对应低电压低频率。每次统计值穿过阈值边界,触发DVFS转换。
> 在调整频率和电压时,要特别注意调整的顺序: > - 当频率由高到低调整时,应该先降频率,再降电压; > - 相反,当升高频率时,应该先升电压,再升频率。
内核目前有一套完整的代码支持DVFS,具体可参考内核下drivers/cpufreq/。
cpufreq governor:负责调频调压的各种策略,每种governor计算频率的方式不同,根据提供的频率范围和参数(阈值等),计算合适的频率。
cpufreq driver:负责平台相关的调频调压机制的实现,基于cpu subsystem driver、OPP、clock driver、regulator driver等模块,提供对CPU频率和电压的控制。kernel中实现了比较通用的驱动模块cpufreq-dt.c
cpufreq stats:负责调频信息和各频点运行时间等统计,提供每个cpu的cpufreq有关的统计信息。
cpufreq相关驱动模块加载后,会在各cpu下创建:/sys/devices/system/cpu/cpuX/cpufreq接口
这是一个软链接:cpufreq -> ../cpufreq/policy0
前缀是scaling的属性文件表示软件可调节的几种属性,前缀是cpuinfo的属性文件表示硬件支持的几种属性。cpuinfo是scaling的子集,因为软件设置范围在硬件支持范围内。 scaling_governor 可以手动修改设置:
echo ondemand > /sys/devices/system/cpu/cpu0/scaling_governor
一般系统启动默认为performance,支持5种模式,可以通过make menuconfig配置。
目前DVFS支持调频调压策略主要就是上面支持的5种:
功耗:performance > ondemand > conservative >powersave
在include/linux/cpufreq.h中,用于描述cpufreq的驱动,是驱动工程师最关注的结构。如下默认值:
static struct cpufreq_driver dt_cpufreq_driver = {
.flags = CPUFREQ_STICKY | CPUFREQ_NEED_INITIAL_FREQ_CHECK,
.verify = cpufreq_generic_frequency_table_verify,
.target_index = set_target,
.get = cpufreq_generic_get,
.init = cpufreq_init,
.exit = cpufreq_exit,
.ready = cpufreq_ready,
.name = "cpufreq-dt",
.attr = cpufreq_dt_attr,
.suspend = cpufreq_generic_suspend,
};
有关struct cpufreq_driver的API包括:
1: int cpufreq_register_driver(struct cpufreq_driver *driver_data);
2: int cpufreq_unregister_driver(struct cpufreq_driver *driver_data);
3:
4: const char *cpufreq_get_current_driver(void);
5: void *cpufreq_get_driver_data(void);
分别为driver的注册、注销。获取当前所使用的driver名称,以及该driver的私有数据结构(driver_data字段)。
linux使用cpufreq policy来抽象cpu设备的调频调压功能,用于描述不同的policy,包含频率表、cpuinfo等各种信息,并且每个policy都会对应某个具体的governor。
min/max frequency,调频范围,对于可以自动调频的CPU而言,只需要这两个参数就够了。 current frequency和governor,对于不能自动调频的CPU,需要governor设置具体的频率值。下面介绍一下governor。 struct cpufreq_policy不会直接对外提供API。
不同policy的管理策略,根据使用场景的不同,会有不同的调频调压策略。如下一个governor的默认值:
static struct cpufreq_governor cpufreq_gov_userspace = {
.name = "userspace",
.init = cpufreq_userspace_policy_init,
.exit = cpufreq_userspace_policy_exit,
.start = cpufreq_userspace_policy_start,
.stop = cpufreq_userspace_policy_stop,
.limits = cpufreq_userspace_policy_limits,
.store_setspeed = cpufreq_set,
.show_setspeed = show_speed,
.owner = THIS_MODULE,
};
cpufreq_register_governor 如果policy中有默认的governor,则调用find_governor,在列表中寻找。cpufreq core定义了一个全局链表变量:cpufreq_governor_list,注册函数首先根据governor的名称,通过__find_governor()函数查找该governor是否已经被注册过,如果没有被注册过,则把代表该governor的结构体添加到cpufreq_governor_list链表中。
系统中可以同时存在多个governor,policy通过cpufreq_policy->governor指针和某个governor相关联。要想一个governor能够被使用,首先要把该governor注册到cpufreq framework中。例如:
static int __init cpufreq_gov_userspace_init(void)
{
return cpufreq_register_governor(&cpufreq_gov_userspace);
}
#ifdef CONFIG_CPU_FREQ_DEFAULT_GOV_USERSPACE
struct cpufreq_governor *cpufreq_default_governor(void)
{
return &cpufreq_gov_userspace;
}
fs_initcall(cpufreq_gov_userspace_init);
注册的gov定义为:
static struct cpufreq_governor cpufreq_gov_userspace = {
.name = "userspace",
.init = cpufreq_userspace_policy_init,
.exit = cpufreq_userspace_policy_exit,
.start = cpufreq_userspace_policy_start,
.stop = cpufreq_userspace_policy_stop,
.limits = cpufreq_userspace_policy_limits,
.store_setspeed = cpufreq_set,
.show_setspeed = show_speed,
.owner = THIS_MODULE,
};
dt_cpufreq_probe()在drivers/cpufreq/cpufreq-dt.c中 系统启动的时候平台驱动dt_cpufreq_platdrv,会执行prob函数dt_cpufreq_probe()
static struct cpufreq_driver dt_cpufreq_driver = {
.flags = CPUFREQ_STICKY | CPUFREQ_NEED_INITIAL_FREQ_CHECK,
.verify = cpufreq_generic_frequency_table_verify,
.target_index = set_target,
.get = cpufreq_generic_get,
.init = cpufreq_init,
.exit = cpufreq_exit,
.ready = cpufreq_ready,
.name = "cpufreq-dt",
.attr = cpufreq_dt_attr,
.suspend = cpufreq_generic_suspend,
};
cpufreq_register_driver(&dt_cpufreq_driver),在drivers/cpufreq/cpufreq.c中 cpufreq_register_driver为cpufreqdriver注册的入口,驱动程序通过调用该函数进行初始化,并传入相关的struct cpufreq_driver,cpufreq_register_driver会调用subsys_interface_register,入参为:
static struct subsys_interface cpufreq_interface = {
.name = "cpufreq",
.subsys = &cpu_subsys,
.add_dev = cpufreq_add_dev,
.remove_dev = cpufreq_remove_dev,
};
最终执行回调函数cpufreq_add_dev。
kernel将cpu都抽象成device,并抽象出cpu_subsys bus,所有cpu都挂载在这个bus下。每个bus都包含一个struct subsys_private结构的成员p,该结构包括一个interface list成员interfaces和设备链表klist_devices。interface list上的一个interface通常用于抽象bus下的一个功能。
cpufreq是CPU device的一类特定功能,也就被抽象为一个subsys interface(kernel使用struct subsys_interface结构表示)即变量cpufreq_interface,, 挂载在interface list下。cpufreq作为一个功能挂载到cpu subsys下后会对相应的所有设备即cpu执行interface.add_dev()操作,表示对subsys_private支持的设备都添加这个功能,在添加这个功能时为每个cpu设备生成具体的policy结构,即struct cpufreq_policy.
上图涉及cpu初始化,在系统启动的时候:
//drivers/base/cpu.c
register_cpu
cpu->dev.bus = &cpu_subsys;
device_register
device_add
bus_add_device
error = device_add_groups(dev, bus->dev_groups);//向总线注册设备
klist_add_tail(&dev->p->knode_bus, &bus->p->klist_devices);//向subsys_private
回到cpureq流程中,subsys_interface_register(),在drivers/base/bus.c中
mutex_lock(&subsys->p->mutex);
list_add_tail(&sif->node, &subsys->p->interfaces);
if (sif->add_dev) {
subsys_dev_iter_init(&iter, subsys, NULL, NULL);
while ((dev = subsys_dev_iter_next(&iter)))
sif->add_dev(dev, sif);
subsys_dev_iter_exit(&iter);
}
mutex_unlock(&subsys->p->mutex);
这里可以看到对于多核,都执行了cpufreq_add_dev,会为cpu device创建struct cpufreq_policy结构。 cpufreq_add_dev(),在drivers/cpufreq/cpufreq.c中
static int cpufreq_add_dev(struct device *dev, struct subsys_interface *sif)
{
struct cpufreq_policy *policy;
unsigned cpu = dev->id;
int ret;
if (cpu_online(cpu)) {
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);
return 0;
}
cpufreq_online(cpu)在drivers/cpufreq/cpufreq.c中
•cpufreq_policy_alloc()创建policy节点/sys/devices/system/cpu/cpufreq/*
•cpufreq_driver->init(policy)指向cpufreq_init()
•cpufreq_add_dev_interface()创建sysfs节点的一些可选属性
•cpufreq_init_policy()初始化policy的governor
cpufreq_driver->init对应cpufreq_init()函数 这个函数会解析cpu信息得到cpu_dev、cpu_clk、opp_table等
cpu_dev = get_cpu_device(policy->cpu);
cpu_clk = clk_get(cpu_dev, NULL);
ret = dev_pm_opp_of_get_sharing_cpus(cpu_dev, policy->cpus);//多CPU共享
opp_table = dev_pm_opp_set_regulators(cpu_dev, &name, 1);
priv->reg_name = name;
priv->opp_table = opp_table;
priv->cpu_dev = cpu_dev;
policy->driver_data = priv;
policy->clk = cpu_clk;
policy->suspend_freq = dev_pm_opp_get_suspend_opp_freq(cpu_dev) / 1000;
ret = cpufreq_table_validate_and_show(policy, freq_table);
cpufreq_table_validate_and_show()里面找到CPU支持的最大和最小频率
int cpufreq_frequency_table_cpuinfo(struct cpufreq_policy *policy,
struct cpufreq_frequency_table *table)
{
struct cpufreq_frequency_table *pos;
unsigned int min_freq = ~0;
unsigned int max_freq = 0;
unsigned int freq;
cpufreq_for_each_valid_entry(pos, table) {
freq = pos->frequency;
if (!cpufreq_boost_enabled()
&& (pos->flags & CPUFREQ_BOOST_FREQ))
continue;
pr_debug("table entry %u: %u kHz
", (int)(pos - table), freq);
if (freq < min_freq)
min_freq = freq;
if (freq > max_freq)
max_freq = freq;
}
policy->min = policy->cpuinfo.min_freq = min_freq;
policy->max = policy->cpuinfo.max_freq = max_freq;
if (policy->min == ~0)
return -EINVAL;
else
return 0;
}
设置policy的时候,会读取cpu的频率表,赋值给policy->min和policy->max。另外各种governor也用到frequency table。
frequency table是CPU core可以正确运行的一组频率/电压组合,之所以存在的一个思考点是:table是频率和电压之间的一个一一对应的组合,因此cpufreq framework只需要关心频率,所有的策略都称做“调频”策略。而cpufreq driver可以在“调频”的同时,通过table取出和频率对应的电压,进行修改CPU core电压,实现“调压”的功能,这简化了设计。 例如在DTS中:
cpufreq_init_policy(),drivers/cpufreq/cpufreq.c在 使用默认策略初始化policy
/* Update governor of new_policy to the governor used before hotplug */
gov = find_governor(policy->last_governor);
if (gov) {
pr_info("dddd Restoring governor %s for cpu %d
",
policy->governor->name, policy->cpu);
} else {
gov = cpufreq_default_governor();
if (!gov)
return -ENODATA;
}
new_policy.governor = gov;
/* set default policy */
return cpufreq_set_policy(policy, &new_policy);
如果policy中有默认的governor,则调用find_governor,在列表中寻找。cpufreq core定义了一个全局链表变量:cpufreq_governor_list,注册函数首先根据governor的名称,通过__find_governor()函数查找该governor是否已经被注册过,如果没有被注册过,则把代表该governor的结构体添加到cpufreq_governor_list链表中。
系统中可以同时存在多个governor,policy通过cpufreq_policy->governor指针和某个governor相关联。要想一个governor能够被使用,首先要把该governor注册到cpufreq framework中。例如:
fs_initcall(cpufreq_gov_performance_init);
static int __init cpufreq_gov_performance_init(void)
{
return cpufreq_register_governor(&cpufreq_gov_performance);
}
这里我们默认使用default
#ifdef CONFIG_CPU_FREQ_DEFAULT_GOV_PERFORMANCE
struct cpufreq_governor *cpufreq_default_governor(void)
{
return &cpufreq_gov_performance;
}
#endif
static struct cpufreq_governor cpufreq_gov_performance = {
.name = "performance",
.owner = THIS_MODULE,
.limits = cpufreq_gov_performance_limits,
};
最后调用cpufreq_set_policy(policy, &new_policy);去设置policy
cpufreq_set_policy(),在drivers/cpufreq/cpufreq.c中
cpufreq_init_governor->policy->governor->init(policy);
cpufreq_start_governor->policy->governor->start(policy);
在governor初始化和启动的时候会发生:CPUFreq通知 CPUFreq子系统会发出通知的情况有两种:CPUFreq的策略变化或者CPU运行频率变化。
在策略变化的过程中,例如cpufreq_set_policy函数中,会发送3次通知:
在频率变化的过程中,例如__cpufreq_notify_transition函数中,会发送2次通知:
/* notification of the new policy */
blocking_notifier_call_chain(&cpufreq_policy_notifier_list,
CPUFREQ_NOTIFY, new_policy);
cpufreq_policy_notifier_list
cpufreq_register_notifier()函数注册这个链表
cpuhp_setup_state_nocalls_cpuslocked(): 参数说明:
__cpuhp_setup_state_cpuslocked(
CPUHP_AP_ONLINE_DYN, "cpufreq:online", false,
cpuhp_cpufreq_online,cpuhp_cpufreq_offline, false);
* __cpuhp_setup_state_cpuuslocked—设置热插拔计算机状态的回调函数
* @state:要设置的状态
* @invoke:如果为true,启动函数将被调用于cpu,cpu state >= @state
* @startup:启动回调函数
* @teardown: teardown回调函数
* @multi_instance:状态是为多个实例设置的,然后添加。
用户空间监控CPUFreq流程图
### 2.3.1 用户接口说明 userspace governor是一种用户可以自己手动调整自己cpu频率的governor,即在linux目录下:/sys/devices/system/cpu/cpu0/cpufreq/,有一个参数scaling_setspeed,是这个governor转有的,其他governor是不能对其进行读写操作的,只有这个governor才能这样做。
对应底层有处理函数,设置也有处理函数。
默认是Performance的策略,我们可以通过make menuconfig选择,如下:
保存后在.config中可以看到
CONFIG_CPU_FREQ_DEFAULT_GOV_USERSPACE=y
在代码里面搜索这个宏,drivers/cpufreq/cpufreq_userspace.c中:
#ifdef CONFIG_CPU_FREQ_DEFAULT_GOV_USERSPACE
struct cpufreq_governor *cpufreq_default_governor(void)
{
return &cpufreq_gov_userspace;
}
cpufreq_gov_userspace对应
static struct cpufreq_governor cpufreq_gov_userspace = {
.name = "userspace",
.init = cpufreq_userspace_policy_init,
.exit = cpufreq_userspace_policy_exit,
.start = cpufreq_userspace_policy_start,
.stop = cpufreq_userspace_policy_stop,
.limits = cpufreq_userspace_policy_limits,
.store_setspeed = cpufreq_set,
.show_setspeed = show_speed,
.owner = THIS_MODULE,
};
可以看到其中有init函数和start函数。 cpufreq_userspace_policy_init 申请一个governor_data
policy->governor_data = setspeed;
cpufreq_userspace_policy_start 设置policy的cur频率
*setspeed = policy->cur;
cpufreq_userspace_policy_limits 就是约束性检查,如果超过max或者小于min进行重新设定
show_setspeed 就是读scaling_setspeed-当前cpu频率
store_setspeed 就是写scaling_setspeed,可以用户控制。改变cpu频率的时候会调用如下函数:
ret = __cpufreq_driver_target(policy, freq, CPUFREQ_RELATION_L);
例如输入命令:
echo 700000 > /sys/devices/system/cpu/cpu0/cpufreq/scaling_setspeed
__cpufreq_driver_target->__target_index->cpufreq_driver->target_index
static int set_target(struct cpufreq_policy *policy, unsigned int index)
{
struct private_data *priv = policy->driver_data;
return dev_pm_opp_set_rate(priv->cpu_dev,
policy->freq_table[index].frequency * 1000);
}
dev_pm_opp_set_rate()函数在drivers/base/power/opp/core.c中定义 找到opp_table进行调频调压,opp_table的名字是/cpus/cpu0_opp_table
opp_table = _find_opp_table(dev);
clk = opp_table->clk;
freq = clk_round_rate(clk, target_freq);
if ((long)freq <= 0)
freq = target_freq;
old_freq = clk_get_rate(clk);
ret = _generic_set_opp_clk_only(dev, clk, old_freq, freq);
clk_set_rate(clk, freq);在drivers/clk/clk.c中定义
ret = clk_core_set_rate_nolock(clk->core, rate);
clk的名字是,rate是要设置的频率
/* change the rates */
clk_change_rate(top);
top的名字为cpu_core0_mux_clk,节点父子关系为:
armpll1_912m_cpu_clk->cpu_core0_mux_clk->cpu_core0_div_clk->cpu_core0_clk
首先设置armpll1_912m_cpu_clk
clk_change_rate()
core->ops->set_parent(core->hw, core->new_parent_index);
set_parent对应clk_mux_set_parent()函数在drivers/clk/clk-mux.c中
static int clk_mux_set_parent(struct clk_hw *hw, u8 index) { struct clk_mux *mux = to_clk_mux(hw);
val = clk_readl(mux->reg);
val &= ~(mux->mask << mux->shift);
val |= index << mux->shift;
clk_writel(val, mux->reg);
mux->reg值是0x42000020,index是4,clk_readl出来是默认值5,需要写入为4
cpu_core0_div_clk进行了频率设置
clk_change_rate
core->ops->set_rate(core->hw, core->new_rate, best_parent_rate);
const struct clk_ops clk_divider_ops = { .recalc_rate = clk_divider_recalc_rate, .round_rate = clk_divider_round_rate, .set_rate = clk_divider_set_rate, };
clk_divider_set_rate
static int clk_divider_set_rate(struct clk_hw *hw, unsigned long rate, unsigned long parent_rate) { value = divider_get_val(rate, parent_rate, divider->table, divider->width, divider->flags);
if (divider->flags & CLK_DIVIDER_HIWORD_MASK) {
val = div_mask(divider->width) << (divider->shift + 16);
} else {
val = clk_readl(divider->reg);
val &= ~(div_mask(divider->width) << divider->shift);
}
val |= (u32)value << divider->shift;
clk_writel(val, divider->reg);
其中divider->reg为0x42000020,value=0 divider->shift的值为8 clk_readl(divider->reg);读出来值为4已经是要设置的值了。
ondemand governor,最终是通过调频接口od_dbs_update实现计算负载进行调频的。
//drivers/cpufreq/cpufreq_ondemand.c
od_dbs_update
od_update
static void od_update(struct cpufreq_policy *policy)
{
unsigned int load = dbs_update(policy);//负载(百分比)(1)
/* Check for frequency increase */
if (load > dbs_data->up_threshold) {//(2)如果负载大于策略设置的阈值,则直接切换到最大频率
/* If switching to max speed, apply sampling_down_factor */
if (policy->cur < policy->max)
policy_dbs->rate_mult = dbs_data->sampling_down_factor;
dbs_freq_increase(policy, policy->max);
} else {
/* Calculate the next frequency proportional to load */
unsigned int freq_next, min_f, max_f;
min_f = policy->cpuinfo.min_freq;
max_f = policy->cpuinfo.max_freq;
freq_next = min_f + load * (max_f - min_f) / 100;
//(3)按照负载百分比,在频率范围内选择合适频率
/* No longer fully busy, reset rate_mult */
policy_dbs->rate_mult = 1;
if (od_tuners->powersave_bias)//(4)
freq_next = od_ops.powersave_bias_target(policy,
freq_next,
CPUFREQ_RELATION_L);
__cpufreq_driver_target(policy, freq_next, CPUFREQ_RELATION_C);//设置频率
}
(1)计算负载函数: od_dbs_update()核心方法是: 当前负载load = 100 * (time_elapsed - idle_time) / time_elapsed idle_time = 本次idle时间 - 上次idle时间 time_elapsed = 本次总运行时间 - 上次总运行时间 该函数返回使用此policy的各个cpu中的最大负载。
(2)当最大负载大于策略设置的最大阈值时,调用dbs_freq_increase()将频率设置在最大频率。
(3)按照负载百分比设置合适频率 freq_next = min_f + load * (max_f - min_f) / 100;
(4) 表明我们为了进一步节省电力,我们希望在计算出来的新频率的基础上,再乘以一个powersave_bias设定的百分比,作为真正的运行频率,powersave_bias的值从0-1000,每一步代表0.1%
不同的governor的触发调频调压流程不一样,这里以schedutil governor为例。 CFS负载变化的时候或者RT、DL任务状态更新的时候,就会启动调频。这几个scheduler类会调用cpufreq_update_util函数(前面注册进来的hook函数)触发schedutil工作。每个cpu最终会回调到sugov_upate_shared或者sugov_upate_single函数中的一个。 由于是从scheduler里直接调用下来的,最终执行调频切换时,无论是快速路径触发的简单写寄存器,还是慢速路径触发的kthread都不会占用过多时间或者调度开销。
Interactive 与Conservative相对,快速提升频率,缓慢降低频率
static struct interactive_governor interactive_gov = {
.gov = {
.name = "interactive",
.max_transition_latency = TRANSITION_LATENCY_LIMIT,
.owner = THIS_MODULE,
.init = cpufreq_interactive_init,
.exit = cpufreq_interactive_exit,
.start = cpufreq_interactive_start,
.stop = cpufreq_interactive_stop,
.limits = cpufreq_interactive_limits,
}
};
后记
本节代码有点多,不是调试这个可以不用关注代码,想深入学习还是需要运行起来代码打点log比较好。
全部0条评论
快来发表一下你的评论吧 !