内核版本:linux-5.15.140
涉及文件:kernel/sched/topology.c、include/linux/sched/topology.h、kernel/sched/sched.h、include/linux/sched/sd_flags.h
前言
PELT 专题(第 34、35 篇)已经回答了"每个 CPU 的负载有多少"这个问题。但负载均衡要回答的是另一个问题:负载在 CPU 之间该怎么搬。要搬,首先得知道哪些 CPU 算"邻居"------共享 L1/L2 缓存的线程是近邻,同一封装的不同核稍远,跨 NUMA 节点则更远。内核用一棵调度域树来描述这种物理拓扑关系。
本篇从零开始拆解调度域的构建过程,调用关系如下:
sched_init_domains() kernel/sched/topology.c:2325
└── build_sched_domains() kernel/sched/topology.c:2176
├── __visit_domain_allocation_hell() 分配 per-cpu 存储
├── build_sched_domain() kernel/sched/topology.c:2106
│ └── sd_init() kernel/sched/topology.c:1511 填充默认值
├── build_sched_groups() kernel/sched/topology.c:1191 环形链表
│ └── get_group() kernel/sched/topology.c:1147
├── init_sched_groups_capacity() kernel/sched/topology.c:1237
└── cpu_attach_domain() kernel/sched/topology.c:687 退化+挂载
└── update_top_cache_domain() kernel/sched/topology.c:653
一、数据结构总览
调度域体系由四个核心数据结构组成,关系如下:
sched_domain → sched_group → sched_group_capacity
^ ^ ^ ^
`-' `-'
sched_domain_shared(可选,仅 cache 共享层)
1.1 sched_domain:调度域
c
// include/linux/sched/topology.h:80
struct sched_domain {
/* These fields must be setup */
struct sched_domain __rcu *parent; /* top domain must be null terminated */
struct sched_domain __rcu *child; /* bottom domain must be null terminated */
struct sched_group *groups; /* the balancing groups of the domain */
unsigned long min_interval; /* Minimum balance interval ms */
unsigned long max_interval; /* Maximum balance interval ms */
unsigned int busy_factor; /* less balancing by factor if busy */
unsigned int imbalance_pct; /* No balance until over watermark */
unsigned int cache_nice_tries; /* Leave cache hot tasks for # tries */
int nohz_idle; /* NOHZ IDLE status */
int flags; /* See SD_* */
int level;
/* Runtime fields. */
unsigned long last_balance; /* init to jiffies. units in jiffies */
unsigned int balance_interval; /* initialise to 1. units in ms. */
unsigned int nr_balance_failed; /* initialise to 0 */
/* idle_balance() stats */
u64 max_newidle_lb_cost;
unsigned long next_decay_max_lb_cost;
u64 avg_scan_cost; /* select_idle_sibling */
// ... CONFIG_SCHEDSTATS 统计字段省略 ...
union {
void *private; /* used during construction */
struct rcu_head rcu; /* used during destruction */
};
struct sched_domain_shared *shared;
unsigned int span_weight;
/*
* Span of all CPUs in this domain.
*
* NOTE: this field is variable length. (Allocated dynamically
* by attaching extra space to the end of the structure,
* depending on how many CPUs the kernel has booted up with)
*/
unsigned long span[];
};
关键字段:
parent/child:双向链接,构成从底层到顶层的域链。每个 CPU 有自己的域链(per-CPU),底部域的child为 NULL,顶部域的parent为 NULL。groups:指向该域的调度组环形链表头。负载均衡就是在这条链表上找最忙/最闲的组。min_interval/max_interval/balance_interval:控制负载均衡的频率。balance_interval是当前间隔,在每次均衡后根据效果动态调整。imbalance_pct:不平衡阈值百分比。只有当某组负载超过平均值的imbalance_pct%才触发迁移。flags:SD_* 标志位集合(详见下文),描述该域的拓扑特征与调度行为。level:在域链中的层级编号,从 0(最底层)递增。span[]:变长 cpumask,记录该域覆盖的所有 CPU。通过sched_domain_span(sd)获取。private:构建期间指向sd_data,构建完成后被rcu复用(域销毁时走 RCU 回收)。shared:指向sched_domain_shared,仅 cache 共享层(SD_SHARE_PKG_RESOURCES)才设置。
1.2 sched_group:调度组
c
// kernel/sched/sched.h:1816
struct sched_group {
struct sched_group *next; /* Must be a circular list */
atomic_t ref;
unsigned int group_weight;
struct sched_group_capacity *sgc;
int asym_prefer_cpu; /* CPU of highest priority in group */
/*
* The CPUs this group covers.
*
* NOTE: this field is variable length.
*/
unsigned long cpumask[];
};
next:环形链表指针,同一个域内所有组首尾相连。ref:引用计数。一个 sched_group 可能被多个 CPU 的 sched_domain 引用(同一物理组在不同 CPU 的域链中复用),claim_allocations通过引用计数决定是否释放。group_weight:组内 CPU 数量。sgc:指向容量信息(见下文)。asym_prefer_cpu:非对称打包(如大小核)场景下,组内优先级最高的 CPU。cpumask[]:变长掩码,记录组覆盖的 CPU。
1.3 sched_group_capacity:组容量
c
// kernel/sched/sched.h:1797
struct sched_group_capacity {
atomic_t ref;
unsigned long capacity; /* CPU capacity, SCHED_CAPACITY_SCALE = max for 1 CPU */
unsigned long min_capacity; /* Min per-CPU capacity in group */
unsigned long max_capacity; /* Max per-CPU capacity in group */
unsigned long next_update;
int imbalance; /* XXX unrelated to capacity but shared group state */
unsigned long cpumask[]; /* Balance mask */
};
capacity:组的总算力。单核为SCHED_CAPACITY_SCALE(1024),多核则乘以核数。负载均衡时用它来计算每个组应该承担多少负载。min_capacity/max_capacity:组内单 CPU 的最小/最大算力,用于检测异构(大小核)场景。cpumask[]:balance mask,记录哪些 CPU 会参与该组的负载均衡。非 overlap 域中与sched_group->cpumask相同;overlap(NUMA)域中可能不同。
1.4 sched_domain_shared:共享状态
c
// include/linux/sched/topology.h:73
struct sched_domain_shared {
atomic_t ref;
atomic_t nr_busy_cpus;
int has_idle_cores;
int nr_idle_scan;
};
仅在 SD_SHARE_PKG_RESOURCES 层设置。nr_busy_cpus 记录该 cache 共享域内有多少 CPU 处于 busy 状态,供 select_idle_sibling 快速判断是否有空闲核。has_idle_cores / nr_idle_scan 是 SMT 空闲核扫描的优化缓存。
二、拓扑层级描述
2.1 sched_domain_topology_level:一层拓扑
c
// include/linux/sched/topology.h:187
struct sched_domain_topology_level {
sched_domain_mask_f mask; /* 返回该层覆盖的 CPU 掩码 */
sched_domain_flags_f sd_flags; /* 返回该层的 SD_* 拓扑标志 */
int flags; /* SDTL_OVERLAP 等 */
int numa_level;
struct sd_data data; /* per-cpu 存储指针 */
#ifdef CONFIG_SCHED_DEBUG
char *name;
#endif
};
mask:函数指针,入参为 CPU 号,返回该层拓扑在该 CPU 上的掩码。例如 SMT 层返回同核兄弟线程,DIE 层返回所有 CPU。sd_flags:函数指针,返回该层的拓扑标志(如 SMT 层返回SD_SHARE_CPUCAPACITY | SD_SHARE_PKG_RESOURCES)。data:sd_data结构,持有 per-cpu 的 sd/sds/sg/sgc 指针数组,构建时按层分配。
2.2 sd_data:per-cpu 存储容器
c
// include/linux/sched/topology.h:180
struct sd_data {
struct sched_domain *__percpu *sd;
struct sched_domain_shared *__percpu *sds;
struct sched_group *__percpu *sg;
struct sched_group_capacity *__percpu *sgc;
};
每层拓扑都有一个 sd_data。__sdt_alloc 为每个 CPU 在每层拓扑上分配一个 sched_domain、一个 sched_domain_shared、一个 sched_group、一个 sched_group_capacity 实例。构建过程就是填充这些预分配的对象。
2.3 default_topology:默认拓扑表
c
// kernel/sched/topology.c:1626
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, },
};
自底向上排列,以 NULL 结尾。典型 x86 多核 + SMT 系统的拓扑为:
DIE [0-7] [0-7] [0-7] [0-7] [0-7] [0-7] [0-7] [0-7]
MC [0-3] [0-3] [0-3] [0-3] [4-7] [4-7] [4-7] [4-7]
SMT [0-1] [0-1] [2-3] [2-3] [4-5] [4-5] [6-7] [6-7]
0 1 2 3 4 5 6 7 ← CPU
for_each_sd_topology(tl) 从底到顶遍历这张表。NUMA 层(如果开启)会在 sched_init_domains 之前通过 sched_init_numa() 动态追加到 sched_domain_topology 指针后面。
标志函数定义在 topology.h 中:
c
// include/linux/sched/topology.h:38-57
#ifdef CONFIG_SCHED_SMT
static inline int cpu_smt_flags(void)
{
return SD_SHARE_CPUCAPACITY | SD_SHARE_PKG_RESOURCES;
}
#endif
#ifdef CONFIG_SCHED_MC
static inline int cpu_core_flags(void)
{
return SD_SHARE_PKG_RESOURCES;
}
#endif
#ifdef CONFIG_NUMA
static inline int cpu_numa_flags(void)
{
return SD_NUMA;
}
#endif
拓扑标志的含义(include/linux/sched/sd_flags.h):
| 标志 | 含义 | metaflags |
|---|---|---|
SD_SHARE_CPUCAPACITY |
域内 CPU 共享算力(SMT) | SHARED_CHILD, NEEDS_GROUPS |
SD_SHARE_PKG_RESOURCES |
域内共享缓存 | SHARED_CHILD, NEEDS_GROUPS |
SD_NUMA |
跨 NUMA 节点 | SHARED_PARENT, NEEDS_GROUPS |
SD_ASYM_PACKING |
非对称打包(大小核) | SHARED_CHILD, NEEDS_GROUPS |
SD_ASYM_CPUCAPACITY |
域内 CPU 算力不同 | SHARED_PARENT, NEEDS_GROUPS |
SD_PREFER_SIBLING |
优先向兄弟域放任务 | NEEDS_GROUPS |
SD_SERIALIZE |
只允许一个均衡实例 | SHARED_PARENT, NEEDS_GROUPS |
SD_OVERLAP |
组掩码可重叠(NUMA) | SHARED_PARENT, NEEDS_GROUPS |
SHARED_CHILD 表示该标志从底层向上传播(子有则父也应有),SHARED_PARENT 反向传播。NEEDS_GROUPS 表示该标志只在域有多个组时才有意义,只有一个组时可安全删除(退化)。
三、sd_init:调度域默认值初始化
c
// kernel/sched/topology.c:1511
static struct sched_domain *
sd_init(struct sched_domain_topology_level *tl,
const struct cpumask *cpu_map,
struct sched_domain *child, int cpu)
{
struct sd_data *sdd = &tl->data;
struct sched_domain *sd = *per_cpu_ptr(sdd->sd, cpu);
int sd_id, sd_weight, sd_flags = 0;
struct cpumask *sd_span;
sd_weight = cpumask_weight(tl->mask(cpu));
if (tl->sd_flags)
sd_flags = (*tl->sd_flags)();
*sd = (struct sched_domain){
.min_interval = sd_weight,
.max_interval = 2*sd_weight,
.busy_factor = 16,
.imbalance_pct = 117,
.cache_nice_tries = 0,
.flags = 1*SD_BALANCE_NEWIDLE
| 1*SD_BALANCE_EXEC
| 1*SD_BALANCE_FORK
| 0*SD_BALANCE_WAKE
| 1*SD_WAKE_AFFINE
| 0*SD_SHARE_CPUCAPACITY
| 0*SD_SHARE_PKG_RESOURCES
| 0*SD_SERIALIZE
| 1*SD_PREFER_SIBLING
| 0*SD_NUMA
| sd_flags
,
.last_balance = jiffies,
.balance_interval = sd_weight,
.max_newidle_lb_cost = 0,
.next_decay_max_lb_cost = jiffies,
.child = child,
};
sd_span = sched_domain_span(sd);
cpumask_and(sd_span, cpu_map, tl->mask(cpu));
sd_id = cpumask_first(sd_span);
sd->flags |= asym_cpu_capacity_classify(sd_span, cpu_map);
sd_init 为每个域设置通用默认值,然后根据拓扑标志做差异化调整。
通用默认值:
min_interval = max_interval = balance_interval = sd_weight:均衡间隔初始为该域 CPU 数。CPU 越多,间隔越长(减少不必要的均衡开销)。busy_factor = 16:系统繁忙时均衡间隔乘以 16。imbalance_pct = 117:默认不平衡阈值 117%,即负载超过平均值的 17% 才触发迁移。cache_nice_tries = 0:默认不因 cache 亲和而跳过迁移。
默认 flags :开启 SD_BALANCE_NEWIDLE、SD_BALANCE_EXEC、SD_BALANCE_FORK、SD_WAKE_AFFINE、SD_PREFER_SIBLING。SD_SHARE_* 和 SD_NUMA 初始为 0,由 sd_flags(来自拓扑层级的标志函数)覆盖。
按拓扑特征调整:
c
// kernel/sched/topology.c:1584
if (sd->flags & SD_SHARE_CPUCAPACITY) {
sd->imbalance_pct = 110; // SMT 层阈值更低,更积极均衡
} else if (sd->flags & SD_SHARE_PKG_RESOURCES) {
sd->imbalance_pct = 117;
sd->cache_nice_tries = 1; // MC 层容忍 1 次 cache 亲和
#ifdef CONFIG_NUMA
} else if (sd->flags & SD_NUMA) {
sd->cache_nice_tries = 2; // NUMA 层容忍 2 次
sd->flags &= ~SD_PREFER_SIBLING; // NUMA 不优先兄弟
sd->flags |= SD_SERIALIZE; // NUMA 层串行化均衡
if (sched_domains_numa_distance[tl->numa_level] > node_reclaim_distance) {
sd->flags &= ~(SD_BALANCE_EXEC |
SD_BALANCE_FORK |
SD_WAKE_AFFINE); // 远距离 NUMA 关闭 exec/fork/affine
}
#endif
} else {
sd->cache_nice_tries = 1;
}
设计逻辑很清晰:越远的域,容忍度越高(cache_nice_tries 更大),但均衡策略越保守(NUMA 关闭 PREFER_SIBLING 并串行化),避免远距离迁移破坏 cache 亲和。
随后设置 sched_domain_shared:
c
// kernel/sched/topology.c:1612
if (sd->flags & SD_SHARE_PKG_RESOURCES) {
sd->shared = *per_cpu_ptr(sdd->sds, sd_id);
atomic_inc(&sd->shared->ref);
atomic_set(&sd->shared->nr_busy_cpus, sd_weight);
}
sd->private = sdd;
return sd;
}
只有 cache 共享层才连接 shared,nr_busy_cpus 初始为该层 CPU 数(假设全 busy)。private 指向 sd_data,供后续 build_sched_groups 取 sg/sgc。
四、build_sched_domain:逐 CPU 构建域链
c
// kernel/sched/topology.c:2106
static struct sched_domain *build_sched_domain(struct sched_domain_topology_level *tl,
const struct cpumask *cpu_map, struct sched_domain_attr *attr,
struct sched_domain *child, int cpu)
{
struct sched_domain *sd = sd_init(tl, cpu_map, child, cpu);
if (child) {
sd->level = child->level + 1;
sched_domain_level_max = max(sched_domain_level_max, sd->level);
child->parent = sd;
if (!cpumask_subset(sched_domain_span(child),
sched_domain_span(sd))) {
pr_err("BUG: arch topology borken\n");
/* Fixup, ensure @sd has at least @child CPUs. */
cpumask_or(sched_domain_span(sd),
sched_domain_span(sd),
sched_domain_span(child));
}
}
set_domain_attribute(sd, attr);
return sd;
}
逻辑很简洁:
- 调
sd_init初始化域默认值。 - 如果有子域(不是最底层),设
level = child->level + 1,建立child->parent = sd双向链接。 - 校验子域 span 必须是父域 span 的子集(
cpumask_subset)。如果不满足,说明架构拓扑描述有 bug,打印错误并做 fixup(把子 span 并入父 span)。 - 调
set_domain_attribute处理relax_domain_level(来自 cpuset 的域松弛级别),如果当前域层级高于请求级别,关闭SD_BALANCE_WAKE和SD_BALANCE_NEWIDLE(减少高层级的空闲均衡)。
五、build_sched_domains:构建主流程
c
// kernel/sched/topology.c:2176
static int
build_sched_domains(const struct cpumask *cpu_map, struct sched_domain_attr *attr)
{
enum s_alloc alloc_state = sa_none;
struct sched_domain *sd;
struct s_data d;
struct rq *rq = NULL;
int i, ret = -ENOMEM;
bool has_asym = false;
if (WARN_ON(cpumask_empty(cpu_map)))
goto error;
alloc_state = __visit_domain_allocation_hell(&d, cpu_map);
if (alloc_state != sa_rootdomain)
goto error;
__visit_domain_allocation_hell 做三件事(按分配顺序,任一步失败则释放已分配的):
__sdt_alloc:为每层拓扑的每个 CPU 分配 sched_domain / sched_domain_shared / sched_group / sched_group_capacity 对象(kzalloc_node+ 变长 cpumask 空间)。alloc_percpu(struct sched_domain *):分配 per-cpu 指针数组d.sd,记录每个 CPU 的基域(最底层域)。alloc_rootdomain:分配 root_domain,管理全局调度状态(如 DL 带宽、max_cpu_capacity)。
分配完成后进入构建阶段,分三步走:
第一步:构建域链
c
// kernel/sched/topology.c:2194
for_each_cpu(i, cpu_map) {
struct sched_domain_topology_level *tl;
sd = NULL;
for_each_sd_topology(tl) {
if (WARN_ON(!topology_span_sane(tl, cpu_map, i)))
goto error;
sd = build_sched_domain(tl, cpu_map, attr, sd, i);
has_asym |= sd->flags & SD_ASYM_CPUCAPACITY;
if (tl == sched_domain_topology)
*per_cpu_ptr(d.sd, i) = sd;
if (tl->flags & SDTL_OVERLAP)
sd->flags |= SD_OVERLAP;
if (cpumask_equal(cpu_map, sched_domain_span(sd)))
break;
}
}
对每个 CPU,从底到顶遍历拓扑层级,每次调 build_sched_domain(tl, cpu_map, attr, sd, i),把上一次构建的 sd 作为 child 传入,形成从 SMT → MC → DIE(→ NUMA)的域链。
topology_span_sane 检查非 NUMA 层的掩码不能部分重叠(要么完全相同,要么完全不相交),否则后续 get_group 的环形链表链接会断裂。
当某层域的 span 已经覆盖整个 cpu_map 时(cpumask_equal),提前终止向上构建------不需要更高层的域。
*per_cpu_ptr(d.sd, i) = sd 记录每个 CPU 的最底层域(构建的起点),供后续 group 构建和挂载使用。
第二步:构建调度组环形链表
c
// kernel/sched/topology.c:2217
for_each_cpu(i, cpu_map) {
for (sd = *per_cpu_ptr(d.sd, i); sd; sd = sd->parent) {
sd->span_weight = cpumask_weight(sched_domain_span(sd));
if (sd->flags & SD_OVERLAP) {
if (build_overlap_sched_groups(sd, i))
goto error;
} else {
if (build_sched_groups(sd, i))
goto error;
}
}
}
对每个 CPU,沿域链从底向上遍历每个域,调 build_sched_groups(非重叠域)或 build_overlap_sched_groups(NUMA 重叠域)构建环形链表。span_weight 记录域内 CPU 数,供负载均衡计算使用。
第三步:容量初始化与挂载
c
// kernel/sched/topology.c:2230
for (i = nr_cpumask_bits-1; i >= 0; i--) {
if (!cpumask_test_cpu(i, cpu_map))
continue;
for (sd = *per_cpu_ptr(d.sd, i); sd; sd = sd->parent) {
claim_allocations(i, sd);
init_sched_groups_capacity(i, sd);
}
}
rcu_read_lock();
for_each_cpu(i, cpu_map) {
rq = cpu_rq(i);
sd = *per_cpu_ptr(d.sd, i);
if (rq->cpu_capacity_orig > READ_ONCE(d.rd->max_cpu_capacity))
WRITE_ONCE(d.rd->max_cpu_capacity, rq->cpu_capacity_orig);
cpu_attach_domain(sd, d.rd, i);
}
rcu_read_unlock();
注意这里逆序遍历 CPU(nr_cpumask_bits-1 到 0),这样高编号 CPU 先执行 claim_allocations,低编号 CPU 后执行------但 claim_allocations 是幂等的(通过引用计数判断),所以顺序不影响正确性。
claim_allocations 把 sd_data 中已被使用的 per-cpu 指针置 NULL,这样后续 __free_domain_allocs 只释放未使用的对象,避免重复释放。
init_sched_groups_capacity 设置每个组的 group_weight 和 asym_prefer_cpu,然后调 update_group_capacity(见第 34 篇涉及的 SCHED_CAPACITY_SCALE)计算组容量。
最后 cpu_attach_domain 把域链挂到 rq->sd 上(RCU 保护)。
六、get_group与build_sched_groups:环形链表构建
6.1 get_group:获取或初始化一个组
c
// kernel/sched/topology.c:1147
static struct sched_group *get_group(int cpu, struct sd_data *sdd)
{
struct sched_domain *sd = *per_cpu_ptr(sdd->sd, cpu);
struct sched_domain *child = sd->child;
struct sched_group *sg;
bool already_visited;
if (child)
cpu = cpumask_first(sched_domain_span(child));
sg = *per_cpu_ptr(sdd->sg, cpu);
sg->sgc = *per_cpu_ptr(sdd->sgc, cpu);
/* Increase refcounts for claim_allocations: */
already_visited = atomic_inc_return(&sg->ref) > 1;
WARN_ON(already_visited != (atomic_inc_return(&sg->sgc->ref) > 1));
if (already_visited)
return sg;
if (child) {
cpumask_copy(sched_group_span(sg), sched_domain_span(child));
cpumask_copy(group_balance_mask(sg), sched_group_span(sg));
} else {
cpumask_set_cpu(cpu, sched_group_span(sg));
cpumask_set_cpu(cpu, group_balance_mask(sg));
}
sg->sgc->capacity = SCHED_CAPACITY_SCALE * cpumask_weight(sched_group_span(sg));
sg->sgc->min_capacity = SCHED_CAPACITY_SCALE;
sg->sgc->max_capacity = SCHED_CAPACITY_SCALE;
return sg;
}
核心思路:一个域的调度组就是它的子域。
- 如果有子域(
child != NULL),组的 span = 子域的 span。cpu被重定向到子域 span 的首 CPU,以复用同一物理组。 - 如果没有子域(最底层),组只包含单个 CPU。
already_visited 通过引用计数判断:如果 atomic_inc_return(&sg->ref) > 1,说明之前已经有别的 CPU 访问过这个组(同一物理组被多个 CPU 的域引用),那 span 已经初始化好了,直接返回即可。
6.2 build_sched_groups:组装环形链表
c
// kernel/sched/topology.c:1191
static int
build_sched_groups(struct sched_domain *sd, int cpu)
{
struct sched_group *first = NULL, *last = NULL;
struct sd_data *sdd = sd->private;
const struct cpumask *span = sched_domain_span(sd);
struct cpumask *covered;
int i;
covered = sched_domains_tmpmask;
cpumask_clear(covered);
for_each_cpu_wrap(i, span, cpu) {
struct sched_group *sg;
if (cpumask_test_cpu(i, covered))
continue;
sg = get_group(i, sdd);
cpumask_or(covered, covered, sched_group_span(sg));
if (!first)
first = sg;
if (last)
last->next = sg;
last = sg;
}
last->next = first;
sd->groups = first;
return 0;
}
逻辑:
covered是临时掩码,记录已加入链表的组覆盖的 CPU。for_each_cpu_wrap(i, span, cpu)从当前 CPU 开始环绕遍历域内所有 CPU。- 对每个未覆盖的 CPU,调
get_group(i, sdd)获取组,把组的 span 并入covered。 - 用
first/last维护链表头尾,最后last->next = first闭合环形链表。 sd->groups = first把链表头挂到域上。
以 CPU0 在 DIE 层为例(8 核 2 核 2 线程),DIE 域 span = 0-7,遍历到 CPU0 时 get_group(0) 返回 MC 层的 0-3 组,covered 设为 0-3;继续遍历到 CPU4 时 get_group(4) 返回 4-7 组,covered 变为 0-7 全覆盖,链表终止。最终 DIE 域有 2 个组:0-3 → 4-7 → 0-3(环)。
七、cpu_attach_domain:退化与挂载
构建好的域链在挂到 rq->sd 之前还要经过退化处理。
c
// kernel/sched/topology.c:687
static void
cpu_attach_domain(struct sched_domain *sd, struct root_domain *rd, int cpu)
{
struct rq *rq = cpu_rq(cpu);
struct sched_domain *tmp;
int numa_distance = 0;
/* Remove the sched domains which do not contribute to scheduling. */
for (tmp = sd; tmp; ) {
struct sched_domain *parent = tmp->parent;
if (!parent)
break;
if (sd_parent_degenerate(tmp, parent)) {
tmp->parent = parent->parent;
if (parent->parent)
parent->parent->child = tmp;
if (parent->flags & SD_PREFER_SIBLING)
tmp->flags |= SD_PREFER_SIBLING;
destroy_sched_domain(parent);
} else
tmp = tmp->parent;
}
if (sd && sd_degenerate(sd)) {
tmp = sd;
sd = sd->parent;
destroy_sched_domain(tmp);
if (sd)
sd->child = NULL;
}
退化(degenerate) 的目的是删除不必要的中间域层,减少均衡开销。两种退化:
sd_degenerate(sd):域只有 1 个 CPU 或只有 1 个组且无SD_WAKE_AFFINE标志 → 该域不提供额外信息,可删除。sd_parent_degenerate(sd, parent):父子域 span 相同且父域没有子域缺少的标志 → 父域冗余,可删除,把SD_PREFER_SIBLING传递给子域。
典型场景:单核非 SMT 系统,SMT 层只有一个 CPU,整个 SMT 域退化掉。
退化完成后挂载:
c
// kernel/sched/topology.c:728
rq_attach_root(rq, rd);
tmp = rq->sd;
rcu_assign_pointer(rq->sd, sd);
dirty_sched_domain_sysctl(cpu);
destroy_sched_domains(tmp);
update_top_cache_domain(cpu);
}
rq->sd 通过 RCU 更新(rcu_assign_pointer),旧的域链通过 destroy_sched_domains 走 RCU 回收。rq 结构体中的域指针(kernel/sched/sched.h:1002):
c
struct sched_domain __rcu *sd;
最后调 update_top_cache_domain 设置 per-CPU 快速缓存。
八、update_top_cache_domain:per-CPU 快速缓存
c
// kernel/sched/topology.c:653
static void update_top_cache_domain(int cpu)
{
struct sched_domain_shared *sds = NULL;
struct sched_domain *sd;
int id = cpu;
int size = 1;
sd = highest_flag_domain(cpu, SD_SHARE_PKG_RESOURCES);
if (sd) {
id = cpumask_first(sched_domain_span(sd));
size = cpumask_weight(sched_domain_span(sd));
sds = sd->shared;
}
rcu_assign_pointer(per_cpu(sd_llc, cpu), sd);
per_cpu(sd_llc_size, cpu) = size;
per_cpu(sd_llc_id, cpu) = id;
rcu_assign_pointer(per_cpu(sd_llc_shared, cpu), sds);
sd = lowest_flag_domain(cpu, SD_NUMA);
rcu_assign_pointer(per_cpu(sd_numa, cpu), sd);
sd = highest_flag_domain(cpu, SD_ASYM_PACKING);
rcu_assign_pointer(per_cpu(sd_asym_packing, cpu), sd);
sd = lowest_flag_domain(cpu, SD_ASYM_CPUCAPACITY_FULL);
rcu_assign_pointer(per_cpu(sd_asym_cpucapacity, cpu), sd);
}
这些 per-CPU 变量(声明在 kernel/sched/sched.h:1783)是调度域树的"快捷指针",让热路径无需遍历整棵树:
sd_llc:最高 cache 共享域(Last Level Cache),供select_idle_sibling查找空闲核。sd_numa:最低 NUMA 域,供 NUMA 均衡使用。sd_asym_packing:非对称打包域,供大小核选核。sd_asym_cpucapacity:异构算力域,标记是否需要跨算力迁移。
小结
- 三层结构描述物理拓扑 :
sched_domain(域)→sched_group(组)→sched_group_capacity(容量)。域是 per-CPU 的,通过 parent/child 双链构成从 SMT 到 DIE/NUMA 的层级树;组在域内组成环形链表,代表"下一级域"的颗粒度。域负责纵向(上下层级),组负责横向(同级互比)。 - default_topology 表是构建蓝图 :从底到顶列出 SMT → MC → DIE(→ NUMA)各层,每层由 mask 函数(返回 CPU 掩码)和 flags 函数(返回拓扑标志)描述。
sd_init为每层填充通用默认值(imbalance_pct=117、busy_factor=16 等),再按拓扑特征做差异化调整(SMT 更积极 110%、NUMA 更保守关闭 PREFER_SIBLING + 串行化)。 - build_sched_domains 三步构建 :先对每个 CPU 从底到顶调
build_sched_domain构建域链;再对每个域调build_sched_groups通过get_group组装环形组链表(组 span = 子域 span);最后claim_allocations回收未使用对象 +cpu_attach_domain退化冗余层并挂到rq->sd。 - 退化机制精简域树 :单 CPU 或单组的域、与子域 span 相同的冗余父域会被
sd_degenerate/sd_parent_degenerate删除,减少均衡遍历开销。SD_PREFER_SIBLING在退化时向下传递。 - update_top_cache_domain 设置快速指针 :为每个 CPU 缓存 LLC 域(
sd_llc)、NUMA 域(sd_numa)、非对称域等快捷指针,让热路径(select_idle_sibling、NUMA 均衡)无需遍历整棵域树。
下篇将进入负载均衡的运行时路径:rebalance_domains / load_balance 主流程,看调度域树如何被遍历来决定迁移哪些任务。