简介

在 Linux SMP(对称多处理器)与 NUMA(非统一内存访问)系统中,调度域(sched_domain) 是内核实现跨 CPU 负载均衡的核心基础设施。系统启动阶段或 CPU 热插拔时,内核必须依据 CPU 物理拓扑(SMT 超线程、物理核心、NUMA 节点)构建分层调度域与调度组(sched_group),为后续负载均衡提供拓扑依据与约束规则。

build_sched_domains作为调度域生成的核心函数,承担着解析 CPU 拓扑、分配调度域 / 调度组内存、构建层级关系、初始化负载均衡参数的关键职责。若调度域拓扑构建异常,会直接导致负载均衡失效、CPU 利用率失衡、跨 NUMA 节点调度时延激增、实时任务抢占紊乱等严重问题。

对于内核开发者、嵌入式 Linux 工程师、服务器性能调优人员而言,吃透build_sched_domains的实现逻辑、拓扑层级生成规则、调度组划分策略,是理解 Linux 多核心调度架构、排查负载均衡异常、优化 NUMA 系统性能、定制化调度拓扑的必备能力。本文从核心概念、环境搭建、源码逐行解析、实操验证、问题排查到最佳实践,全链路拆解调度域生成流程,内容可直接用于内核源码研读、学术论文撰写、服务器性能优化方案落地。

一、核心概念与术语解析

1.1 调度域 struct sched_domain

调度域是内核为 CPU 划分的调度管理单元,每个 CPU 对应一个分层的调度域链表(通过parent指针串联),核心作用是定义负载均衡的范围与规则

// kernel/sched/sched.h 核心结构体
struct sched_domain {
    struct sched_domain *parent;    /* 父调度域,指向更高层级 */
    struct sched_domain *child;     /* 子调度域,指向更低层级 */
    struct sched_group *groups;     /* 调度组链表,循环链表 */
    struct cpumask span;            /* 该调度域覆盖的CPU掩码 */
    unsigned int level;             /* 拓扑层级(SMT/MC/SMP/NUMA) */
    unsigned long flags;            /* 负载均衡控制标志(SD_*) */
    int imbalance_pct;              /* 负载不均衡阈值 */
    /* 负载统计、均衡周期等成员省略 */
};
  • 层级关系:从底层 SMT(超线程)→ MC(多核心)→ SMP(物理 CPU)→ NUMA(节点)逐层向上,span范围逐层扩大。
  • 核心规则:负载均衡仅在同一调度域内的调度组之间进行,跨域不触发均衡。

1.2 调度组 struct sched_group

调度组是调度域内的 CPU 分组单元,将调度域的 CPU 划分为若干组,作为负载均衡的基本操作对象

// kernel/sched/sched.h 核心结构体
struct sched_group {
    struct sched_group *next;       /* 同域调度组循环链表 */
    atomic_t ref;                    /* 引用计数(多CPU共享) */
    unsigned int group_weight;      /* 组内CPU数量 */
    struct sched_group_capacity *sgc; /* 组容量信息 */
    unsigned long cpumask[0];       /* 组内CPU掩码(柔性数组) */
};
  • 分组规则:调度域内所有调度组的cpumask并集等于调度域span,交集为空。
  • 共享特性:调度组为只读数据,可被同层级多个 CPU 的调度域共享,减少内存占用。

1.3 CPU 物理拓扑层级

Linux 内核默认按 4 层拓扑构建调度域,层级从低到高:

  1. SMT 层(超线程):同一物理核心下的虚拟 CPU(如 CPU0/1 为同一核心超线程)。
  2. MC 层(多核心):同一物理 CPU 下的多个核心(不含超线程)。
  3. SMP 层(物理 CPU):同一 NUMA 节点下的所有物理 CPU。
  4. NUMA 层(节点):整个系统的所有 NUMA 节点(最高层级)。

1.4 关键函数与调用链路

  • build_sched_domains:调度域生成入口,解析拓扑、构建层级、初始化参数。
  • build_sched_domain:构建单个层级的调度域,分配内存、设置spanflags
  • build_sched_groups:为调度域划分调度组,构建循环链表。
  • 调用链路start_kernelsched_init_smpinit_sched_domainsbuild_sched_domains

二、环境准备

2.1 软硬件环境要求

