Linux支持非常多的硬件,这些硬件有各种各样的CPU搭配,大到多个NUMA节点组成的大型机,小到只有一个处理器的小型嵌入式设备。内核会在开机时根据硬件配置建立一组数据结构来描述系统的CPU物理拓扑。CPU物理拓扑的一个典型使用者就是调度域。CPU物理拓扑相关的概念也可以见这篇博客的介绍。
此外,一个系统中的CPU架构可以有多种,典型的如ARM的big-LITTLE架构,这些CPU的能力是不一样的,这种设计可以较好的兼顾性能和功耗。Linux内核也需要建立一组数据结构来描述各cpu的能力。
由于cpu能力的维护和cpu物理拓扑的在实现时紧密相关,这篇笔记对二者的实现代码进行分析。分析的代码基于Linux 5.10内核,涉及体系结构相关部分以ARM64为例,主要涉及如下几个文件:
文件 | 描述 |
arch/arm64/kernel/topology.c | ARM64的CPU拓扑实现文件,包含了核心的构建CPU拓扑表的流程 |
drivers/base/topology.c | CPU拓扑驱动,通过sysfs向用户态暴露了CPU拓扑结构 |
include/linux/topology.h | 体系结构无关的CPU拓扑头文件,提供了默认的CPU拓扑相关接口实现。外部模块总是应该引用该头文件 |
include/linux/arch_topology.h | 体系结构相关的CPU拓扑信息 |
CPU物理拓扑
数据结构
内核用struct cpu_topology结构体来描述一个逻辑cpu的物理拓扑关系。逻辑cpu指的是可以执行一个线程的处理器,系统有几个逻辑cpu,就意味着可以并行执行几个线程。
struct cpu_topology {
int thread_id;
int core_id;
int package_id;
int llc_id;
cpumask_t thread_sibling;
cpumask_t core_sibling;
cpumask_t llc_sibling;
};
struct cpu_topology cpu_topology[NR_CPUS]; // CPU拓扑信息表
理解这些字段的含义需要先了解下硬件上的几个基本概念(只谈和cpu物理拓扑相关的含义,并非属于描述):
- NUMA节点:一个复杂系统可以有多个NUMA节点组成,每个NUMA节点可以有多个物理处理器。
- Package/Socket/Cluster:一个物理处理器,外观上为一个封装(Package),ARM上也称作Cluster,X86上称作插槽(Socket),可以由多个物理Core组成。
- SMT:超线程。不支持超线程时,一个物理Core就是一个逻辑CPU;支持超线程时,一个物理Core可以有多个逻辑cpu,典型的为一个物理core有2个逻辑cpu。
之后再来理解cpu_topology中各字段就很容易了:
- llc_id、llc_sibling:NUMA系统时才有用,ARM平台不支持NUMA,实际情况没见过,不太清楚真实值是怎么样的。猜测llc_id就是NUMA节点ID,llc_sibling就是同一个NUMA节点中所有逻辑cpu的mask。
- package_id:即Cluster ID。
- core_id、core_sibling:core_id为同一个Cluster下面物理Core的编号,从0开始;core_sibling表示同一个Cluster下面所有逻辑cpu的掩码。
- thread_id、thread_sibling:thread_id为同一个物理Core下面的线程编号,从0开始;thread_sibling表示同一个物理Core下面所有逻辑cpu的掩码。
构建CPU物理拓扑
Linux内核在开机初始化阶段完成cpu拓扑表的构建,不同体系结构触发构建的时机不同,下面是ARM64的建立过程。
首先是是调用init_cpu_topology()从DTS中解析基本的拓扑信息:
void __init init_cpu_topology(void)
{
reset_cpu_topology();
/*
* Discard anything that was parsed if we hit an error so we
* don't use partial information.
*/
if (parse_acpi_topology())
reset_cpu_topology();
// ARM64是从DTS中解析
else if (of_have_populated_dt() && parse_dt_topology())
reset_cpu_topology();
}
static int __init parse_dt_topology(void)
{
struct device_node *cn, *map;
int ret = 0;
int cpu;
// 从/cpus/cpu_map节点中解析拓扑
cn = of_find_node_by_path("/cpus");
map = of_get_child_by_name(cn, "cpu-map");
ret = parse_cluster(map, 0); // 解析所有Cluster配置
// cpu的raw_capacity信息是和拓扑信息一起配置,解析完毕后进行一次cpu能力的归一化计算
topology_normalize_cpu_scale();
// 进行一次基本的配置检查,确保配置中有每一个逻辑cpu的配置
for_each_possible_cpu(cpu)
if (cpu_topology[cpu].package_id == -1)
ret = -EINVAL;
...
}
static int __init parse_cluster(struct device_node *cluster, int depth)
{
char name[20];
bool leaf = true;
bool has_cores = false;
struct device_node *c;
static int package_id __initdata;
int core_id = 0;
int i, ret;
// 循环递归的调用parse_cluster()函数可以完成/cpus/cpu-map节点下所有cluster节点的解析
i = 0;
do {
snprintf(name, sizeof(name), "cluster%d", i);
c = of_get_child_by_name(cluster, name);
if (c) {
leaf = false;
ret = parse_cluster(c, depth + 1);
of_node_put(c);
if (ret != 0)
return ret;
}
i++;
// c为空表示当前节点下没有名为clusterXX的子节点,结束循环,尝试解析cluster下的core节点
} while (c);
// 和上面类似,实现解析cluster节点下的所有core节点
i = 0;
do {
snprintf(name, sizeof(name), "core%d", i);
c = of_get_child_by_name(cluster, name);
if (c) {
has_cores = true;
// core节点只能存在cluster节点下,如果直接出现在cpu-map下属于配置错误
if (depth == 0) {
pr_err("%pOF: cpu-map children should be clusters\n", c);
of_node_put(c);
return -EINVAL;
}
if (leaf) {
ret = parse_core(c, package_id, core_id++);
} else {
pr_err("%pOF: Non-leaf cluster with core %s\n", cluster, name);
ret = -EINVAL;
}
of_node_put(c);
if (ret != 0)
return ret;
}
i++;
} while (c);
if (leaf && !has_cores)
pr_warn("%pOF: empty cluster\n", cluster);
if (leaf)
package_id++;
return 0;
}
static int __init parse_core(struct device_node *core, int package_id,
int core_id)
{
char name[20];
bool leaf = true;
int i = 0;
int cpu;
struct device_node *t;
do { // 如果有配置threadXXX,解析它,支持SMT才会配置
snprintf(name, sizeof(name), "thread%d", i);
t = of_get_child_by_name(core, name);
if (t) {
leaf = false;
cpu = get_cpu_for_node(t);
if (cpu >= 0) { // 解析结果填充cpu拓扑信息表
cpu_topology[cpu].package_id = package_id;
cpu_topology[cpu].core_id = core_id;
cpu_topology[cpu].thread_id = i;
} else if (cpu != -ENODEV) {
pr_err("%pOF: Can't get CPU for thread\n", t);
of_node_put(t);
return -EINVAL;
}
of_node_put(t);
}
i++;
} while (t);
// 根据配置找到对应的CPU ID
cpu = get_cpu_for_node(core);
if (cpu >= 0) {
if (!leaf) {
pr_err("%pOF: Core has both threads and CPU\n", core);
return -EINVAL;
}
cpu_topology[cpu].package_id = package_id;
cpu_topology[cpu].core_id = core_id;
} else if (leaf && cpu != -ENODEV) {
pr_err("%pOF: Can't get CPU for leaf core\n", core);
return -EINVAL;
}
return 0;
}
static int __init get_cpu_for_node(struct device_node *node)
{
struct device_node *cpu_node;
int cpu;
cpu_node = of_parse_phandle(node, "cpu", 0); // cpu节点配置
if (!cpu_node)
return -1;
cpu = of_cpu_node_to_id(cpu_node);
if (cpu >= 0) // 解析cpu raw_capacity,见后面cpu能力的分析
topology_parse_cpu_capacity(cpu_node, cpu);
else
pr_info("CPU node for %pOF exist but the possible cpu range is :%*pbl\n",
cpu_node, cpumask_pr_args(cpu_possible_mask));
of_node_put(cpu_node);
return cpu;
}
第一阶段解析会将拓扑表中每个逻辑cpu的package_id、core_id和thread_id(支持超线程时)字段进行填充。第二阶段会调用store_cpu_topology()根据已有信息设置其它字段。
void store_cpu_topology(unsigned int cpuid)
{
struct cpu_topology *cpuid_topo = &cpu_topology[cpuid];
u64 mpidr;
if (cpuid_topo->package_id != -1)
goto topology_populated;
// DTS未配置的情况下根据硬件的寄存器设置CPU拓扑表,这里我们不关注
mpidr = read_cpuid_mpidr();
...
topology_populated:
update_siblings_masks(cpuid);
}
void update_siblings_masks(unsigned int cpuid)
{
struct cpu_topology *cpu_topo, *cpuid_topo = &cpu_topology[cpuid];
int cpu;
// 更新每一个逻辑cpu的拓扑表信息
for_each_online_cpu(cpu) {
cpu_topo = &cpu_topology[cpu];
// 设置属于同一个NUMA节点的逻辑cpu的llc_sibling掩码
if (cpuid_topo->llc_id == cpu_topo->llc_id) {
cpumask_set_cpu(cpu, &cpuid_topo->llc_sibling);
cpumask_set_cpu(cpuid, &cpu_topo->llc_sibling);
}
// 下面属于同一个Cluster的逻辑cpu才需要设置
if (cpuid_topo->package_id != cpu_topo->package_id)
continue;
// 设置属于同一个Cluster的逻辑cpu的core_sibling掩码
cpumask_set_cpu(cpuid, &cpu_topo->core_sibling);
cpumask_set_cpu(cpu, &cpuid_topo->core_sibling);
// 下面属于同一个物理core的逻辑cpu才需要设置
if (cpuid_topo->core_id != cpu_topo->core_id)
continue;
// 设置属于同一个物理core的逻辑cpu的thread_sibling掩码
cpumask_set_cpu(cpuid, &cpu_topo->thread_sibling);
cpumask_set_cpu(cpu, &cpuid_topo->thread_sibling);
}
}
对外接口
对内核其它模块,可以通过如下宏来获取某个逻辑cpu的拓扑信息:
#define topology_physical_package_id(cpu) (cpu_topology[cpu].package_id)
#define topology_core_id(cpu) (cpu_topology[cpu].core_id)
#define topology_core_cpumask(cpu) (&cpu_topology[cpu].core_sibling)
#define topology_sibling_cpumask(cpu) (&cpu_topology[cpu].thread_sibling)
#define topology_llc_cpumask(cpu) (&cpu_topology[cpu].llc_sibling)
对于用户态,可以通过sysfs中的/sys/devices/system/cpu/cpuX/topology目录中的各属性文件查看指定逻辑cpu的拓扑信息,驱动代码很简单,这里不再展开,下面是一个2大核+6小核手机上的各cpu拓扑信息值:
cpu | physical_package_id | core_id | core_siblings | core_siblings_list | thread_siblings | thread_siblings_list |
0 | 0 | 0 | 3F | 0-5 | 01 | 0 |
1 | 0 | 1 | 3F | 0-5 | 02 | 1 |
2 | 0 | 2 | 3F | 0-5
| 04 | 2 |
3 | 0 | 3 | 3F | 0-5 | 08 | 3 |
4 | 0 | 4 | 3F | 0-5 | 10 | 4 |
5 | 0 | 5 | 3F | 0-5 | 20 | 5 |
6 | 0 | 0 | C0 | 6-7 | 40 | 6 |
7 | 0 | 1 | C0 | 6-7 | 80 | 7 |
CPU能力
CPU能力指的就是cpu的算力,它和两个因素有关:
- CPU的硬件架构,通常用DMIPS(每秒百万条指令数)来描述这一能力,这里有个约束是1MHz频率运行的条件。
- CPU的运行频率,频率越高,算力越强,当然功耗也更高。
DMIPS是硬件规格,一旦硬件确定后就不会发生改变。CPU的运行频率却受如下几个方面的影响:
- 规格最高频:硬件规格决定的cpu能够达到的最大频率,硬件确定后就不会发生改变。
- 策略最高频:基于策略(典型的如热、功耗)对cpu当前可以达到的最大频率进行限制,该值必然是小于规格最高频的。这个值是一个动态变化的值。
- 当前运行频率:cpufreq使用governor(典型的如schedutil根据cpu利用率)动态的在[0,策略最高频]之间选择一个运行频率,这个值在实际中往往是快速变化的。
基于上面的一些基本概念来看Linux内核对cpu能力实现。首先,在上面解析cpu拓扑信息时,有调用topology_parse_cpu_capacity()从DTS中解析每个逻辑cpu的DMIPS,然后保存到raw_capacity数组中。
static DEFINE_PER_CPU(u32, freq_factor) = 1;
static u32 *raw_capacity;
bool __init topology_parse_cpu_capacity(struct device_node *cpu_node, int cpu)
{
struct clk *cpu_clk;
int ret;
u32 cpu_capacity;
ret = of_property_read_u32(cpu_node, "capacity-dmips-mhz",
&cpu_capacity);
if (!raw_capacity) {
raw_capacity = kcalloc(num_possible_cpus(),
sizeof(*raw_capacity), GFP_KERNEL);
}
raw_capacity[cpu] = cpu_capacity;
pr_debug("cpu_capacity: %pOF cpu_capacity=%u (raw)\n",
cpu_node, raw_capacity[cpu]);
// 得到启动初期的cpu运行频率保存到Percpu变量中
cpu_clk = of_clk_get(cpu_node, 0);
if (!PTR_ERR_OR_ZERO(cpu_clk)) {
per_cpu(freq_factor, cpu) = clk_get_rate(cpu_clk) / 1000;
clk_put(cpu_clk);
}
return !ret;
}
之后,在拓扑信息解析完毕,还调用topology_normalize_cpu_scale()函数对raw_capacity数组中的DMIPS值进行归一化处理,能力最强的cpu的DMIPS值被折算为1024,其它cpu的DMIPS值按照比例折算为小于1024的值。
raw_capacity[cpu]
cpu_scale[cpu] = -------------------------- * 1024
capacity_scale
void topology_normalize_cpu_scale(void)
{
u64 capacity;
u64 capacity_scale;
int cpu;
if (!raw_capacity)
return;
// 找到系统中能力最强的cpu的能力保存到capacity_scale中,这里考虑了规格最高频
capacity_scale = 1;
for_each_possible_cpu(cpu) {
capacity = raw_capacity[cpu] * per_cpu(freq_factor, cpu);
capacity_scale = max(capacity, capacity_scale);
}
pr_debug("cpu_capacity: capacity_scale=%llu\n", capacity_scale);
// 对每个逻辑cpu的能力进行折算,折算后的结果保存在Percpu变量cpu_scale中
for_each_possible_cpu(cpu) {
capacity = raw_capacity[cpu] * per_cpu(freq_factor, cpu);
capacity = div64_u64(capacity << SCHED_CAPACITY_SHIFT, capacity_scale);
topology_set_cpu_scale(cpu, capacity);
pr_debug("cpu_capacity: CPU%d cpu_capacity=%lu\n",
cpu, topology_get_cpu_scale(cpu));
}
}
DEFINE_PER_CPU(unsigned long, cpu_scale) = SCHED_CAPACITY_SCALE;
void topology_set_cpu_scale(unsigned int cpu, unsigned long capacity)
{
per_cpu(cpu_scale, cpu) = capacity;
}
Linux内核用cpufreq子系统来管理cpu的频率,在初始化时还向cpufreq子系统注册了policy变化通知事件,目的是在cpufreq确定了所有cpu的规格最高频后调用上面的topology_normalize_cpu_scale()函数重新进行一次归一化计算。
static struct notifier_block init_cpu_capacity_notifier = {
.notifier_call = init_cpu_capacity_callback,
};
static int __init register_cpufreq_notifier(void)
{
int ret;
if (!acpi_disabled || !raw_capacity)
return -EINVAL;
// cpus_to_visit用来标记是否已经收到了所有逻辑cpu的policy通知
if (!alloc_cpumask_var(&cpus_to_visit, GFP_KERNEL))
return -ENOMEM;
cpumask_copy(cpus_to_visit, cpu_possible_mask);
// 向cpufreq子系统注册policy变化通知事件
ret = cpufreq_register_notifier(&init_cpu_capacity_notifier,
CPUFREQ_POLICY_NOTIFIER);
return ret;
}
core_initcall(register_cpufreq_notifier);
static int init_cpu_capacity_callback(struct notifier_block *nb,
unsigned long val, void *data)
{
struct cpufreq_policy *policy = data;
int cpu;
if (val != CPUFREQ_CREATE_POLICY) // 只处理policy的第一个事件
return 0;
// 删除已经收到通知的cpu
cpumask_andnot(cpus_to_visit, cpus_to_visit, policy->related_cpus);
// 保存cpu的规格最高频率,单位为MHz
for_each_cpu(cpu, policy->related_cpus)
per_cpu(freq_factor, cpu) = policy->cpuinfo.max_freq / 1000;
// 变为空时表示收到了所有逻辑cpu的policy事件
if (cpumask_empty(cpus_to_visit)) {
topology_normalize_cpu_scale(); // 重新进行归一化计算
schedule_work(&update_topology_flags_work); // 去注册policy事件监听
free_raw_capacity();
schedule_work(&parsing_done_work);
}
return 0;
}
至此,Percpu变量cpu_scale保存了归一化的cpu能力值,取值范围为[0, 1024],系统中能力最大的cpu的值为1024,其它CPU按照和最大能力cpu的比例折算为小于1024的值。该能力值只考虑了DMIPS(硬件规格)和规格最高频,所以这个值在确定后就不会再发生变化,它代表了各逻辑cpu的最高算力。用户态可以从/sys/devices/system/cpu/cpuX/cpu_capacity文件获取对应cpu的该能力值。
其余两个影响cpu能力的因素是策略最高频和cpu当前运行频率,这两个值是动态变化的,而且都来自cpufreq子系统。当cpu的运行频率发生变化时,会调用arch_set_freq_scale()函数设置cpu的当前运行频率,以及cpu的规格最高频率。
unsigned int cpufreq_driver_fast_switch(struct cpufreq_policy *policy,
unsigned int target_freq)
{
...
arch_set_freq_scale(policy->related_cpus, freq,
policy->cpuinfo.max_freq);
}
#define arch_set_freq_scale topology_set_freq_scale
// 记录每个逻辑cpu的当前运行频率相对于规格最高频折算为1024的值
cur_freq
freq_scale[cpu] = -------------------- * 1024
max_freq
DEFINE_PER_CPU(unsigned long, freq_scale) = SCHED_CAPACITY_SCALE;
void topology_set_freq_scale(const struct cpumask *cpus, unsigned long cur_freq,
unsigned long max_freq)
{
unsigned long scale;
int i;
if (WARN_ON_ONCE(!cur_freq || !max_freq))
return;
scale = (cur_freq << SCHED_CAPACITY_SHIFT) / max_freq;
for_each_cpu(i, cpus)
per_cpu(freq_scale, i) = scale;
}
说明:在更低一些版本的内核中,还有一个Percpu变量(max_freq_scale)保存了策略最高频相对于规格最高频折算到1024的参数,但是在5.10删除了这部分代码。
对外接口
内核其它模块可以通过如下接口来获取cpu能力信息。
// 获取cpu的归一化能力值,它考虑了DMIPS和规格最高频,可用于在系统中所有cpu之间进行能力比较
#define arch_scale_cpu_capacity topology_get_cpu_scale
static inline unsigned long topology_get_cpu_scale(int cpu)
{
return per_cpu(cpu_scale, cpu);
}
// 获取cpu的当前运行频率相对于其规格最高频折算到1024的值
#define arch_scale_freq_capacity topology_get_freq_scale
static inline unsigned long topology_get_freq_scale(int cpu)
{
return per_cpu(freq_scale, cpu);
}
下面是PELT在计算cpu利用率时的代码,它通过将运行时间running(实际时长)结合cpu能力进行折算后统计,这样不同能力的cpu上统计出来的利用率就可以互相进行比较了。
#define cap_scale(v, s) ((v)*(s) >> SCHED_CAPACITY_SHIFT)
static inline void update_rq_clock_pelt(struct rq *rq, s64 delta)
{
...
delta = cap_scale(delta, arch_scale_cpu_capacity(cpu_of(rq)));
delta = cap_scale(delta, arch_scale_freq_capacity(cpu_of(rq)));
rq->clock_pelt += delta;
}
计算公式换种写法更好理解些,后两个因子值在[0,1]之间,相当于对实际运行时长进行打折,能力越差的cpu折扣越多。
cpu归一化能力 cpu运行频率归一化
delta = delta * --------------------- * -------------------------
1024 1024