简介

在多核乃至众核 Linux 服务器、嵌入式多核工业主控、车载域控制器等硬件架构普及的当下,单 CPU 内核调度逻辑已经无法满足整机算力调度需求。内核为了实现跨核心任务分发、算力负载均衡、异构核调度隔离、NUMA 架构内存亲和性调度,设计出调度域 (Scheduling Domain)调度组 (Scheduling Group) 双层层级管理架构。

其中struct sched_group作为调度域内部最核心的管理单元,专门用于抽象划分调度域下的 CPU 物理核心子集,依托组内 CPU 数量、调度权重、算力容量、负载统计等核心字段,为内核负载均衡算法提供精准的数据依据,彻底解决多核场景下任务扎堆拥堵、空闲核心算力闲置、跨核调度开销过大、实时任务调度紊乱等一系列线上生产环境常见问题。

从实际工程落地层面来讲,服务器集群算力调度、嵌入式多核实时系统任务分片、云计算虚拟机 CPU 亲和性绑定、工控系统软硬实时核隔离部署,全都依赖调度组完成底层 CPU 资源划分与负载决策。对于底层内核开发工程师、嵌入式 Linux 研发、服务器性能调优工程师、实时操作系统架构设计者而言,吃透sched_group结构体成员含义、层级组织方式、负载统计逻辑、均衡触发判定规则,是掌握 Linux 多核负载均衡体系、优化整机调度吞吐量、降低跨核调度延迟、定制 CPU 资源隔离策略的核心前提。同时该部分源码也是撰写内核调度相关论文、技术调研报告、内核裁剪定制方案不可或缺的核心知识点,本文将以一线底层开发工程师实战视角,从理论概念、环境搭建、源码拆解、实操验证、问题排查全维度完成讲解。

一、核心概念与专业术语解析

1.1 调度域 Scheduling Domain

调度域是 Linux 内核划分 CPU 拓扑结构的最高层级调度单元,内核依据 CPU 物理拓扑、NUMA 节点、CPU 物理封装、CPU 核心层级,自上而下划分多层调度域。 常见层级划分:NUMA 节点域 > 物理 CPU 封装域 > CPU 核心簇域 > 单核调度域。 同一调度域内的 CPU 核心具备调度互通、负载互相迁移的权限,不同层级调度域定义不同的负载均衡粒度与均衡触发阈值。

1.2 调度组 struct sched_group

调度组是隶属于调度域内部的 CPU 资源子集,一个调度域可以切分为一个或者多个调度组,每一个sched_group结构体代表一组逻辑绑定的 CPU 集合。 内核将物理 CPU 核心按照业务需求、硬件拓扑划分为不同调度组,后续所有负载计算、空闲算力统计、任务迁移决策,均以调度组为最小统计单位,而非直接遍历所有 CPU,极大缩减负载均衡运算量。

1.3 核心关键字段释义

1.3.1 基础结构体源码定义(内核 6.1/5.15 通用)
// 路径:kernel/sched/sched.h
struct sched_group {
    /* 指向下一个同层级调度组,实现调度组链表串联 */
    struct sched_group *next;
    /* 该调度组包含的CPU掩码,精准标记组内所有CPU编号 */
    struct cpumask cpumask;

    /* 调度组权重:代表本组整体调度优先级与算力占比 */
    unsigned int group_weight;
    /* 调度组算力容量:表征本组CPU最大可承载任务负载上限 */
    unsigned int group_capacity;
    /* 调度组当前实时负载值,内核周期统计更新 */
    unsigned long group_load;

    /* 调度组内空闲CPU数量,快速判定本组是否存在空闲算力 */
    int nr_idle_cpus;
    /* 调度组内总CPU数量 */
    int group_size;

    /* 调度组均衡偏移阈值,用于判定是否触发组间任务迁移 */
    int imbalance_offset;

    /* 预留扩展字段,用于实时调度、能耗调度扩展 */
    struct sched_group_ext *ext;
};

