0%

Linux中的调度域和调度组

基本逻辑

Linux内核把CPU核按照调度域和调度组范围进行管理,调度域/调度组是一个层级结构,并
不是只有一层。之所以要把CPU核分组管理,是因为在进行线程负载均衡的时候,“距离”较
近的核的附属资源一般集中在一起,当线程在这些核之间迁移时,因为线程迁移带来的开销
更小。所以,如果线程均衡会优先在“距离”近的核上进行,在达成均衡的目的的同时,尽量
减小开销。

具体的看,当硬件具有SMT时,在同一个物理核的不同虚拟核上迁移时,因为虚拟核共享L1
cache,迁移引发的cache开销是很小的;当线程在cluster内迁移时,因为cluster内的CPU
核可能共享L3 cache,迁移引发的cache开销比较小;当线程在NUMA节点内部迁移时,NUMA
balancing只是重新补上NUMA balancing扫描时断开的页表项,而在不同NUMA节点之间迁移
时,NUMA balancing会进行物理页面的迁移(NUMA balancing的具体逻辑可以参考这里)。

调度域/调度组的概念在内核文档中已经有描述:Documentation/scheduler/sched-domains.rst。
下面是一个包含SMT/cluster/NUMA的CPU系统拓扑图,我们用这个图做具体说明。

1
2
3
4
5
6
7
8
9
+------------------------ NUMA0 ------------------------+-------------------------- NUMA1 ----------------------------+
| | |
+--------- cluster0 --------+--------- cluster1 --------+--------- cluster2 ----------+---------- cluster3 -----------+
| | | | |
+-------------+-------------+-------------+-------------+-------------+---------------+---------------+---------------+
| (p_core0) | (p_core1) | (p_core2) | (p_core3) | (p_core4) | (p_core5) | (p_core6) | (p_core7) |
| | | | | | | | |
| core0 core1 | core2 core3 | core4 core5 | core6 core7 | core8 core9 | core10 core11 | core12 core13 | core14 core16 |
+-------------+-------------+-------------+-------------+-------------+---------------+---------------+---------------+

对于上面的系统,描述调度域的数据结构是这样的,对于每个core,在每个层级上都有一个
sched_domain的数据结构描述调度范围,比如core0在最底层(SMT)的sched_domain里包含
core0和core1,在再上一层(cluster)的sched_domain里包含core0-core3,在NUMA这一层的
sched_domain里包含core0-core7,在最上层的sched_domain里包含全部core。

调度域在一层中收拢所有的CPU核,调度组则为负载均衡增加一个从上到下的视角。比如,
core0的最底层sched_domain里就包含两个调度组,其中一个调度组里包含core0,另一个
调度组里包含core1;core0的cluster这一层的调度域里包含两个调度组,一个调度组包含
core0/core1,另一个调度组里包含core2/core3。

注意,调度里还有一个task_group的概念,task_group是线程的集合,为的是把一组线程作
为一个调度实体参与调度,利用task_group也可以控制线程使用CPU资源的情况。

代码分析

schedule domain和group创建的基本逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
start_kernel
...
+-> kernel_init_freeable
+-> sched_init_smp
+-> sched_init_domains(cpu_active_mask)
+-> doms_cur = alloc_sched_domains(1)
/*
* doms_cur[0]是系统中active cpu的mask,sched domain和group都是在
* 这个函数里创建起来的。
*/
+-> build_sched_domains(doms_cur[0], NULL)

build_sched_domains
/*
* 为各级和各个CPU分配domain/group/domain_shared/group_capacity等的内存,
* 也就是对于每个CPU核在每一个调度层级都有一个domain和group的数据结构(我们
* 先聚焦分析domain和group)。我们在下面把所有的数据结构都写出来,显示到一
* 张图里。
*/
+-> __visit_domain_allocation_hell

