4. 调度域(Scheduling Domain)概念

4.1 调度域的定义

调度域(Scheduling Domain)是 Linux 内核在 CFS(完全公平调度器) 之上构建的一组逻辑 CPU 集合,用于描述 CPU 之间的 负载均衡层级。每个调度域对应一个特定的拓扑层次(如单核、多核、NUMA 节点、整个系统),域内的 CPU 共享相同的调度策略和负载均衡规则。

核心定义: 调度域 = { CPU 位图 + 负载均衡策略 + 子域指针 + 父域指针 }。内核通过递归遍历调度域层级,实现从底层 CPU 到跨 NUMA 节点的全局负载均衡。

调度域的关键属性包括:

  • span(CPU 位图):该域覆盖的所有 CPU 编号。
  • level(层级编号):从底层(0)到顶层(N-1)递增。
  • flags(标志位):控制负载均衡行为,如 SD_BALANCE_NEWIDLE、SD_BALANCE_EXEC、SD_BALANCE_FORK。
  • child(子域指针):指向更细粒度的下层域。
  • parent(父域指针):指向更粗粒度的上层域。
  • groups(调度组链表):域内划分的 CPU 组,用于组间负载均衡。

4.2 调度域的层级结构

调度域采用 树形层级 组织,从最底层的物理 CPU 开始,逐层向上合并,直到覆盖整个系统。典型的层级结构如下:

层级 名称 覆盖范围 典型场景
0 MC(Multi-Core) 单个物理 CPU 的所有核心 超线程 / 同核共享 L2 缓存
1 DIE(Die) 单个 NUMA 节点内的所有 CPU 同节点共享 L3 缓存 / 内存控制器
2 NUMA(Non-Uniform Memory Access) 跨 NUMA 节点的 CPU 集合 跨节点内存访问延迟差异
3 ALL(System) 系统中所有 CPU 全局负载均衡兜底
注意: 层级数量取决于硬件拓扑。在单 NUMA 节点系统中,MC 域直接作为顶层域;在大型 NUMA 系统中,层级可能扩展到 4 层甚至更多(如 AMD EPYC 的 CCD → CCX → NUMA → System)。

调度域层级结构的核心遍历逻辑如下:

// 从当前 CPU 的底层域开始,逐层向上执行负载均衡
for_each_domain(cpu, sd) {
    // sd 指向当前层级的调度域
    // 执行该域内的负载均衡操作
    load_balance(cpu, sd, idle);
    // 通过 sd->parent 进入上一层域
    sd = sd->parent;
}

4.3 调度域与 CPU 拓扑的关系

调度域是 CPU 拓扑的软件抽象。内核在启动时通过 sched_init_topology() 函数,读取 ACPI/DTB 中的 CPU 拓扑信息,动态构建调度域树。CPU 拓扑的物理结构直接决定了调度域的层级数量和覆盖范围。

以下 SVG 图展示了典型的 2-NUMA 节点系统中,调度域与 CPU 拓扑的映射关系:

调度域层级与 CPU 拓扑映射(2-NUMA 节点系统) SD_ALL(系统域) SD_NUMA(节点 0) SD_NUMA(节点 1) SD_DIE(节点0-DIE0) SD_DIE(节点0-DIE1) SD_DIE(节点1-DIE0) MC(CPU0-1) MC(CPU2-3) MC(CPU4-5) MC(CPU6-7) MC(CPU8-9) CPU0 CPU1 CPU2 CPU3 CPU4 CPU5 CPU6 说明: • 每个调度域包含一个 CPU 位图(span),表示该域覆盖的 CPU 集合。 • 负载均衡从底层 MC 域开始,逐层向上执行,直到系统域。

4.4 调度域的关键数据结构

内核使用 struct sched_domain 描述每个调度域,其核心字段如下:

struct sched_domain {
    struct sched_domain *parent;    /* 指向父域(上层) */
    struct sched_domain *child;     /* 指向子域(下层) */
    struct sched_group  *groups;    /* 该域内的调度组链表 */
    unsigned long        span[0];   /* CPU 位图,标记该域包含的 CPU */
    int                  level;     /* 层级编号,从 0 开始 */
    unsigned int         flags;     /* 控制标志位 */
    unsigned int         imbalance_pct; /* 负载不均衡阈值百分比 */
    unsigned int         cache_nice_tries; /* 缓存亲和性尝试次数 */
    unsigned int         busy_factor;      /* 繁忙因子 */
    unsigned int         max_newidle_lb_cost; /* 新空闲负载均衡最大开销 */
    unsigned int         max_interval;      /* 负载均衡最大间隔 */
    unsigned int         min_interval;      /* 负载均衡最小间隔 */
};
关键理解: 调度域不是静态的。当 CPU 热插拔、cgroup 配置变化或 NUMA 节点状态改变时,内核会通过 sched_domain_debug() 和 sched_init_domains() 重新构建调度域树。错误的拓扑配置会导致负载均衡失效,表现为 CPU 利用率不均、跨节点内存访问延迟增加。

4.5 调度域与负载均衡的交互流程

调度域是负载均衡的 执行容器。每次负载均衡触发时,内核从当前 CPU 所属的 最底层调度域 开始,逐层向上检查是否需要迁移任务。具体流程如下:

  1. 触发条件:任务唤醒、任务 fork、CPU 空闲、定时器触发。
  2. 域内扫描:在当前调度域内,遍历所有调度组,计算组间负载差异。
  3. 任务迁移:如果负载差异超过 imbalance_pct 阈值,从最忙组迁移任务到最闲组。
  4. 层级上升:如果当前域无法平衡(所有组负载接近),通过 parent 指针进入上层域。
  5. 全局兜底:到达系统域(SD_ALL)后,若仍不平衡,执行跨 NUMA 节点迁移。

以下 SVG 图展示了负载均衡在调度域层级中的执行路径:

负载均衡在调度域层级中的执行路径 触发负载均衡 MC 域:组间负载均衡 平衡成功? 结束 否 DIE 域:跨核均衡 NUMA 域:跨节点均衡 执行规则: 1. 从 MC 域开始,逐层向上。 2. 每层先尝试组间平衡。 3. 平衡成功则停止;否则进入上层域。 4. 到达系统域后,执行全局兜底迁移。

4.6 调度域配置的查看与验证

在实际系统中,可以通过 /proc/schedstat 和 /sys/kernel/debug/sched/domains 查看调度域的实时拓扑。以下命令输出示例:

# 查看所有 CPU 的调度域层级
cat /proc/schedstat | grep "domain"

# 输出示例(2-NUMA 节点,每节点 4 核):
# domain0 0-3   (MC 域:CPU0-3)
# domain1 0-7   (NUMA 域:CPU0-7)
# domain2 0-7   (系统域:全部 CPU)

# 查看每个调度域的详细参数
cat /sys/kernel/debug/sched/domains/cpu0/domain0/flags
# 输出:0x00000fff  (SD_BALANCE_NEWIDLE | SD_BALANCE_EXEC | ...)