逐字段实战释义

  1. next:同调度域下所有调度组依靠该指针形成单向循环链表,内核遍历所有分组仅需遍历链表即可,无需遍历全局 CPU;
  2. cpumask:CPU 位图,是调度组绑定物理核心的核心标识,例如0-3号CPU划为一组,位图内对应位置置 1;
  3. group_weight:组调度权重,权重越高,在负载均衡过程中越容易被分配新任务,常用于大小核架构调度优先级区分;
  4. group_capacity:组最大承载容量,结合 CPU 算力主频、核数计算得出,限制本组任务最大负载上限,避免算力溢出;
  5. group_load:实时累计负载,统计组内所有就绪任务总权重负载,是均衡决策最核心数据源;
  6. nr_idle_cpus:组内空闲核心统计,快速筛选空闲分组,跳过高负载分组,提升均衡效率;
  7. group_size:组内 CPU 总个数,用于负载均值计算。

1.4 调度组与调度域层级关系

  1. 一个调度域 = 多个调度组组成;
  2. 调度域负责定义均衡策略、均衡周期、迁移延迟等全局规则;
  3. 调度组负责统计本组负载、空闲状态、算力资源,向上为调度域提供决策数据;
  4. 负载均衡完整流程:调度域触发均衡检查 → 遍历域内所有调度组 → 对比各组负载与空闲状态 → 高负载组向低负载 / 空闲组迁移就绪任务。

1.5 负载均衡核心判定逻辑

内核依靠sched_group三大核心数据完成均衡决策:

  1. 对比group_load:识别高负载调度组与轻负载调度组;
  2. 查看nr_idle_cpus:优先向存在空闲 CPU 的调度组迁移任务;
  3. 参考group_capacity:避免向达到算力上限的调度组派发任务; 最终实现多核 CPU 之间负载均匀分布,杜绝部分核心跑满、部分核心休眠的资源浪费现象。

二、环境准备

2.1 软硬件环境配置标准

环境类别 详细配置要求
操作系统 Ubuntu 20.04 / Ubuntu 22.04 64 位服务器版
内核版本 Linux 5.15 LTS、Linux 6.1 LTS(主流工业与服务器稳定内核,调度组源码无大幅改动)
硬件平台 x86_64 多核 CPU(推荐 4 核 8 核 16 核),支持 CPU 拓扑层级划分;具备 NUMA 架构更佳
编译依赖 gcc 9.0+、make、libncurses-dev、bison、flex、libelf-dev
调试工具 ftrace、perf、trace-cmd、gdb、sysfs 拓扑查看工具、cpuset 工具
辅助工具 htop、mpstat、taskset,用于实时观测 CPU 负载与任务绑定状态

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

2.2.1 一键安装编译依赖
sudo apt update -y
sudo apt install build-essential libncurses-dev bison flex libssl-dev libelf-dev -y

作用:补齐内核编译、配置、模块构建全部依赖包,避免编译报错中断。

2.2.2 下载稳定版内核源码
# 下载Linux6.1长期支持内核
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.2.3 开启调度域与调度组调试配置
# 继承当前系统内核默认配置
cp /boot/config-$(uname -r) .config
# 可视化内核配置界面
make menuconfig

必须开启以下核心配置项,否则无法调试查看调度组运行逻辑:

CONFIG_SCHED_SMT=y          # 开启超线程调度拓扑划分
CONFIG_SCHED_MC=y           # 开启多核CPU核心调度分组
CONFIG_NUMA=y               # 开启NUMA架构调度域分组(多节点服务器必备)
CONFIG_DEBUG_KERNEL=y       # 内核全局调试开关
CONFIG_SCHED_DEBUG=y        # 调度子系统专属调试接口
CONFIG_FTRACE=y             # 函数跟踪,跟踪调度组负载更新函数
CONFIG_CPUMASK_OFFSTACK=y   # 优化CPU掩码存储,适配大批量分组管理

保存配置退出,开始编译内核:

# 多线程编译,利用全部CPU核心加速编译
make -j$(nproc)
# 安装内核模块
sudo make modules_install
# 安装新内核镜像
sudo make install
# 更新系统启动引导项
sudo update-grub

重启服务器,在启动项中选择新编译完成的内核进入实验环境。

2.3 核心源码路径定位

kernel/sched/sched.h        # sched_group、sched_domain结构体定义
kernel/sched/topology.c     # CPU拓扑构建、调度域与调度组初始化创建
kernel/sched/fair.c         # 调度组负载统计、组间任务均衡迁移逻辑
kernel/sched/debug.c        # 调度组调试信息导出接口

三、实际应用场景(300 字精准阐述)

