4、调度域初始化:sched_init_domains函数流程、CPU拓扑构建、节点与缓存层级映射
调度域(Scheduling Domain)是Linux内核在NUMA架构下实现负载均衡的核心数据结构。它描述了CPU之间的拓扑关系、缓存层级以及内存访问代价,是调度器感知NUMA并做出智能决策的基础。本章将深入分析调度域的初始化流程,重点解析sched_init_domains函数的执行路径、CPU拓扑的构建方法,以及节点与缓存层级的映射关系。
4.1 sched_init_domains函数流程
sched_init_domains是调度域初始化的入口函数,在内核启动阶段由sched_init_smp调用。该函数的核心任务是遍历所有可能的CPU,为每个CPU构建从底层到顶层的完整调度域层级。
关键路径: start_kernel → rest_init → kernel_init → kernel_init_freeable → smp_init → sched_init_smp → sched_init_domains
4.1.1 函数调用流程
- 初始化域层级数组:清零
sched_domain_topology,准备存储各层级的构建函数指针。 - 注册默认拓扑层级:调用
set_sched_topology注册默认的CPU拓扑层级描述。 - 遍历所有CPU:对每个在线CPU,调用
build_sched_domains构建其调度域树。 - 检测与修复:调用
sched_domain_debug进行一致性检查,修复可能的域层级断裂。 - 激活调度域:设置
sched_domain_span位图,使能负载均衡。
| 步骤 | 函数/操作 | 说明 |
|---|---|---|
| 1 | init_sched_domain_topology | 初始化拓扑层级数组,设置默认层级数 |
| 2 | set_sched_topology | 注册CPU拓扑描述符,包含各层级的构建回调 |
| 3 | build_sched_domains | 为单个CPU构建完整的调度域树,包含域、组和标志 |
| 4 | sched_domain_debug | 验证域层级完整性,输出调试信息 |
| 5 | update_sched_domain | 激活调度域,使能周期性负载均衡 |
4.1.2 核心数据结构关系
调度域初始化涉及三个核心数据结构:sched_domain(调度域)、sched_group(调度组)和cpumask(CPU位图)。它们之间的关系如下:
// 调度域结构体(简化)
struct sched_domain {
struct sched_domain *parent; // 指向父域
struct sched_domain *child; // 指向子域
struct sched_group *groups; // 本域内的调度组链表
unsigned long *span; // 本域覆盖的CPU位图
int level; // 域层级(0=最底层)
unsigned int flags; // 域标志(如SD_BALANCE_NEWIDLE)
unsigned int imbalance_pct; // 负载不均衡阈值百分比
unsigned int cache_nice_tries; // 缓存亲和性尝试次数
};
// 调度组结构体(简化)
struct sched_group {
struct sched_group *next; // 链表下一个组
unsigned long *cpumask; // 本组包含的CPU位图
unsigned int group_weight; // 组内CPU数量
enum group_type type; // 组类型(平衡/非平衡)
};
设计要点:调度域采用树形层级结构,每个域包含多个调度组。组是负载均衡的基本单元,域定义了均衡的范围。从底层(单个CPU或SMT线程)到顶层(整个NUMA节点或全系统),层级逐级合并,形成完整的拓扑视图。
4.2 CPU拓扑构建
CPU拓扑构建是调度域初始化的核心环节。内核通过cpu_topology数组记录每个CPU的物理拓扑信息,包括核心ID、簇ID、NUMA节点ID等。调度域构建函数build_sched_domains利用这些信息,自底向上创建域层级。
4.2.1 拓扑层级定义
默认的CPU拓扑层级由default_topology[]数组定义,每个层级包含一个cpu_mask函数和一个sd_flags函数:
// 默认CPU拓扑层级定义(简化)
static struct sched_domain_topology_level default_topology[] = {
// 第0层:SMT(同步多线程)
{ cpu_smt_mask, cpu_smt_flags, SD_INIT_NAME(SMT) },
// 第1层:MC(多核,共享L2/L3缓存)
{ cpu_coregroup_mask, cpu_core_flags, SD_INIT_NAME(MC) },
// 第2层:DIE(芯片内,共享L3缓存)
{ cpu_die_mask, cpu_die_flags, SD_INIT_NAME(DIE) },
// 第3层:NUMA(跨节点)
{ cpu_numa_mask, cpu_numa_flags, SD_INIT_NAME(NUMA) },
{ NULL, },
};
| 层级名称 | 掩码函数 | 覆盖范围 | 典型粒度 |
|---|---|---|---|
| SMT | cpu_smt_mask | 同一物理核心的硬件线程 | 2-4个CPU |
| MC | cpu_coregroup_mask | 共享最后一级缓存的CPU簇 | 4-16个CPU |
| DIE | cpu_die_mask | 同一芯片封装内的所有CPU | 8-64个CPU |
| NUMA | cpu_numa_mask | 同一NUMA节点内的所有CPU | 16-256个CPU |
4.2.2 构建过程详解
build_sched_domains函数为每个CPU执行以下步骤:
- 分配域节点:从
sched_domains内存池中分配sched_domain结构体。 - 设置CPU位图:调用层级对应的
cpu_mask函数,获取本域应包含的CPU集合。 - 创建调度组:根据CPU位图划分调度组,每个组包含一个或多个CPU。
- 设置域标志:调用
sd_flags函数,根据层级特性设置SD_BALANCE_*等标志。 - 链接父子关系:将当前域设置为上一层的子域,形成树形结构。
- 计算均衡参数:设置
imbalance_pct、cache_nice_tries等调优参数。
关键算法:调度组的划分采用"最小化组间通信代价"策略。在同一域内,内核尽量将共享缓存的CPU划分到同一组,以减少跨组迁移时的缓存失效开销。
4.3 节点与缓存层级映射
NUMA节点与缓存层级的映射关系决定了调度域的结构深度和负载均衡策略。内核通过numa_distance表和cache_shared_cpu_map来建立这种映射。
4.3.1 NUMA节点距离矩阵
每个NUMA节点对之间都有一个"距离"值,存储在numa_distance矩阵中。距离值越小,表示内存访问延迟越低,调度域合并的优先级越高。
// NUMA距离矩阵示例(4节点系统)
// 节点0 节点1 节点2 节点3
// [10, 20, 30, 40] // 节点0到各节点的距离
// [20, 10, 40, 30] // 节点1到各节点的距离
// [30, 40, 10, 20] // 节点2到各节点的距离
// [40, 30, 20, 10] // 节点3到各节点的距离
// 内核根据距离阈值决定是否合并域
#define NUMA_DISTANCE_THRESHOLD 20 // 距离小于等于20视为"近邻"
4.3.2 缓存层级映射规则
缓存层级映射遵循以下规则:
- L1/L2缓存:通常为每个CPU核心私有,不参与调度域映射。
- L3缓存(LLC):同一簇内CPU共享,映射到MC层级。
- 跨芯片缓存:同一NUMA节点内可能包含多个芯片,映射到DIE层级。
- 跨节点缓存:不同NUMA节点之间无共享缓存,映射到NUMA层级。
注意:在AMD Zen/Zen2/Zen3架构中,CCX(Core Complex)内部的L3缓存是共享的,但跨CCX的L3缓存不共享。这导致MC层级可能进一步细分为CCX域,需要内核通过ACPI SRAT表或CPUID指令动态识别。