环境类型 版本 / 配置要求
操作系统 Ubuntu 20.04 / 22.04 64 位(支持 SMP/NUMA)
内核版本 Linux 5.15、6.1、6.6(LTS 版,拓扑逻辑稳定)
硬件配置 4 核 8G 以上(支持 SMT/NUMA,推荐 Intel i7 或 AMD 锐龙)
编译工具 gcc 9.4+、make、libncurses-dev、bison、flex
调试工具 gdb、kgdb、perf、trace-cmd、ftrace、numactl

2.2 内核源码获取与编译配置

1. 下载内核源码
# 安装依赖
sudo apt update && sudo apt install build-essential libncurses-dev bison flex libssl-dev libelf-dev

# 下载Linux 6.1 LTS源码
wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.1.tar.xz
tar -xf linux-6.1.tar.xz
cd linux-6.1
2. 开启调度域调试与拓扑选项
cp -v /boot/config-$(uname -r) .config
make menuconfig

必须开启以下配置:

CONFIG_SCHED_DOMAIN=y          # 启用调度域
CONFIG_SCHED_DEBUG=y           # 调度调试
CONFIG_FTRACE=y                # 函数跟踪
CONFIG_NUMA=y                  # 支持NUMA拓扑
CONFIG_SMP=y                   # 支持SMP
CONFIG_DEBUG_KERNEL=y          # 内核调试
3. 编译安装内核
make -j$(nproc)
sudo make modules_install
sudo make install
sudo update-grub

重启后选择新内核进入。

2.3 源码定位

调度域核心源码路径:

kernel/sched/topology.c   // build_sched_domains主逻辑
kernel/sched/sched.h      // sched_domain/sched_group结构体
kernel/sched/core.c       // 调度域挂载与初始化

三、应用场景

调度域拓扑构建是多核心 / NUMA 系统性能的基石,在服务器、嵌入式、高性能计算场景中至关重要。云服务器 NUMA 架构下,build_sched_domains按节点划分调度域,确保负载优先在本地 NUMA 节点内均衡,避免跨节点内存访问的高时延(跨 NUMA 访问时延是本地的 2-3 倍)。工业嵌入式多核心设备(如 PLC、运动控制器)中,SMT/MC 层级调度域隔离实时与非实时任务,防止非实时任务抢占导致实时任务超时。高性能计算(HPC)集群中,调度域拓扑约束负载均衡范围,减少跨核心 / 节点的上下文切换与缓存失效,提升计算密集型任务吞吐量。此外,CPU 热插拔场景下,build_sched_domains动态重构拓扑,保障系统稳定性与负载均衡连续性。

四、实际案例与源码深度剖析

4.1 调度域生成入口:init_sched_domains

系统启动时初始化调度域,调用build_sched_domains完成核心构建:

// kernel/sched/topology.c
int init_sched_domains(const struct cpumask *cpu_map)
{
    int err;
    // 架构层更新CPU拓扑(如NUMA节点、SMT信息)
    arch_update_cpu_topology();

    // 分配调度域数组(排除隔离CPU)
    ndoms_cur = 1;
    doms_cur = alloc_sched_domains(ndoms_cur);
    if (!doms_cur)
        doms_cur = &fallback_doms;
    cpumask_andnot(doms_cur[0], cpu_map, cpu_isolated_map);

    // 核心:调用build_sched_domains生成调度域拓扑
    err = build_sched_domains(doms_cur[0], NULL);
    if (err)
        return err;

    // 注册调度域sysctl接口(/proc/sys/kernel/domain)
    register_sched_domain_sysctl();
    return 0;
}

代码说明cpu_map为在线 CPU 掩码,函数先过滤隔离 CPU,再调用核心函数生成拓扑。

4.2 核心函数:build_sched_domains 源码解析

该函数完成拓扑解析、调度域分配、层级构建、调度组划分,核心逻辑如下(精简版,带注释):