调度组 CPU 集合管理机制广泛落地于各类多核 Linux 工程场景。在云计算服务器虚拟化场景中,运维人员借助调度组将物理机 CPU 划分为多个独立分组,不同虚拟机绑定专属调度组,实现租户 CPU 资源隔离,避免业务之间互相抢占算力。在车载多核域控制器场景下,研发人员划分实时调度组与非实时调度组,自动驾驶感知、决策等高优先级实时任务划入高权重调度组,中控娱乐、多媒体进程划入普通调度组,依靠组权重保障实时任务优先调度。在大型分布式工控系统中,多核心主控依托调度组完成任务分片分发,将采集、运算、控制业务拆分至不同 CPU 分组,提升整机并发处理能力。同时在服务器性能调优场景下,运维通过调整调度组容量与均衡阈值,优化数据库、中间件进程的跨核调度策略,有效降低 CPU 上下文切换开销,大幅提升高并发业务场景下系统运行稳定性与整体吞吐性能。

四、实战案例与完整代码实操

4.1 查看系统原生 CPU 调度分组拓扑信息

4.1.1 查看全局 CPU 拓扑层级
# 查看CPU物理核心拓扑结构
cat /proc/cpuinfo | grep -E "processor|core id|physical id"
# 查看系统NUMA节点与CPU绑定关系
numactl --hardware

使用场景:确认物理硬件原生 CPU 分组结构,对照内核初始化生成的调度组划分规则。

4.1.2 通过 sysfs 查看调度域与调度组信息
# 进入调度拓扑调试目录
cd /sys/devices/system/cpu/sched_domain/
# 查看单个CPU对应的调度域层级与所属调度组
ls -l
cat cpu0/domain*/group/*

作用:直观读取内核自动划分的调度组 CPU 掩码、组内核心数量、默认负载阈值等原生参数。

4.2 内核源码:调度组初始化创建流程代码

内核在系统启动阶段,依据 CPU 物理拓扑自动创建sched_group分组,核心实现代码如下:

// kernel/sched/topology.c
/* 依据CPU掩码创建调度组并初始化核心参数 */
static struct sched_group *
build_sched_group(struct cpumask *cpu_mask, int group_weight)
{
    struct sched_group *sg;
    int cpu_num;

    // 分配调度组内存空间
    sg = kzalloc(sizeof(struct sched_group), GFP_KERNEL);
    if (!sg)
        return NULL;

    // 绑定当前分组对应的CPU位图
    cpumask_copy(&sg->cpumask, cpu_mask);
    // 统计组内CPU总数量
    sg->group_size = cpumask_weight(cpu_mask);
    // 设置调度组调度权重
    sg->group_weight = group_weight;
    // 初始化组算力容量,默认与CPU数量成正比
    sg->group_capacity = sg->group_size * SCHED_CPU_CAP_BASE;
    // 初始化负载与空闲核心计数
    sg->group_load = 0;
    sg->nr_idle_cpus = 0;
    sg->imbalance_offset = 0;
    // 链表指针初始化为空
    sg->next = NULL;

    return sg;
}

代码详细注释

  1. kzalloc:内核内存分配函数,申请调度组结构体内存并清零初始化;
  2. cpumask_copy:将传入的 CPU 核心掩码绑定至当前调度组,确定分组管辖范围;
  3. cpumask_weight:自动统计掩码内有效 CPU 个数,赋值给group_size
  4. 批量初始化权重、容量、负载、空闲核心等业务字段,完成调度组基础配置;
  5. 该函数为内核底层通用创建接口,自动分组、手动自定义分组均会调用此接口。

4.3 调度组负载实时统计核心源码

内核定时扫描组内任务,更新sched_group负载数值,为均衡决策提供数据:

// kernel/sched/fair.c
/* 周期更新指定调度组整体负载信息 */
void update_sg_group_load(struct sched_group *sg)
{
    int cpu;
    unsigned long total_load = 0;
    int idle_cnt = 0;

    // 遍历当前调度组内所有CPU核心
    for_each_cpu(cpu, &sg->cpumask)
    {
        struct rq *rq = cpu_rq(cpu);
        // 累加单个CPU运行队列负载至组总负载
        total_load += rq->load.weight;
        // 统计空闲CPU核心数量
        if (rq->nr_running == 0)
            idle_cnt++;
    }

    // 刷新调度组全局负载
    sg->group_load = total_load;
    // 刷新组内空闲核心计数
    sg->nr_idle_cpus = idle_cnt;
}

