文章说明:
Linux内核版本:5.0
架构:ARM64
参考资料及图片来源:《奔跑吧Linux内核》
Linux 5.0内核源码注释仓库地址:
1. 引言
对于睡眠或阻塞的进程,PELT还会继续计算其衰减负载,也就是继续为就绪队列贡献量化负载,但是这些继续贡献的负载对于下—次唤醒其实没有什么用处,因此会推延降低CPU频率的速度,从而增加CPU功耗。
针对上述在移动设备上的问题,可以使用新的计算进程负载的算法——窗口辅助负载追踪(Window-Assisted Load Tracking,WALT)算法,该算法已经被Android社区采纳,并在Android 7.x中被采用,但是官方的Linux内核并没有采纳。
在绿色节能调度器的发展过程中,为了把绿色节能调度器合并到Linux社区里,绿色节能调度器放弃使用 WALT 算法,而采用Linux内核的 PELT 算法。在Linux 5.0内核中,绿色节能调度器的大部分代码已经合并到Linux社区里。
现在Linux内核中关于电源管理的几个重要模块都比较独立,并没有完全协同工作,如CFS、CPUidle模块、CPUfreq 模块和针对大/小核设计的HMP调度器等模块都有各自的独有机制和策略.
ARM 和 Linaro 组织希望对以性能优先的调度策略、调度器、CPUidle 模块和CPUfreq 模块之间相对独立的现状做出改变,让它们可以紧密地工作在—起,从而进一步降低功耗并提升效率,这个改变叫作绿色节能调度(Energy Aware Scheduling,EAS),本文把对应的调度器称为绿色节能调度器,绿色节能调度器的设计目标是在保证系统性能的前提下尽可能地降低功耗。
绿色节能调度器由Linaro组织负责开发,本文采用绿色节能调度器核心开发人员Quentin Perret单独维护的内核版本进行讲解。
2. 量化计算能力
绿色节能调度器提出了两个概念,分别是频率恒定引擎(Frequency Invairant Engme,FIE) 和CPU恒定引擎(CPU Invariant Engine,CIE)。
- FIE:指在计算CPU负载时考虑CPU频率的变化
- CIE:指考虑不同CPU架构的计算能力对负载的影响,如在相同频率下,ARM的大/小核架构的CPU计算能力是不同的
为了体现FIE和CIE的概念,绿色节能调度器在rq数据结构中添加了cpu_capacity_orig
和 cpu_capacity
这两个成员:
struct rq {
...
// CPU 对应普通进程的量化计算能力,系统大约会预留最高计算能力的 80% 给普通进程,预留 20% 给实时进程,即 cpu_capacity
// 表示 CPU 在 CFS 调度类中的计算能力
unsigned long cpu_capacity;
// CPU 最高的量化计算能力(cpu_capacity_orig 表示该 CPU 原本的计算能力,在系统启动之初,建立系统调度域拓扑时
// 就会计算每个 CPU 的计算能力),指所有调度器的计算能力之和,如 realtime 调度类、deadline 调度类和 CFS 调度类
unsigned long cpu_capacity_orig;
...
}
在topology_normalize_cpu_scale()
函数中会设置每个CPU的量化计算能力:
void topology_set_cpu_scale(unsigned int cpu, unsigned long capacity)
{
per_cpu(cpu_scale, cpu) = capacity;
}
// 设置每个 CPU 的量化计算能力
void topology_normalize_cpu_scale(void)
{
...
for_each_possible_cpu(cpu) {
...
capacity = (raw_capacity[cpu] << SCHED_CAPACITY_SHIFT)
/ capacity_scale;
topology_set_cpu_scale(cpu, capacity);
...
}
...
}
其计算公式如下所示:
raw_capacity
是每个CPU原始的计算能力SCHED_CAPACITY_SHIFT
是量化系数,1 << SCHED_CAPACITY_SHIFT 等于 1024capacity_scale
就是系统中性能最高的处理器的计算能力
通常系统中性能最高的处理器的计算能力可以量化为1024。
3. 能效模型
绿色节能调度器是基于能效模型构建的。能效模型需要考虑CPU的计算能力(capacity)和功耗(energy)两方面的因素。在Linux内核中实现了一个能效模型软件层,为Linux内核调度器与Thermal模块提供CPU计算能力和功耗等数据信息,这样绿色节能调度器可以根据能效模型获得的信息来做出最佳的调度策略。能效模型的架构如下图所示:
绿色节能调度器使用性能域(Performance Domain,PD)来表述哪些CPU可以组成一个组来进行性能和功耗调整,如调整CPU频率等。通常在同—个性能域中的CPU必须拥有同样的架构,如采用相同的微处理器架构。Cortex-A53 和 Cortex-A73 就是不同微处理器架构的实现,它们的架构和计算能力都是不一样的。
性能域采用 em_perf_domain
数据结构描述:
// 用于描述性能域
struct em_perf_domain {
// table 指表述 CPU 频率和功耗之间关系的一个表
struct em_cap_state *table;
// 表示 CPU 有多少个频率点或者能力点
int nr_cap_states;
// cpus 表示这个性能域所包含的 CPU 位图
unsigned long cpus[0];
};
**性能操作点(OPP)**指SoC中某个电源域(powerdomam)的频率和电压的节点,OPP用em_cap_state
数据结构来描述,em_cap_state
数据结构用来描述CPU的频率和功耗之间的关系。
// 性能域容量状态
struct em_cap_state {
// CPU 的频率,单位为千赫兹
unsigned long frequency;
// 在该频率下的功耗,单位是毫瓦(mW)
unsigned long power;
// 该频率下的能效系数,通常 cost = power x max_frequency / frequency
unsigned long cost;
};
CPUfreq 模块驱动通常维护着一个频率和电压的对应表,每个表项({频率,工作电压})就是OPP。
对于ARM64处理器,我们通常使用一个基于设备树的CPUfreq驱动程序,即CPUfreq-dt驱动程序。在cpufreq_init()
初始化函数中会完成对OPP表和能效模型进行初始化,OPP和能效模型的初始化流程如下图所示:
注意dev_pm_opp_of_register_em()
函数在 Linux 5.1 内核版本之后合并到社区,接下来:
补充:
同一性能域上所有的CPU必须属于同一个处理器架构,不能混搭不同架构的CPU,所以需要遍历CPU位图中所有CPU,检查计算能力是否—致
em_create_pd()
em_create_pd()函数的核心是调用em_data_callback的回调函数active_power()来调取OPP子系统中的OPP表,计算功耗值和频率值
在绿色节能调度器里,我们不会直接读取OPP子系统中的OPP表,而是读取能效模型子系统的 em_cap_state表,em_create_pd() 函数相当于在 OPP 表和 em_cap_state 表之间做了一个转换
4. CPU的选择
与CFS相比,绿色节能调度器的重要改变是在唤醒进程时如何选择CPU,内核在唤醒进程时通常需要选择—个最合适的CPU,即wake_up_process()->try_to_wake_up()->select_task_rq()->select_task_rq_fair()
:
select_task_rq_fair()
:
// select_task_rq 方法在 CFS 调度类中的实现
// 参数 p 表示将要唤醒的进程
// 参数 prev_cpu 表示该进程上一次调度运行的 CPU
// 参数 sd_flag 表示调度域的标志位
// 参数 wake_flags 表示唤醒标志位
static int
select_task_rq_fair(struct task_struct *p, int prev_cpu, int sd_flag, int wake_flags)
{
...
// sched_energy_present 是一个全局变量,用于控制当前系统是否支持绿色节能调度器
if (static_branch_unlikely(&sched_energy_present)) {
// find_energy_efficient_cpu() 函数查找一个能效比最优的 CPU 来唤醒进程
new_cpu = find_energy_efficient_cpu(p, prev_cpu);
if (new_cpu >= 0)
return new_cpu;
new_cpu = prev_cpu;
}
...
}
select_task_rq_fair()->find_energy_efficient_cpu()
:
// 查找一个能效比最优的 CPU
static int find_energy_efficient_cpu(struct task_struct *p, int prev_cpu)
{
...
// 若系统中没有初始化性能域或者满足 overutilized 条件,那么直接退出该函数
if (!pd || READ_ONCE(rd->overutilized))
goto fail;
head = pd;
// 读取当前 CPU 对应的 sd_asym_cpucapacity 调度域,sd_asym_cpucapacity 调度域表示有高速缓存共享属性的调度域
sd = rcu_dereference(*this_cpu_ptr(&sd_asym_cpucapacity));
// 从调度域往上查找一个合适的调度域,该合适的调度域需要包含当前 CPU 和 prev_cpu,因为这两个 CPU 是最有可能用来
// 运行这个待唤醒进程的 CPU
while (sd && !cpumask_test_cpu(prev_cpu, sched_domain_span(sd)))
sd = sd->parent;
if (!sd)
goto fail;
// 因为有一个进程马上要被唤醒了,所以更新该进程的 blocked 负载
sync_entity_load_avg(&p->se);
// task_util_est() 获取进程的实际算力
if (!task_util_est(p))
goto unlock;
for (; pd; pd = pd->next) {
unsigned long cur_energy, spare_cap, max_spare_cap = 0;
int max_spare_cap_cpu = -1;
for_each_cpu_and(cpu, perf_domain_span(pd), sched_domain_span(sd)) {
if (!cpumask_test_cpu(cpu, &p->cpus_allowed))
continue;
/* 跳过将被过度使用的cpu */
util = cpu_util_next(cpu, p, cpu);
cpu_cap = capacity_of(cpu);
if (cpu_cap * 1024 < util * capacity_margin)
continue;
/* 总是使用prev_cpu作为候选。 */
if (cpu == prev_cpu) {
// compute_energy() 函数预测功耗的情况
prev_energy = compute_energy(p, prev_cpu, head);
best_energy = min(best_energy, prev_energy);
continue;
}
/* 在性能域中查找空闲容量最大的CPU */
spare_cap = cpu_cap - util;
if (spare_cap > max_spare_cap) {
max_spare_cap = spare_cap;
max_spare_cap_cpu = cpu;
}
}
/* 评估使用此CPU对能源的影响 */
if (max_spare_cap_cpu >= 0) {
// compute_energy() 函数预测功耗的情况
cur_energy = compute_energy(p, max_spare_cap_cpu, head);
if (cur_energy < best_energy) {
best_energy = cur_energy;
best_energy_cpu = max_spare_cap_cpu;
}
}
}
unlock:
rcu_read_unlock();
/* 如果prev_cpu不能使用,或者它至少节省了prev_cpu使用的6%的能量,那么选择best_energy_cpu */
if (prev_energy == ULONG_MAX)
return best_energy_cpu;
if ((prev_energy - best_energy) > (prev_energy >> 4))
return best_energy_cpu;
return prev_cpu;
fail:
rcu_read_unlock();
return -1;
}
select_task_rq_fair()->find_energy_efficient_cpu()->compute_energy()
:
// 当进程 p 迁移到 dst_cpu 之后,计算系统的整体功耗值是多少
static long
compute_energy(struct task_struct *p, int dst_cpu, struct perf_domain *pd)
{
...
// 首先在第一个 for 循环里,遍历系统中所有的性能域,然后计算每一个性能域的功耗,最后合并计算结果
for (; pd; pd = pd->next) {
max_util = sum_util = 0;
// 遍历性能域中每一个在线和活跃的 CPU
for_each_cpu_and(cpu, perf_domain_span(pd), cpu_online_mask) {
// 若进程 p 迁移到 dst_cpu,那么 cpu_util_next() 函数计算该 CPU 的实际算力
util = cpu_util_next(cpu, p, dst_cpu);
util = schedutil_energy_util(cpu, util);
max_util = max(util, max_util);
// sum_util 是该性能域中所有 CPU 的实际算力总和
sum_util += util;
}
// 通过该性能域的 max_util 和 sum_util 计算该性能域的整体功耗
energy += em_pd_energy(pd->em_pd, max_util, sum_util);
}
// 整体功耗 energy 是一个预测功耗,前提条件是进程 p 迁移到 dst_cpu
return energy;
}
select_task_rq_fair()->find_energy_efficient_cpu()->compute_energy()
:
// 用于预测一个性能域的功耗情况
// 参数 pd 表示将要预测哪个性能域的功耗情况
// 参数 max_util 表示在这个性能域中所有的 CPU 里面最高的 CPU 使用率
// 参数 sum_util 表示所有 CPU 的总 CPU 利用率
static inline unsigned long em_pd_energy(struct em_perf_domain *pd,
unsigned long max_util, unsigned long sum_util)
{
...
// 获取性能域里第一个 CPU 的额定算力,以进一步获得整个性能域中所有CPU的额定算力,因为性能域里所有 CPU
// 基于相同的微处理器架构,额定算力也是一样的
scale_cpu = arch_scale_cpu_capacity(NULL, cpu);
// 获取该性能域里频率最高的表项(table中频率是升序)
cs = &pd->table[pd->nr_cap_states - 1];
// map_util_freq() 函数做一个映射,为 CPU 最大实际算力 max_util、CPU 额定算力以及 OPP 表里的最高频率
// 建立一个映射关系,以换算 max_util 对应的频率(freq)是多少
freq = map_util_freq(max_util, cs->frequency, scale_cpu);
// 在 OPP 表里,查找一个正好和刚才换算出来的 freq 相等或者稍微大一点的频率点,接下来就使用这个频率点来计
// 算整个性能域的功耗
for (i = 0; i < pd->nr_cap_states; i++) {
cs = &pd->table[i];
if (cs->frequency >= freq)
break;
}
// 计算性能域的功耗
return cs->cost * sum_util / scale_cpu;
}
5. CPU动态调频
在 Linux4.7 内核开发期间,Intel的工程师Rafael J.Wysocki 提交了一个新的CPU freq驱动程序——schedutil。这是一个充分利用调度器提供的CPU使用率(CPU Utilization)信息来作为 CPU频率调节依据的新驱动程序。schedutil驱动程序实现在kernel/sched/cpufreq_schedutil.c文件中。如果要使用这个驱动程序,需要在配置内核时打开CONFIG_CPU_FREQ_GOV_SCHEDUTIL选项。
在schedutil驱动程序初始化时会注册一个CPUfreq管理者子系统:
// CPUfreq 管理者子系统
struct cpufreq_governor schedutil_gov = {
.name = "schedutil",
.owner = THIS_MODULE,
.dynamic_switching = true,
.init = sugov_init,
.exit = sugov_exit,
.start = sugov_start,
.stop = sugov_stop,
.limits = sugov_limits,
};
#ifdef CONFIG_CPU_FREQ_DEFAULT_GOV_SCHEDUTIL
struct cpufreq_governor *cpufreq_default_governor(void)
{
return &schedutil_gov;
}
#endif
在sugov_start()
函数中会为每个CPU建立schedutil的一个回调函数:
DEFINE_PER_CPU(struct update_util_data *, cpufreq_update_util_data);
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))
{
...
// cpufreq_update_util_data 是调度器和 schedutil 驱动程序沟通的"桥梁"
rcu_assign_pointer(per_cpu(cpufreq_update_util_data, cpu), data);
}
static int sugov_start(struct cpufreq_policy *policy)
{
...
for_each_cpu(cpu, policy->cpus) {
struct sugov_cpu *sg_cpu = &per_cpu(sugov_cpu, cpu);
// 建立回调函数
cpufreq_add_update_util_hook(cpu, &sg_cpu->update_util,
policy_is_shared(policy) ?
sugov_update_shared :
sugov_update_single);
}
return 0;
}
CPU调频的触发: