Capacity Aware Scheduling
1.CPU容量
1.1 介绍
传统的同质SMP平台由完全相同的CPU组成,相对应的异构平台由具有不同性能特征的CPU组成——在这样的平台上,并非所有CPU的能力都是一样的。
CPU容量是衡量CPU所能达到的性能的指标,与系统中性能最高的CPU进行标准化。异构系统也称为非对称CPU容量系统,因为它们包含不同容量的CPU。
最大可实现性能(最大CPU容量IOW)的差异源于两个因素:
- 并非所有CPU都具有相同的微体系结构(µarch)。
- 使用动态电压和频率缩放(DVFS),并非所有CPU都能够在物理上获得更高的操作性能点(OPP)
ARM的大小核架构就是就是这两者的一个例子。大核CPU比小核CPU更注重性能(更多的流水线级、更大的缓存、更智能的预测器等),通常可以达到比小核CPU高的OPP。
CPU性能通常以每秒百万条指令(MIPS)表示,也可以表示为每Hz可获得的给定指令量,导致:
capacity(cpu) = work_per_hz(cpu) * max_freq(cpu)
1. 2 调度器相关术语
调度程序中使用了两个不同的容量值。CPU的“capacity_orig”是其最大可达到的容量,即其最大可实现的性能水平。CPU的“容量”是其“capacity_orig”,减去可用性能的一些损失(例如处理IRQ所花费的时间)。(个人理解:因为中断处理的代码,不在调度器的统计之中,所以计算CPU上的rq队列中的算力消耗的时候,漏掉了irq的处理部分)
请注意,CPU的“capacity”仅用于CFS类,而“capacity_orig”与类无关。为简明起见,本文档的其余部分将使用术语“capacity”代表了“capacity_orig”的含义。
1.3 平台举例
1.3.1 相同的OPP
考虑一个假设的双核非对称CPU容量系统,其中
> work_per_hz(CPU0) = W
> work_per_hz(CPU1) = W/2
> 所有CPU都以相同的固定频率运行
根据上述容量定义:
> 容量(CPU0)=C
> 容量(CPU1)=C/2
根据大小核的概念,我们可以将CPU0当做大核,CPU1当做小核。
对于定期执行固定工作量的工作负载,您将得到如下执行跟踪:
CPU0具有系统(C)中最高的容量,并在T个时间单位内完成固定量的工作W。另一方面,CPU1的容量是CPU0的一半,因此在T中只完成W/2。
1.3.2 不同的最大OPP
通常,不同容量值的CPU也具有不同的最大OPP。考虑与上面相同的CPU(即相同的work_per_hz()):
> max_freq(CPU0) = F
> max_freq(CPU1) = 2/3 * F
这将导致:
> capacity(CPU0) = C
> capacity(CPU1) = C/3
执行与上面所述相同的工作负载,每个CPU以其最大频率运行会导致:
1.4 重要说明
应该注意的是,使用单个值来表示CPU性能的差异可能不是很准确。两个不同架构之间的相对性能差异可能是,整数运算的X%,浮点运算的Y%,分支运算的Z%,等等。不过,使用这种简单方法的结果目前还是令人满意的。
2. 任务利用率
2.1 简介
容量感知调度(CAS)需要描述任务对CPU容量的要求。每个调度器类都可以用不同的方式表达这一点,虽然任务利用率是CFS特有的,但为了引入更通用的概念,在这里借用任务使用率描述它会更方便。
任务利用率是表示任务所需要的吞吐量的百分比。它的一个简单近似值是任务的占空比,即:
task_util(p) = duty_cycle(p)
在具有固定频率的SMP系统上,100%的利用率表明任务是繁忙执行周期。相反,10%的利用率意味着这是一个周期性轻量任务,它的睡眠时间比执行时间长。可变的CPU频率和不对称的CPU容量使这一点复杂化;以下部分将对这些进行扩展。
2.2 频率不变性
需要考虑的一个问题是,工作负载的占空比直接受到CPU当前运行的OPP的影响。考虑以给定频率F运行周期性工作负载:
这种情况下 duty_cycle(p) == 25%.
现在,考虑以频率F/2运行相同的工作负载:
这种情况下,尽管任务在两次执行中具有完全相同的行为(即执行相同的工作量)但是这次的duty_cycle(p)==50%
task_util_freq_inv(p) = duty_cycle(p) * (curr_frequency(cpu) / max_frequency(cpu))
将此公式应用于上述两个示例,将因为评率不同产生25%的利用率的差异。
2.3 CPU不变性
CPU容量对任务利用率有类似的影响,因为在不同容量值的CPU上运行相同的工作负载将产生不同的占空比。
考虑1.3.1的情况
容量(CPU0)=C
容量(CPU1)=C/3
在每个CPU上以最大频率执行给定的周期性工作负载将导致:
换句话说:
如果p以其最大频率在CPU0上运行,duty_cycle(p)==25%
如果p以其最大频率在CPU1上运行,duty_cycle(p)==75%
可以使用以下公式使任务利用率在不同的CPU上保持不变:
task_util_cpu_inv(p) = duty_cycle(p) * (capacity(cpu) / max_capacity)
其中“max_capacity”是系统中最高的CPU容量值。将此公式应用于上面的示例,两种情况下都得到25%的CPU利用率。
2.4 不变的任务利用率
为了获得一致性的结果,需要将频率和CPU不变性应用于任务利用率。因此,对于给定任务p,CPU和频率不变的任务利用率的伪公式为:
换句话说,不变任务利用率描述了一个任务在容量最高的CPU上以最大的频率运行时的使用率。以下章节中提到的任务利用率默认都是按照这个公式进行定义的。
2.5 利用率估算
如果没有crystal ball,任务行为(以及任务利用率)就无法在任务首次可运行时准确预测。CFS类基于PELT机制维护少量CPU和任务信号,计算出一个平均利用率(而不是瞬时利用率)。
这意味着,尽管将考虑“真实”任务利用率(使用crystal ball)来编写容量感知调度标准,但实际实现时我们只能通过某种方式进行估算起任务使用率。
3.1 CAS的需求
3.1 CPU容量
Linux目前无法自行计算CPU容量,因此必须从系统架构arch中获取这个值。arch必须定义arch_scale_cpu_capacity()。
下面是高通8450架构中定义的这个函数。代码路径:/kernel_platform/msm-kernel/include/linux/arch_topology.h
20 static inline unsigned long topology_get_cpu_scale(int cpu)
21 {
22 return per_cpu(cpu_scale, cpu);
23 }
3.2频率不变性
如2.2所述,容量感知调度需要频率不变的任务利用率。架构必须为此定义arch_scale_freq_capacity(cpu)。
实现此功能需要确定每个CPU的运行频率。实现这一点的一种方法是利用硬件计数器,其递增速率随CPU的当前频率而变化(x86上的APERF/MPERF,arm64上的AMU)。另一种方法是,当内核意识到切换到的频率(arm/arm64也使用)时,直接使用cpufreq frequency的信息。
下面是高通8450架构中定义的这个函数。代码路径:/kernel_platform/msm-kernel/include/linux/arch_topology.h
29 static inline unsigned long topology_get_freq_scale(int cpu)
30 {
31 return per_cpu(freq_scale, cpu);
32 }
4.调度器拓扑
在构建调度域的过程中,调度器将确定系统是否具有非对称的CPU容量。也就是需要符合下面两个条件:
检查sched_asym_cpucapacity静态关键字是否被使能。
SD_ASYM_CPUCAPACITY标志将设置在最低的sched_domain级别,也就是说这个级别只包含了一个容量级别而没有跨越多个容量级别。
sched_asym_cpucapacity静态关键字用于保护非对称CPU容量系统的代码段。但是,请注意,所述关键字是“系统范围”的。假设CPUSET有如下设置:
可以通过如下命令的配置,达到这个目的
mkdir /sys/fs/cgroup/cpuset/cs0
echo 0-1 > /sys/fs/cgroup/cpuset/cs0/cpuset.cpus
echo 0 > /sys/fs/cgroup/cpuset/cs0/cpuset.mems
mkdir /sys/fs/cgroup/cpuset/cs1
echo 2-7 > /sys/fs/cgroup/cpuset/cs1/cpuset.cpus
echo 0 > /sys/fs/cgroup/cpuset/cs1/cpuset.mems
echo 0 > /sys/fs/cgroup/cpuset/cpuset.sched_load_balance
由于系统中存在*CPU容量不对称,将启用sched_asym_cpucapacity静态关键字。然而,CPU 0-1的sched_domain层次结构只包含了一个容量值,SD_ASYM_CPUCAPACITY没有在该层次结构中被设置,它描述了一个SMP孤岛,所以不应该把它当做非对称容量来处理。
因此,用于保护满足不对称CPU容量的代码路径的“规范”模式是:检查sched_asym_cpucapacity静态关键字如果已启用,则还检查sched_domain层次结构中是否存在SD_asym_cpucapacity(如果设置,即代码路径针对特定CPU或其组)
5.容量感知调度实现
5.1 CFS
5.1.1 容量适应性
CFS的主要容量调度标准为:
task_util(p) < capacity(task_cpu(p))
这通常被称为容量适合性标准,即CFS必须确保任务“适合”其CPU。如果不满足这个条件,也就是CPU运行到最大频率的情况下,也不能满足任务的容量,这被称为CPU-bound。
此外,uclamp允许用户空间通过sched_setattr()或cgroup接口(请参阅Documentation/admin-guide/cgroup-v2.rst)为任务指定最小和最大利用率值。顾名思义,这可以用于将task_util()限制在前面的标准中。
5.1.2唤醒CPU选择
CFS任务唤醒CPU选择遵循上述容量适应标准。最重要的是,uclamp用于限制任务利用率值,这让用户空间在CFS任务的CPU选择上有更多的优势。也就是说,CFS唤醒CPU选择搜索满足以下条件的CPU:
clamp(task_util(p), task_uclamp_min(p), task_uclamp_max(p)) < capacity(cpu)
使用uclamp,用户空间可以通过设置一个比较低的uclamp.max,使本来使用率100%的任务跑在任何一个CPU上;或者设置一个比较大的uclamp.max,让使用率10%的任务执行在高性能的CPU上。
CFS中的唤醒CPU选择可能会被能量感知调度(EAS)所体会,这在Documentation/scheduler/sched-Energy.rst中有描述。
5.1.3负载平衡
唤醒CPU选择中的一种有问题情况就是,一个任务很少睡眠(如果有的话)时,因此也很少走到唤醒流程。考虑:
w == wakeup event
capacity(CPU0) = C
capacity(CPU1) = C / 3
本来这个任务应该运行在CPU0上,但是出现了下面的情况:
从一开始就计划不正确(初始利用率估计不准确)
从一开始就正确安排了,但突然需要更多的处理能力
那么就可能变成了CPU受限了。也就是说 task_util(p) > capacity(task_cpu(p))
CPU容量调度标准被违反,并且可能不再有任何唤醒事件来通过唤醒CPU选择来解决此问题。
在这种情况下的任务被称为“不匹配”任务。利用CFS负载均衡器,可以讲不合适的任务迁移到合适的CPU上,更具体地说是进行主动负载平衡(它适合迁移当前正在运行的任务)。当负载平衡发生时,如果不匹配任务可以迁移到容量大于当前容量的CPU,则将触发不匹配活动负载平衡。
5.2 RT调度
5.2.1唤醒CPU选择
RT任务唤醒CPU选择搜索满足以下条件的CPU:task_uclamp_min(p) <= capacity(task_cpu(cpu))
同时仍然遵循通常的优先级约束。如果没有一个候选CPU能够满足这个容量标准,那么将遵循严格的基于优先级的调度,并忽略CPU容量。
5.3 DL调度
5.3.1唤醒CPU选择
DL任务唤醒CPU选择搜索满足以下条件的CPU:task_uclamp_min(p) <= capacity(task_cpu(cpu))
同时仍然遵守通常的带宽和期限限制。如果没有一个候选CPU能够满足此容量标准,则任务将保留在其当前CPU上。