代码作用

  1. 遍历调度组绑定的所有 CPU 运行队列;
  2. 汇总所有 CPU 就绪任务总权重负载,更新group_load
  3. 统计无运行任务的空闲核心,赋值nr_idle_cpus
  4. 内核负载均衡定时器周期调用该函数,保证负载数据实时有效。

4.4 组间负载均衡判定逻辑代码

// kernel/sched/fair.c
/* 判定两个调度组之间是否需要执行任务迁移 */
static int sg_need_balance(struct sched_group *src_sg, struct sched_group *dst_sg)
{
    unsigned long src_avg_load, dst_avg_load;

    // 计算源分组单CPU平均负载
    src_avg_load = src_sg->group_load / src_sg->group_size;
    // 计算目标分组单CPU平均负载
    dst_avg_load = dst_sg->group_load / dst_sg->group_size;

    // 负载差值超过阈值,且目标组存在空闲CPU,触发均衡迁移
    if ((src_avg_load > dst_avg_load + src_sg->imbalance_offset) 
        && dst_sg->nr_idle_cpus > 0)
    {
        return 1; // 需要迁移任务
    }
    return 0; // 无需均衡
}

实战使用场景 内核遍历调度域内所有调度组,两两调用该判定函数,筛选出高负载源分组与空闲低负载目标分组,执行就绪任务跨组迁移,实现整机 CPU 负载拉平。

4.5 用户态实操:手动绑定进程至指定 CPU 调度组

编写测试压力程序,绑定至固定 CPU 分组,观测调度组负载变化

4.5.1 多核压力测试 C 语言代码
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <stdlib.h>

// 开启死循环占用CPU资源
void *cpu_stress(void *arg)
{
    while(1)
    {
        // 空循环消耗CPU算力
        ;
    }
    return NULL;
}

int main()
{
    pthread_t tid;
    int i;
    // 创建8条压力线程占用CPU
    for(i = 0; i < 8; i++)
    {
        pthread_create(&tid, NULL, cpu_stress, NULL);
    }
    while(1)
    {
        sleep(1);
    }
    return 0;
}

编译运行命令:

gcc stress_test.c -o cpu_stress -lpthread
# 后台运行压力程序
nohup ./cpu_stress &
4.5.2 使用 taskset 绑定进程至指定 CPU 分组
# 查找压力进程PID
pidof cpu_stress
# 将进程绑定至0-1号CPU调度组
taskset -c 0-1 进程PID
# 查看CPU实时负载占用
mpstat -P ALL 1

实操现象:仅 0、1 号 CPU 负载拉满,其余 CPU 保持空闲,对应所属调度组group_load数值大幅升高,其余分组负载不变,完美印证调度组资源隔离特性。

4.6 Ftrace 跟踪调度组核心函数调用

通过内核跟踪工具,实时观测调度组负载更新、均衡判定执行流程

# 挂载调试文件系统
sudo mount -t debugfs none /sys/kernel/debug
# 清空历史跟踪日志
echo > /sys/kernel/debug/tracing/trace
# 指定需要跟踪的调度组内核函数
echo update_sg_group_load >> /sys/kernel/debug/tracing/set_ftrace_filter
echo sg_need_balance >> /sys/kernel/debug/tracing/set_ftrace_filter
# 开启函数跟踪模式
echo function > /sys/kernel/debug/tracing/current_tracer
# 启动跟踪
echo 1 > /sys/kernel/debug/tracing/tracing_on

新开终端运行 CPU 压力程序后,执行以下命令查看调用日志:

cat /sys/kernel/debug/tracing/trace
# 关闭跟踪
echo 0 > /sys/kernel/debug/tracing/tracing_on

实战价值:直观查看调度组负载更新频率、均衡判定触发时机,验证源码逻辑与系统实际运行流程完全一致。

五、常见问题与实战答疑

Q1:系统开机后调度组划分混乱,和物理 CPU 拓扑不匹配如何解决?

解答:首先检查是否关闭CONFIG_SCHED_MC多核调度拓扑配置,未开启则内核无法按照物理核心划分分组;其次关闭 BIOS 内异常超线程、CPU 节能聚合功能,重启系统重新初始化调度域与调度组;最后通过topology.c源码调试打印 CPU 层级信息,手动修正拓扑识别异常问题。

Q2:调度组 group_load 负载数值更新延迟,负载均衡调度不及时

