# 【Linux内核三十六】进程管理模块:CFS负载均衡(一):sched_domain与sched_group层级构建

内核版本: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)。
  • datasd_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_NEWIDLESD_BALANCE_EXECSD_BALANCE_FORKSD_WAKE_AFFINESD_PREFER_SIBLINGSD_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 共享层才连接 sharednr_busy_cpus 初始为该层 CPU 数(假设全 busy)。private 指向 sd_data,供后续 build_sched_groupssg/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;
}

逻辑很简洁:

  1. sd_init 初始化域默认值。
  2. 如果有子域(不是最底层),设 level = child->level + 1,建立 child->parent = sd 双向链接。
  3. 校验子域 span 必须是父域 span 的子集(cpumask_subset)。如果不满足,说明架构拓扑描述有 bug,打印错误并做 fixup(把子 span 并入父 span)。
  4. set_domain_attribute 处理 relax_domain_level(来自 cpuset 的域松弛级别),如果当前域层级高于请求级别,关闭 SD_BALANCE_WAKESD_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 做三件事(按分配顺序,任一步失败则释放已分配的):

  1. __sdt_alloc:为每层拓扑的每个 CPU 分配 sched_domain / sched_domain_shared / sched_group / sched_group_capacity 对象(kzalloc_node + 变长 cpumask 空间)。
  2. alloc_percpu(struct sched_domain *):分配 per-cpu 指针数组 d.sd,记录每个 CPU 的基域(最底层域)。
  3. 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_allocationssd_data 中已被使用的 per-cpu 指针置 NULL,这样后续 __free_domain_allocs 只释放未使用的对象,避免重复释放。

init_sched_groups_capacity 设置每个组的 group_weightasym_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;
}

逻辑:

  1. covered 是临时掩码,记录已加入链表的组覆盖的 CPU。
  2. for_each_cpu_wrap(i, span, cpu) 从当前 CPU 开始环绕遍历域内所有 CPU。
  3. 对每个未覆盖的 CPU,调 get_group(i, sdd) 获取组,把组的 span 并入 covered
  4. first / last 维护链表头尾,最后 last->next = first 闭合环形链表。
  5. 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-34-70-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:异构算力域,标记是否需要跨算力迁移。

小结

  1. 三层结构描述物理拓扑sched_domain(域)→ sched_group(组)→ sched_group_capacity(容量)。域是 per-CPU 的,通过 parent/child 双链构成从 SMT 到 DIE/NUMA 的层级树;组在域内组成环形链表,代表"下一级域"的颗粒度。域负责纵向(上下层级),组负责横向(同级互比)。
  2. default_topology 表是构建蓝图 :从底到顶列出 SMT → MC → DIE(→ NUMA)各层,每层由 mask 函数(返回 CPU 掩码)和 flags 函数(返回拓扑标志)描述。sd_init 为每层填充通用默认值(imbalance_pct=117、busy_factor=16 等),再按拓扑特征做差异化调整(SMT 更积极 110%、NUMA 更保守关闭 PREFER_SIBLING + 串行化)。
  3. build_sched_domains 三步构建 :先对每个 CPU 从底到顶调 build_sched_domain 构建域链;再对每个域调 build_sched_groups 通过 get_group 组装环形组链表(组 span = 子域 span);最后 claim_allocations 回收未使用对象 + cpu_attach_domain 退化冗余层并挂到 rq->sd
  4. 退化机制精简域树 :单 CPU 或单组的域、与子域 span 相同的冗余父域会被 sd_degenerate / sd_parent_degenerate 删除,减少均衡遍历开销。SD_PREFER_SIBLING 在退化时向下传递。
  5. update_top_cache_domain 设置快速指针 :为每个 CPU 缓存 LLC 域(sd_llc)、NUMA 域(sd_numa)、非对称域等快捷指针,让热路径(select_idle_sibling、NUMA 均衡)无需遍历整棵域树。

下篇将进入负载均衡的运行时路径:rebalance_domains / load_balance 主流程,看调度域树如何被遍历来决定迁移哪些任务。

相关推荐
pride.li21 分钟前
WSL教程-WSL安装与配置Ubuntu
linux
weixin_3077791324 分钟前
Databricks里用PySpark统计指定表和字段中各字段的空值、空字符串或零值比例
运维·数据仓库·python·spark·云计算
躺不平的理查德29 分钟前
Nginx与Ajax 备忘录
linux·运维·服务器
多多鼠31 分钟前
Tool Calling的信任边界:从协议校验到执行沙箱的完整链路设计
运维·开发语言·网络·人工智能·python·langchain
惜离殇37 分钟前
从零开始的敲代码生活--Linux应用软件(文件操作基础2)
linux·文件io·目录io
Neighbor_OldY44 分钟前
【实战复盘】RDP暴力破解与内网横向移动溯源:Windows全套排查命令与事件ID硬核解析
运维·windows·web安全
项目管理实用笔记1 小时前
CI/CD是什么?持续集成持续交付入门
运维·ci/cd·研发效能·项目管理·团队开发
Sunqk56651 小时前
将开发板串口连接到Ubuntu后未找到对应的设备文件?
linux·运维·ubuntu
Sophnet云平台1 小时前
MCP与A2A双协议解析:2026年Agent互操作的技术选型
linux·服务器·网络·上下文·mcp·大模型测评·sophnet