+-> for_each_cpu(i, cpu_map) {
for_each_sd_topology(tl) {
+-> build_sched_domain(tl, cpu_map, attr, sd, i)
}
}
+-> for_each_cpu(i, cpu_map) {
for (sd = *per_cpu_ptr(d.sd, i); sd; sd = sd->parent) {
+-> (build_sched_groups(sd, i)
}
}
+-> for_each_cpu(i, cpu_map) {
/* 没有理解LLC imbalanced这里的逻辑? */
}
+-> for_each_cpu(i, cpu_map) {
+-> cpu_attach_domain(sd, d.rd, i)
/* root domain是什么?*/
+-> rq_attach_root(rq, rd)
/* 把core的最底层domain放到core的rq上 */
+-> rcu_assign_pointer(rq->sd, sd)
}

如下的这个图展示如上系统拓扑中所有CPU核相关的sched_domain/sched_group的数据结构,
用d(xxx)表示domain,括号中的数字表示domain里包含的CPU核,用g(xxx)表示group,括号
里的数字表示group里包含的CPU核,d(xxx)下的多个g(xxx)表示一个domain里包含多个group。
如下只画出来一个NUMA中CPU核上的相关数据结构,另外一个NUMA上的数据结构是类似的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
      -----------------------------------------------------------------
NUMA: d(0-15) d(0-15) d(0-15) d(0-15) d(0-15) d(0-15) d(0-15) d(0-15)
g(0-7) g(0-7) g(0-7) g(0-7)
g(8-15) g(8-15) g(8-15) g(8-15) ...

-----------------------------------------------------------------
MC: d(0-7) d(0-7) d(0-7) d(0-7) d(0-7) d(0-7) d(0-7) d(0-7)
g(0-3) g(0-3) g(0-3) g(0-3)
g(3-7) g(3-7) g(3-7) g(3-7) ...

-----------------------------------------------------------------
CLS: d(0-3) d(0-3) d(0-3) d(0-3) d(4-7) d(4-7) d(4-7) d(4-7)
g(0-1) g(0-1) g(0-1) g(0-1)
g(2-3) g(2-3) g(2-3) g(2-3) ...

-----------------------------------------------------------------
SMT: d(0/1) d(0/1) d(2/3) d(2/3) d(4/5) d(4/5) d(6/7) d(6/7)
g(0) g(0) g(2) g(2)
g(1) g(1) g(3) g(3) ...

core0 core1 core2 core3 core4 core5 core6 core7

Debug信息

我们采用目前最新的ARM64版本的qemu(v8.2.50)构造一个如上拓扑结构的系统出来。大概的
命令是这样:

1
2
3
4
5
6
7
8
9
10
11
12
qemu-system-aarch64 \
-smp 16,sockets=2,clusters=2,threads=2 \
-cpu cortex-a57 \
-machine virt \
-append "console=ttyAMA0" \
-nographic -m 4096m \
-kernel ~/repos/linux/arch/arm64/boot/Image \
-initrd ~/tests/kernel_debug_using_qemu/rootfs.cpio.gz \
-object memory-backend-ram,id=mem0,size=2048M \
-object memory-backend-ram,id=mem1,size=2048M \
-numa node,memdev=mem0,nodeid=0,cpus=0-7 \
-numa node,memdev=mem1,nodeid=1,cpus=8-15

如上命令表示系统里有16个core,两个socket(一个socket对应一个NUMA),一个NUMA里包含
两个cluster,一个物理CPU核包含两个逻辑CPU core。

在schedule的debugfs里可以看到如下和domain相关的debug信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# pwd
/sys/kernel/debug/sched
# echo Y > verbose
# cd domains/
# ls
cpu0 cpu10 cpu12 cpu14 cpu2 cpu4 cpu6 cpu8
cpu1 cpu11 cpu13 cpu15 cpu3 cpu5 cpu7 cpu9
# cd cpu2/
# tree
.
├── domain0
│   ├── busy_factor 16
│   ├── cache_nice_tries 0
│   ├── flags
│   ├── groups_flags
│   ├── imbalance_pct 110
│   ├── max_interval 4
│   ├── max_newidle_lb_cost
│   ├── min_interval 2
│   └── name <--- SMT
├── domain1
│   ├── busy_factor 16
│   ├── cache_nice_tries 1
│   ├── flags
│   ├── groups_flags
│   ├── imbalance_pct 117
│   ├── max_interval 8
│   ├── max_newidle_lb_cost
│   ├── min_interval 4
│   └── name <--- CLS
├── domain2
│   ├── busy_factor 16
│   ├── cache_nice_tries 1
│   ├── flags
│   ├── groups_flags
│   ├── imbalance_pct 117
│   ├── max_interval 16
│   ├── max_newidle_lb_cost
│   ├── min_interval 8
│   └── name <--- MC
└── domain3
├── busy_factor 16
├── cache_nice_tries 2
├── flags
├── groups_flags
├── imbalance_pct 117
├── max_interval 32
├── max_newidle_lb_cost
├── min_interval 16
└── name <--- NUMA