解答:该问题大多为内核负载均衡扫描周期设置过大导致,可修改内核调度均衡滴答时钟周期,缩短调度组负载统计间隔;同时检查是否开启 CPU 节能降频策略,节能模式会抑制跨组任务迁移,关闭节能策略即可恢复正常均衡效率。

Q3:手动划分自定义调度组之后,进程无法跨组迁移

解答:自定义调度组必须隶属于同一个调度域,不同调度域下的调度组默认关闭任务迁移权限;其次检查调度组imbalance_offset均衡偏移阈值设置过大,导致负载差值无法达到触发条件,适当调低阈值即可恢复跨组迁移能力。

Q4:NUMA 多节点服务器中,跨节点调度组负载均衡性能极差

解答:NUMA 架构下跨节点内存访问延迟极高,内核默认限制跨节点调度组任务迁移频率;工程最佳方案为同 NUMA 节点内划分调度组,尽量避免任务跨节点调度组迁移,减少远程内存访问带来的性能损耗。

Q5:修改 sched_group 结构体字段后内核编译报错

解答:修改调度组结构体后,必须完整重新编译内核而非仅编译模块;同时同步修改topology.cfair.c中所有引用该结构体的业务逻辑代码,保证字段调用统一,避免结构体成员不匹配引发内核 Oops 崩溃。

六、实践建议与工程最佳实践

  1. 嵌入式多核系统分组最佳实践 嵌入式工控、车载系统建议采用功能隔离式调度组划分,将实时控制业务划入高权重调度组,后台日志、升级、运维进程划入低权重分组,依靠group_weight权重字段保障实时业务调度优先级,杜绝非核心进程抢占算力。

  2. 服务器性能调优调度组优化技巧 高并发数据库、缓存服务器场景下,合理拆分调度组规模,单个调度组内 CPU 数量控制在 4~8 核为宜,分组过小会增加均衡调度开销,分组过大容易出现内部负载不均;同时调高业务核心调度组group_capacity算力上限,保障核心业务算力充足。

  3. 内核调试排错规范 排查多核调度卡顿、负载失衡问题时,排查顺序固定为:查看调度组 CPU 掩码绑定状态 → 观测group_load负载统计是否正常 → 验证空闲核心nr_idle_cpus统计准确性 → 最后核查均衡判定阈值与迁移策略,快速定位问题根因。

  4. 内核二次开发定制建议 进行调度策略二次开发时,尽量不要删减sched_group基础核心字段,可通过ext扩展指针新增自定义调度属性,基于原有负载统计、均衡决策逻辑做功能扩展,兼容内核原生调度框架,大幅降低内核版本迭代适配成本。

  5. 容器虚拟化场景资源隔离方案 K8s、Docker 容器环境中,直接依托内核调度组实现 CPU 资源硬隔离,将容器进程限定在专属调度组范围内运行,相比传统 CPU 使用率限制更加稳定,彻底解决容器之间算力互相抢占的线上故障。

七、全文总结与工程延伸应用

本文从行业实际需求出发,完整梳理了 Linux 内核调度组 sched_group整体技术体系,从基础理论概念、实验环境搭建、CPU 拓扑结构解析、结构体字段深度拆解,再到内核初始化源码、负载统计源码、均衡判定源码逐层剖析,搭配用户态压力测试、CPU 绑定实操、ftrace 内核函数跟踪等实战案例,全方位讲清调度组作为调度域 CPU 集合管理单元的核心工作原理。

调度组本质是 Linux 内核为适配多核硬件架构设计的分层算力资源管理容器,依靠 CPU 掩码完成物理核心绑定,依托权重、容量、实时负载、空闲核心等统计字段,简化多核负载均衡运算复杂度,让内核无需遍历全局所有 CPU,仅通过分组聚合数据即可高效完成任务调度与算力分发,是 Linux 从单核调度走向多核众核调度架构升级的核心基石。

在当下工业实时控制系统、自动驾驶车载系统、云计算虚拟化集群、大型互联网服务器集群等主流技术场景中,调度组 CPU 集合管理机制都承担着底层资源调度的核心作用。建议各位底层研发与运维工程师,结合本文提供的内核源码与实操命令,自行修改调度组权重、均衡阈值等参数进行对比实验,观测不同参数下整机调度延迟、CPU 负载均衡度、进程响应速度的变化规律,真正做到吃透底层调度原理,将调度组相关技术落地到实际项目性能优化、内核定制开发、实时系统架构搭建等真实业务场景中。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