sched-domain
CPU拓扑
调度域和cpu的拓扑结构密切相关,cpu拓扑结构的相关代码位于:
- kernel/sched/topology.c
- include/linux/sched/topology.h
- drivers/base/arch_topology.c
- include/linux/arch_topology.h
For RISC-V & ARM64 systems, the CPU topology is generated by reading the “cpu-map” node from the device tree.
cpu_topology数据结构
//include/linux/arch_topology.h
struct cpu_topology {
int thread_id; //CONFIG_SCHED_SMT
int core_id;
int package_id; //packages (clusters)
int llc_id; //last level cache, Currently only supported on ARM64 using ACPI
cpumask_t thread_sibling;
cpumask_t core_sibling; //用于表示一个cluster内的所有兄弟cpu的bit mask
cpumask_t llc_sibling; //A bit mask of packages that share the last level cache
};
内核中有一个全局数组来保存cpu topology table:
drivers/base/arch_topology.c
/*
* cpu topology table
*/
struct cpu_topology cpu_topology[NR_CPUS];
EXPORT_SYMBOL_GPL(cpu_topology);
CPU Capacity
The following values are read and calculated through the device tree.
- “capacity-dmips-mhz” property value
- Save in raw_capacity[].
- The maximum value is stored in capacity_scale.
- It is simply calculated as follows:
- cpu_scale[] = raw_capacity[] / capacity_scale
系统以所有cpu core中最大的capacity作为capacity_scale,所有cpu以该值来作为归一化处理。
cpu_topology初始化
下图以4个cpu为例,0、1核属于little cluster,2、3核属于big cluster:
注意:上图中,cpu_topology数据结构的初始化,只进行了红框中成员的初始化。如右下角表中thread_sibling、core_sibling等都属于未初始化状态!
如果cpu topology没能从device tree中获取,则还可以通过mpidr寄存器获取,如下图:
注意:store_cpu_topology()有从mpidr获取cpu topology的功能(如果已经从dts中获取,则跳过相关逻辑),更重要的是获取sibling信息(update_siblings_masks())! 每一个booted cpu都要调用该函数!
调度域和调度组
注:本节中load balance的“scheduling group”和cgroup中的“group scheduling”是完全不相关的两种技术!
scheduling domain topology
ARM64 架构一般按照以下顺序创建一个调度域topology:
- SMT
- Gives more priority to CPUs that share the L1 cache (virtual core)
- It is the cheapest because it does not require flushing the cache.
- arm64 is not utilized yet.
- Using the CONFIG_SCHED_SMT kernel option
- MC
- Next, we give priority to CPUs within the cluster that share the L2 cache.
- If there are clusters such as big/little, use this option to manage them by cluster unit.
- Using the CONFIG_SCHED_MC kernel option
- DIE
- Next, it gives priority to CPUs that use the same die (sharing the L3 cache).
- Most systems with less than a few cores, but not multi-clusters, typically only use this step.
- NUMA
- NUMA levels can use multiple levels depending on the NUMA distance.
- Gives priority to the CPU using the shortest path across NUMA nodes.
- Using the CONFIG_SCHED_NUMA kernel option
下面是几种常见的cpu拓扑结构:
如下是内核中定义的default topology:
/*
* Topology list, bottom-up.
*/
static struct sched_domain_topology_level default_topology[] = {
#ifdef CONFIG_SCHED_SMT
{ cpu_smt_mask, cpu_smt_flags, SD_INIT_NAME(SMT) },
#endif
#ifdef CONFIG_SCHED_MC
{ cpu_coregroup_mask, cpu_core_flags, SD_INIT_NAME(MC) },
#endif
{ cpu_cpu_mask, SD_INIT_NAME(DIE) },
{ NULL, },
};
如下是内核中定义的arm topology:
static struct sched_domain_topology_level arm_topology[] = {
#ifdef CONFIG_SCHED_MC
{ cpu_corepower_mask, cpu_corepower_flags, SD_INIT_NAME(GMC) },
{ cpu_coregroup_mask, cpu_core_flags, SD_INIT_NAME(MC) },
#endif
{ cpu_cpu_mask, SD_INIT_NAME(DIE) },
{ NULL, },
};
Initialize scheduling domains
kernel_init() -> kernel_init_freeable() -> sched_init_smp() ->sched_init_domains(cpu_active_mask)
构建调度域,主要分为以下几步:
- __visit_domain_allocation_hell:分配和初始化调度域拓扑结构(根据cpu_map及cpu topology),包括root domain。
- build_sched_domain:构建调度域(for_each_cpu<for_each_sd_topology>)
- build_sched_groups:构建调度组(for_each_cpu)
- init_sched_groups_capacity:初始化调度组的capacity
- cpu_attach_domain:attach调度域(for_each_cpu)
分配和初始化调度拓扑
__visit_domain_allocation_hell——》__sdt_alloc
该函数会根据具体CPU架构的调度域拓扑(比如,arm架构一般只有两级——MC、DIE),来分配percpu的sched_domain, sched_group, and sched_group_capacity等结构。下图是以DIE level来呈现,初始化后的数据结构关系:
如上图中sched_domain_topology_level的data结构的子成员都是percpu属性的二级指针,会为该level调度域中的每个cpu分配sd、sg以及sgc等结构空间。
下图是描述如何分配和初始化root domain:
上图中,init_dl_bw、cpudl_init和cpupri_init是初始化deadline调度器的相关成员。
构建调度域
为每个cpu自下而上构建调度域结构:
for_each_cpu
for_each_sd_topology
如下是3 level的调度域结构图,其中的span成员表示该调度域管辖的cpu bitmap。最底层的sched_domain结构指针(这里是SMT,arm结构是MC)保存在percpu的runqueue的sd成员中(参见cpu_attach_domain())。
sd_init()是具体负责初始化sched_domain结构的函数,下图展示了sched_domain初始化后的数据结构:
下图是sd_init() 初始化后,各level的sched_domain的sd_flags值:
构建sched_groups
构建sched groups主要在build_sched_groups() 函数中进行,其会percpu、per_sched_domain_level进行初始化。下图是一个2 clusters * 2 cores = 4 cpus的一个示例,展示了sched_group的构建过程和数据结构关系。
需要注意的是,在一个sched_domain_topology_level级别内(span内),所有的sched_group双向链接成环,并被该level的所有sched_domain共享(span内)!
下图则展示了2 clusters * 2 cores * 2 hw-threads = 8 cpus架构的sched domain和group的关系:
下图是一个2 clusters * 2 cores = 4 cpus架构get_group()函数执行的一个示例(看看就行^^):
初始化调度组的cpu_capacity
sched_group_capacity用于指示一个调度组的所有cpu的容量和(参照前面cpu capacity章节),在负载均衡场景,作为均衡一个调度域中不同调度组负载的一个依据。
下图中cluster0包含两个cpu,capacity分别为1535、1535;而cluster1包含的两个cpu的capacity分别为430、430。所以在DIE级别的sgc[0]的sched_group_capacity即为1535+1535=3070;而sgc[1]的sched_group_capacity则为860。
关联cpu和调度域
这个步骤非常简单,由cpu_attach_domain()函数完成,主要是将sched_domain_topology_level最底层的domain以及root domain与cpu的runqueue关联。
这里还做了一个逻辑,就是将sched domain tree中不参与调度或者在调度中没有贡献的域删除,具体参见
sd_parent_degenerate()和sd_degenerate()函数。
example
下面给出一个示例:共有3个cluster
cluster-0: cpu0-cpu2,小核
cluster-1: cpu3-cpu6,中核
cluster-2: cpu7,大核
其sched_domain/sched_group情况如下图:
注意:
- 每个cpu rq的sd指向的最低层次的sched domain, 这里指向的是MC domain
- 每个cpu 都有自己的sched domain实例,即sd地址不一样,cpu-0 MC和cpu-1/2 MC的sd地址不一样
- 每个相同level、相同sched domain中sg是共享的;即cpu-0/1/2 MC domain共享一个sg0, sg1, sg2双向链表;cpu-3/4/5/6 MC domain共享一个sg3, sg4, sg5, sg6双向链表;而cpu-0/1/2/3/4/5/6/7 DIE domain共享一个sg012, sg3456, sg7双向链表。参见前面构建sched groups章节。
- cpu7比较特别,由于cluster2中只有一个cpu7,所以MC domain没必要存在,退化了,只有一个DIE domain了
- 每个cpu sched_domain groups指向比较有意思,指向包含该cpu的sg,后续load balance相关逻辑会依赖这个