// kernel/sched/topology.c
static int
build_sched_domains(const struct cpumask *cpu_map, struct sched_domain_attr *attr)
{
    struct sched_domain_topology_level *tl;
    struct sched_domain *sd, *prev_sd;
    struct cpumask *tmp_mask;
    int cpu, i;

    // 1. 初始化拓扑层级(SMT→MC→SMP→NUMA)
    tl = sched_domain_topology;
    for (; tl->init; tl++) {
        // 遍历每个CPU,构建对应层级调度域
        for_each_cpu(cpu, cpu_map) {
            // 分配调度域内存(per-CPU,无锁访问)
            sd = __sdt_alloc(cpu);
            if (!sd)
                return -ENOMEM;

            // 2. 设置调度域span(覆盖CPU范围)
            tl->mask(cpu, &sd->span);
            cpumask_and(&sd->span, &sd->span, cpu_map);

            // 3. 初始化调度域标志(负载均衡规则)
            sd->flags = tl->flags;
            sd->level = tl->level;
            sd->imbalance_pct = tl->imbalance_pct;

            // 4. 构建层级关系(child/parent指针)
            prev_sd = per_cpu(d.sd, cpu);
            sd->child = prev_sd;
            if (prev_sd)
                prev_sd->parent = sd;
            per_cpu(d.sd, cpu) = sd;
        }
    }

    // 5. 为每个调度域构建调度组
    for_each_cpu(cpu, cpu_map) {
        for (sd = per_cpu(d.sd, cpu); sd; sd = sd->child) {
            // 划分调度组,构建循环链表
            build_sched_groups(sd, cpu);
            // 初始化调度组容量(CPU算力、负载能力)
            init_sched_groups_capacity(cpu, sd);
        }
    }

    // 6. 挂载调度域到CPU运行队列
    for_each_cpu(cpu, cpu_map) {
        rq = cpu_rq(cpu);
        rq->sd = per_cpu(d.sd, cpu);
    }

    return 0;
}

核心逻辑拆解

  1. 拓扑遍历:按SMT→MC→SMP→NUMA顺序遍历层级,sched_domain_topology为全局拓扑表。
  2. 调度域分配:每个 CPU per-CPU 分配调度域,无锁访问,避免缓存伪共享。
  3. Span 设置:通过tl->mask获取当前层级 CPU 范围,过滤后作为span
  4. 层级关联:通过child/parent指针串联层级,底层为 child,高层为 parent。
  5. 调度组构建build_sched_groups按物理拓扑分组,如 SMT 层每组 1 个虚拟 CPU,MC 层每组 1 个物理核心。

4.3 调度组构建:build_sched_groups 源码

为调度域划分调度组,构建循环链表,确保负载均衡可正常执行:

// kernel/sched/topology.c
static void build_sched_groups(struct sched_domain *sd, int cpu)
{
    struct sched_group *sg, *first_sg;
    struct cpumask *span = &sd->span;
    int i;

    first_sg = NULL;
    // 遍历span中的CPU,按物理拓扑分组
    for_each_cpu(i, span) {
        // 分配调度组(柔性数组存cpumask)
        sg = alloc_sched_group();
        if (!sg)
            panic("Failed to allocate sched group");

        // 设置组内CPU掩码
        cpumask_set_cpu(i, (struct cpumask *)sg->cpumask);
        sg->group_weight = 1;
        atomic_set(&sg->ref, 1);

        // 构建循环链表
        if (!first_sg) {
            first_sg = sg;
            sg->next = sg;
        } else {
            sg->next = first_sg->next;
            first_sg->next = sg;
        }
    }

    // 调度域关联调度组链表
    sd->groups = first_sg;
}

代码说明:调度组为循环链表,groups指向包含当前 CPU 的组,确保负载均衡从本地组开始。

4.4 实操验证:查看调度域拓扑

1. 查看系统调度域层级
# 挂载debugfs
sudo mount -t debugfs none /sys/kernel/debug

# 查看CPU0的调度域层级(SMT→MC→SMP→NUMA)
cat /sys/kernel/debug/sched/domains/cpu0/domain

输出示例(层级从底层到高层):

domain 0 (SMT): span=0-1, groups=0,1
domain 1 (MC): span=0-3, groups=0-1,2-3
domain 2 (SMP): span=0-7, groups=0-3,4-7
domain 3 (NUMA): span=0-15, groups=0-7,8-15
2. 查看调度组信息
# 查看CPU0的SMT层调度组
cat /sys/kernel/debug/sched/domains/cpu0/domain/groups

4.5 Ftrace 跟踪 build_sched_domains 调用

# 清空跟踪缓存
echo > /sys/kernel/debug/tracing/trace

# 设置跟踪函数
echo build_sched_domains >> /sys/kernel/debug/tracing/set_ftrace_filter
echo build_sched_domain >> /sys/kernel/debug/tracing/set_ftrace_filter
echo build_sched_groups >> /sys/kernel/debug/tracing/set_ftrace_filter

# 开启跟踪
echo function > /sys/kernel/debug/tracing/current_tracer
echo 1 > /sys/kernel/debug/tracing/tracing_on

# 重启系统触发调度域重建(或热插拔CPU)
sudo reboot

# 查看跟踪日志
cat /sys/kernel/debug/tracing/trace

日志可清晰看到拓扑构建的完整调用链路,验证源码逻辑。

五、常见问题与解答

Q1:调度域层级顺序错乱,导致负载均衡异常?

解答:大概率是sched_domain_topology全局拓扑表配置错误,或arch_update_cpu_topology未正确识别 CPU 拓扑(如 SMT/NUMA 未开启)。排查:1. 检查内核CONFIG_SMP/NUMA/SMT是否开启;2. 查看/sys/kernel/debug/sched/domains层级是否为 SMT→MC→SMP→NUMA;3. 重新编译内核,确保拓扑表未被篡改。

Q2:NUMA 系统跨节点负载均衡频繁,时延高?

解答:调度域flags未正确设置SD_NUMA标志,导致高层 NUMA 域强制均衡,忽略本地节点优先规则。排查:1. 查看 NUMA 域flags是否包含SD_NUMA;2. 调整imbalance_pct(默认 25%),提高 NUMA 域不均衡阈值,减少跨节点均衡;3. 绑定任务到本地 NUMA 节点,降低跨节点访问。

Q3:CPU 热插拔后,调度域拓扑未更新?

解答:热插拔未触发build_sched_domains重建,或cpu_isolated_map过滤异常。排查:1. 查看热插拔日志dmesg | grep sched_domain;2. 手动触发重建:echo 1 > /sys/kernel/debug/sched/rebuild_domains;3. 检查内核CONFIG_HOTPLUG_CPU是否开启。

Q4:调度组链表断裂,导致内核崩溃?

解答build_sched_groups分配调度组失败,或循环链表构建错误(next指针异常)。排查:1. 查看内核崩溃日志,确认是否为sched_group空指针;2. 开启CONFIG_SCHED_DEBUG,启用调度域错误检查;3. 重新编译内核,确保内存分配函数正常。

Q5:如何自定义调度域拓扑层级?

解答:内核支持通过set_sched_topology覆盖默认拓扑表,自定义层级与标志。步骤:1. 定义自定义拓扑数组struct sched_domain_topology_level my_topology[];2. 在arch_init_sched_domains中调用set_sched_topology(my_topology);3. 编译内核,验证自定义拓扑是否生效。

六、实践建议与最佳实践

  1. 内核调试建议:研读源码时重点关注build_sched_domains的拓扑遍历、调度组构建逻辑,配合ftracedebugfs动态观测,比静态源码更易理解层级关系;调试负载均衡异常时,优先核查调度域spanflags配置。

  2. NUMA 系统优化:关闭不必要的跨 NUMA 节点负载均衡,提高 NUMA 域imbalance_pct至 30%-40%;将计算密集型任务绑定到本地 NUMA 节点,减少跨节点内存访问时延。

  3. SMT 系统调优:SMT 层调度域设置SD_SMT标志,限制超线程间过度抢占;实时任务绑定到独立物理核心,避免超线程共享核心导致的时延波动。

  4. 内核定制开发:自定义调度策略时,不要修改默认拓扑生成逻辑,可通过新增调度域层级或修改flags扩展功能,保留原生负载均衡能力。

  5. 问题排查规范:负载均衡异常排查顺序:1. 检查调度域层级是否完整;2. 确认span覆盖范围正确;3. 核查调度组链表是否正常;4. 分析flags与均衡阈值配置,快速定位根因。

七、总结与应用延伸

本文从理论概念、环境搭建、结构体定义、核心源码逐行解析、实操验证、问题排查到工程最佳实践,完整拆解了 Linux 调度域生成核心函数build_sched_domains的设计思想、拓扑构建逻辑与层级管理机制。调度域本质是 CPU 物理拓扑的软件抽象,通过分层span与调度组划分,为负载均衡提供精准的范围约束与操作单元,是 Linux 多核心 / NUMA 系统高性能调度的基石。

从工程应用来看,该机制是云服务器、工业嵌入式、高性能计算集群的底层调度支撑;从学术研究与内核开发角度,掌握build_sched_domains逻辑,可深入理解 Linux SMP/NUMA 调度架构、物理拓扑与软件调度的映射关系、负载均衡的约束设计思想,可直接用于内核源码论文撰写、NUMA 系统性能优化、定制化调度拓扑开发。

建议读者基于本文提供的源码、实操命令与调试方法,自行编译内核复现实验,修改build_sched_domains的拓扑层级或flags配置,观测负载均衡行为与系统性能变化,真正做到从理论到实战吃透 Linux 调度域生成核心原理。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