news 2026/6/1 19:57:27

Linux 组调度的层次化负载传播:子组负载的向上聚合

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux 组调度的层次化负载传播:子组负载的向上聚合

简介

在现代 Linux 系统中,cgroup 配合内核组调度(Group Scheduling)已经成为容器虚拟化、云服务器、嵌入式系统、服务器算力隔离的底层基石。传统单任务调度仅能对进程做独立调度与负载统计,当大量进程归属于同一业务、同一租户、同一容器时,会出现任务无限创建抢占 CPU、资源隔离失效、负载统计失真等问题。为此 Linux 内核引入task_group任务组模型,将一组关联进程统一管理,并基于层次化树状结构组织父子任务组。

组调度的核心难点在于负载统计与传播:每个子任务组、组内单个进程都会产生运行负载,内核需要把最底层子组的负载逐层向上聚合至父组、根组,最终汇总到 CPU 运行队列与调度域,为 CFS 负载均衡、CPU 带宽限流、调度权重分配提供精准数据支撑。如果负载聚合逻辑出错,会直接导致多核负载均衡偏移、容器资源限制不生效、业务之间互相抢占、整机负载监控数据异常。

本文聚焦子组负载向上聚合这一核心机制,从数据结构、内核源码、执行流程、实操验证、问题排查全维度展开。内容基于 Linux 5.15/6.1 长期稳定内核编写,适配生产环境内核研读、内核模块开发、性能调优、毕业论文与技术报告撰写。对于 Linux 运维工程师、云原生研发、内核开发、嵌入式 Linux 工程师而言,吃透负载层次化传播逻辑,是理解 cgroup 资源隔离、CFS 组调度、多核负载均衡的必经之路,也是线上负载不均、容器卡顿、限流失效等疑难问题的核心排查切入点。

一、核心概念与术语解析

1.1 组调度与 task_group

Linux 组调度依托cgroup v1/v2实现,内核使用struct task_group描述一个任务组,所有任务组以树形层次结构组织:根任务组为系统全局组,根组下可创建多级父 / 子任务组,对应业务分层、容器层级、用户分组。

内核编译选项控制组调度能力:

  • CONFIG_CGROUP_SCHED:总开关,启用 cgroup 调度支持;
  • CONFIG_FAIR_GROUP_SCHED:CFS 普通进程组调度(本文重点);
  • CONFIG_RT_GROUP_SCHED:实时进程组调度。

1.2 调度实体与组调度实体

CFS 调度中,调度实体(sched_entity)是调度的基本单元:

  1. 普通进程 / 线程:对应底层struct sched_entity
  2. 任务组:同样被抽象为组调度实体,挂载在各级运行队列中,实现 “组与组之间公平调度”。

层次关系:进程实体 → 子组实体 → 父组实体 → 根组实体 → CPU 运行队列,负载沿着这条链路自底向上聚合。

1.3 负载(load)与负载权重

内核 CFS 不再单纯统计进程数量,而是使用调度负载衡量 CPU 占用压力:

  • load_avg:滑动平均负载,动态反映近期 CPU 占用强度,是负载均衡的核心依据;
  • load.weight:调度权重,决定组 / 进程能分到的 CPU 时间片比例;
  • 负载聚合:子组的load_avgload.weight累加、更新后,同步更新父组对应字段,形成链式传播。

1.4 层次化负载传播

这是本文核心定义: 任务组为树形结构,当底层子组内进程状态变化(唤醒、睡眠、退出)、CPU 占用波动时,内核会逐级向上刷新负载数据,将子组负载合并到直接父组,父组再向上合并,直到根任务组与 CPU 运行队列。该机制保证每一级父组的负载,都等价于其所有子组 + 直属进程的负载总和,让调度器在任意层级都能获取真实负载。

1.5 运行队列 rq 与组队列 cfs_rq

每个 CPU 拥有独立struct rq(CPU 运行队列),CFS 部分由struct cfs_rq管理。

  • 普通进程:挂载在对应 CPU 的底层 cfs_rq
  • 每个 task_group 在每个 CPU 上都拥有独立cfs_rq,父子组cfs_rq相互关联,构成层次化队列;
  • 负载聚合、传播、计算全部依托各级cfs_rq完成。

二、环境准备

2.1 软硬件环境

环境分类版本与配置要求
操作系统Ubuntu 20.04 / 22.04 LTS(64 位),适配 cgroup v1/v2
内核版本Linux 5.15 LTS、Linux 6.1 LTS(主流生产内核,源码逻辑一致)
硬件x86_64 多核 CPU(建议 4 核及以上)、内存 4G+,用于多组压测
编译工具gcc 9.4+、make、binutils、libncurses-dev、bison、flex
调试工具gdb、ftrace、trace-cmd、perf、sysfs-utils、cgroup-tools
依赖组件libelf-dev、libssl-dev(内核编译)

2.2 环境部署与内核配置

2.2.1 安装基础依赖与工具

可直接复制执行:

# 更新软件源并安装编译、调试、cgroup工具 sudo apt update && sudo apt install -y \ build-essential libncurses-dev bison flex libssl-dev libelf-dev \ gdb trace-cmd perf cgroup-tools sysfs-utils
2.2.2 下载并配置内核源码

负载聚合核心源码路径固定:

  • 任务组、cfs_rq 定义:kernel/sched/sched.h
  • 负载计算、向上传播函数:kernel/sched/fair.c
  • task_group 管理逻辑:kernel/sched/group.c
# 下载 Linux 6.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 # 继承当前系统内核配置 cp /boot/config-$(uname -r) .config # 打开配置界面 make menuconfig

必须开启的内核选项(保证组调度与负载追踪可用):

General setup ---> [*] Control Group support # 启用cgroup [*] Group CPU scheduler # cgroup调度 Processor type and features ---> [*] Enable kernel debugging (DEBUG_KERNEL) [*] Compile the kernel with debug info # 调试符号 Kernel hacking ---> [*] Tracing support # ftrace跟踪 Scheduler features ---> [*] CFS group scheduling # 核心:CFS组调度 [*] RT group scheduling # 可选:实时组调度
2.2.3 编译安装内核
# 多核编译,加速构建 make -j$(nproc) # 安装内核模块 sudo make modules_install # 安装内核镜像 sudo make install # 更新引导项 sudo update-grub

执行完成后重启系统,在 GRUB 菜单选择新编译内核进入。

2.2.4 验证 cgroup 与组调度状态
# 查看当前cgroup版本 mount | grep cgroup # 检查内核组调度开关 zcat /proc/config.gz | grep FAIR_GROUP_SCHED

输出CONFIG_FAIR_GROUP_SCHED=y代表环境准备完成。

三、应用场景

组调度与层次化负载聚合广泛应用在算力隔离场景。在 K8s、Docker 容器集群中,每个 Pod、容器对应独立 task_group,多个容器归属于同一命名空间父组,子容器的 CPU 负载会逐层向上聚合,集群调度器依靠聚合后的总负载完成节点调度与扩容。在云主机场景下,单台物理机划分多个云主机租户,租户作为一级任务组,租户内部多个业务进程作为子组,负载向上聚合保证租户 CPU 配额精准限流,避免单租户抢占整机资源。在服务器多业务混部场景,核心业务、后台任务、日志采集划分为不同层级任务组,负载聚合数据支撑内核完成组间公平调度,保障核心业务优先级。同时在嵌入式安卓系统中,前台 APP、后台服务、系统进程分组管理,依靠负载传播统计整机负载,实现前台交互低延迟。

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

4.1 核心数据结构源码解析

4.1.1 cfs_rq 组调度运行队列

cfs_rq是负载统计、聚合、传播的载体,每一个 task_group 在每个 CPU 上都存在实例:

// kernel/sched/sched.h struct cfs_rq { /* 该队列总负载,包含自身进程 + 所有子组聚合负载 */ struct load_avg load; /* 指向父组的cfs_rq,用于向上遍历、负载传播 */ struct cfs_rq *parent; /* 组内调度实体链表、红黑树根节点 */ struct rb_root_cached tasks_timeline; unsigned int nr_running; // 就绪任务/子组数量 /* 任务组指针,归属当前cfs_rq */ struct task_group *tg; /* 以下为负载计算、衰减、更新使用的辅助字段 */ u64 load_last_update_time; long nr_hanging; };

代码说明parent指针是层次化结构的关键,负载更新时通过parent逐层找到父队列,完成向上聚合。load字段存储当前队列最终聚合后的负载值。

4.1.2 task_group 任务组结构体

树形结构的核心管理体:

// kernel/sched/sched.h struct task_group { /* 父子组指针,构建树形层级 */ struct task_group *parent; struct list_head siblings; struct list_head children; /* 每个CPU对应的CFS运行队列数组 */ struct cfs_rq **cfs_rq; /* 组调度权重,决定CPU分配比例 */ unsigned long shares; /* CPU带宽限流相关(quota/period) */ struct tg_cfs_sched_bandwidth cfs_bandwidth; };

代码说明parent指向父任务组,children链表管理所有子任务组。当子组负载变化时,内核会遍历parent链路,逐级刷新上层cfs_rq->load

4.2 负载更新与向上聚合核心流程

整体流程:进程状态变更 → 底层 cfs_rq 负载更新 → 调用传播函数 → 逐级遍历 parent 父队列 → 父组负载重计算 → 直至根组

4.2.1 基础负载更新函数 __update_load_avg

该函数负责计算单个调度实体的滑动负载,是聚合的基础:

// kernel/sched/fair.c static void __update_load_avg(struct sched_entity *se, int cpu) { struct cfs_rq *cfs_rq = se->cfs_rq; u64 now = rq_clock_task(cpu_rq(cpu)); u64 delta; /* 计算距离上一次更新的时间差 */ delta = now - cfs_rq->load_last_update_time; if (delta == 0) return; /* 1. 更新当前调度实体自身负载(衰减+新增负载) */ decay_load(&se->load, delta); accumulate_load(&se->load, se->load.weight, delta); /* 2. 更新当前层级cfs_rq总负载 */ decay_load(&cfs_rq->load, delta); accumulate_load(&cfs_rq->load, se->load.weight, delta); cfs_rq->load_last_update_time = now; /* 3. 核心入口:向上传播负载到所有父组 */ propagate_load_up(cfs_rq, delta); }

代码作用:进程 / 组负载发生变化时,先更新当前队列负载,再调用propagate_load_up执行向上聚合

4.2.2 核心函数 propagate_load_up 负载向上聚合

这是实现 “子组负载向上传递” 的核心函数,逐层遍历父队列并刷新负载:

// kernel/sched/fair.c static void propagate_load_up(struct cfs_rq *curr_cfs_rq, u64 delta) { struct cfs_rq *parent_rq; /* 循环终止条件:到达根组 cfs_rq(parent = NULL) */ for (parent_rq = curr_cfs_rq->parent; parent_rq; parent_rq = parent_rq->parent) { /* 父队列负载衰减 */ decay_load(&parent_rq->load, delta); /* 累加当前子队列的负载增量到父队列 */ accumulate_load(&parent_rq->load, curr_cfs_rq->load.load_sum, delta); /* 迭代:父队列变为下一轮的“子队列”,继续向上传播 */ curr_cfs_rq = parent_rq; } }

逐行解析

  1. 以当前发生负载变化的子组cfs_rq为起点;
  2. 通过parent指针拿到直接父组队列;
  3. 对父组负载做时间衰减(模拟负载自然下降);
  4. 将子组最新负载累加到父组,完成一级聚合;
  5. 把父组作为新的子节点,继续循环,直到parent=NULL(根组);
  6. 最终所有上层队列的负载,都包含了下层所有子组的负载总和。
4.2.3 任务入队 / 出队时触发负载聚合

进程唤醒(入队)、睡眠(出队)是负载变化最频繁的场景,会主动触发负载传播:

// kernel/sched/fair.c static void enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags) { /* 将调度实体加入CFS红黑树 */ __enqueue_entity(cfs_rq, se); cfs_rq->nr_running++; /* 更新负载并向上聚合 */ __update_load_avg(se, cpu_of(rq_of(cfs_rq))); } static void dequeue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags) { /* 将调度实体从红黑树移除 */ __dequeue_entity(cfs_rq, se); cfs_rq->nr_running--; /* 负载下降,同样触发向上重新聚合 */ __update_load_avg(se, cpu_of(rq_of(cfs_rq))); }

代码说明:进程进出就绪队列时,负载发生增减,必然执行__update_load_avg,进而调用propagate_load_up完成全层级负载刷新。这保证任意子组状态变化,上层所有父组负载都会实时同步。

4.3 用户态实操:创建层次化任务组并观测负载聚合

下面通过cgroup v1创建多级父子任务组,运行压测进程,结合工具验证负载向上聚合效果。

4.3.1 挂载 cgroup CPU 子系统
# 创建挂载目录 sudo mkdir -p /sys/fs/cgroup/cpu # 挂载cpu子系统 sudo mount -t cgroup -o cpu none /sys/fs/cgroup/cpu
4.3.2 创建多级父子任务组

构建层级:根组 → parent_group(父组) → child_group(子组)

# 1. 创建一级父组 sudo mkdir /sys/fs/cgroup/cpu/parent_group # 2. 在父组内创建二级子组 sudo mkdir /sys/fs/cgroup/cpu/parent_group/child_group
4.3.3 编写 CPU 压测程序(子组内进程)

编写死循环消耗 CPU 的测试程序,模拟业务负载:

// load_test.c #include <stdio.h> #include <unistd.h> int main(void) { printf("Child group load task running...\n"); /* 空循环持续占用CPU,产生稳定负载 */ while(1) { ; } return 0; }

编译命令:

gcc load_test.c -o load_test
4.3.4 将进程加入底层子组,观测负载传播
# 后台运行压测程序 ./load_test & # 获取进程PID PID=$! echo "Test PID: $PID" # 将进程加入最底层子组(child_group) sudo echo $PID > /sys/fs/cgroup/cpu/parent_group/child_group/cgroup.procs
4.3.5 查看各级组 CPU 负载统计

cgroup 文件cpuacct.usage统计累计 CPU 时间,验证聚合效果:

# 每隔1秒查看 子组、父组、根组 的CPU占用 while true do echo "===== 底层子组 child_group =====" cat /sys/fs/cgroup/cpu/parent_group/child_group/cpuacct.usage echo "===== 父组 parent_group =====" cat /sys/fs/cgroup/cpu/parent_group/cpuacct.usage echo "===== 系统根组 =====" cat /sys/fs/cgroup/cpu/cpuacct.usage sleep 1 done

现象说明: 子组、父组、根组的 CPU 累计时间同步上涨,证明子组负载已经向上聚合到父组与根组。杀掉子组进程后,各级组负载增长停止,聚合链路同步失效。

4.3.6 使用 ftrace 跟踪内核负载聚合函数

跟踪propagate_load_up__update_load_avg调用,直观看到执行链路:

# 挂载debugfs sudo mount -t debugfs none /sys/kernel/debug # 清空跟踪缓冲区 sudo echo > /sys/kernel/debug/tracing/trace # 设置需要跟踪的负载聚合函数 sudo echo propagate_load_up >> /sys/kernel/debug/tracing/set_ftrace_filter sudo echo __update_load_avg >> /sys/kernel/debug/tracing/set_ftrace_filter # 开启函数跟踪 sudo echo function > /sys/kernel/debug/tracing/current_tracer sudo echo 1 > /sys/kernel/debug/tracing/tracing_on

重新启停压测进程,然后停止跟踪并查看日志:

sudo echo 0 > /sys/kernel/debug/tracing/tracing_on sudo cat /sys/kernel/debug/tracing/trace

日志中可以清晰看到:进程入队 / 出队时,__update_load_avg被调用,随后触发propagate_load_up循环向上遍历父队列,完整复现源码逻辑。

4.4 多级组负载叠加测试

在同一个父组下创建多个子组,分别运行压测进程:

# 创建第二个子组 sudo mkdir /sys/fs/cgroup/cpu/parent_group/child_group2 # 启动第二个压测进程并加入新子组 ./load_test & PID2=$! sudo echo $PID2 > /sys/fs/cgroup/cpu/parent_group/child_group2/cgroup.procs

再次查看各级cpuacct.usage,可以发现:父组 CPU 负载 = 子组 1 + 子组 2 负载之和,完全验证层次化聚合的数学逻辑。

五、常见问题与解答

Q1:为什么负载不能只在底层统计,必须向上逐层聚合?

解答:Linux 负载均衡、CPU 限流、调度权重分配是跨层级执行的。调度器需要计算父组整体负载来判断是否需要迁移任务、是否触发带宽限流。如果仅统计子组负载,父组无法感知下属所有子组的总压力,会造成负载均衡判断错误、容器配额限制失效。

Q2:负载向上聚合会不会产生性能损耗?层级越多性能影响越大吗?

解答:会有少量损耗。每一次负载更新都要遍历整条父子链路,任务组层级越深,循环次数越多。内核做了优化:负载更新基于时间窗口做衰减合并,不会无限频繁触发;生产环境建议控制任务组层级在 3 层以内,避免深度嵌套。

Q3:子组进程全部退出后,父组负载为什么没有立刻归零?

解答:内核使用滑动平均负载,内置时间衰减算法。进程退出后,底层负载会逐步衰减,再逐层向上传递,并非瞬时清零。等待数秒后各级组负载会逐步回落至 0,属于正常机制。

Q4:修改 task_group 权重(shares)后,负载聚合逻辑会变化吗?

解答:聚合链路不变。权重仅影响组内 CPU 时间片分配比例,propagate_load_up只负责负载数值的传递与累加,和调度权重相互独立。权重修改不会破坏层次化传播流程。

Q5:ftrace 抓不到 propagate_load_up 函数调用,是什么原因?

解答:1. 内核未开启CONFIG_FTRACE与调试选项;2. 未正确挂载 debugfs;3. 测试进程长时间休眠,无负载变化,函数不会被触发;4. 使用了 cgroup v2,部分函数符号略有差异,需要对应调整跟踪函数名。

Q6:跨 CPU 任务迁移时,负载聚合如何处理?

解答:任务从 CPU0 迁移到 CPU1 时,会在原 CPU 执行dequeue_entity(负载下降、向上聚合),在新 CPU 执行enqueue_entity(负载上升、向上聚合)。两个 CPU 的各级任务组队列都会独立完成负载更新,保证多核场景下统计准确。

六、实践建议与最佳实践

6.1 任务组层级设计规范

生产环境中控制 task_group 层级深度,建议最大层级不超过 3 级。过深的树形结构会加长负载聚合循环链路,增大调度延迟。容器、云主机场景尽量采用 “父组 - 子组” 两级结构。

6.2 性能调优技巧

  1. 高并发业务不要频繁创建、销毁任务组与进程,频繁的入队 / 出队会高频触发负载聚合函数,拉高 CPU 软中断与调度开销;
  2. 对 CPU 密集型容器单独划分任务组,并绑定固定 CPU 核心,减少跨核任务迁移带来的多次负载刷新;
  3. 若业务对调度延迟极度敏感,可适当调大负载更新时间窗口,减少__update_load_avg调用频次。

6.3 问题排查流程(负载统计异常)

  1. 先检查 cgroup 挂载状态、内核组调度开关是否正常;
  2. 查看各级cpuacct.usage,判断是单组异常还是全层级异常;
  3. 使用 ftrace 跟踪propagate_load_up,确认负载传播链路是否正常执行;
  4. 检查是否存在僵尸进程、未正确退出的线程,导致底层负载无法衰减;
  5. 核对内核版本,部分老旧内核存在负载聚合计算 BUG,建议使用 5.15 及以上 LTS 版本。

6.4 内核二次开发建议

若需要基于组调度做定制开发,不要修改propagate_load_up的循环遍历逻辑。如需新增负载指标,在cfs_rq中扩展字段,并沿用现有parent链路完成向上聚合,保证内核原有负载均衡、限流逻辑兼容。

6.5 线上监控建议

监控体系需要分层采集 cgroup 负载,不仅监控根组整机负载,也要监控二级、三级子组负载。当出现整机负载高但业务无压力时,大概率是底层子组负载聚合异常,分层监控可以快速定位问题组。

七、总结与应用延伸

本文完整拆解了 Linux 组调度层次化负载向上聚合的全套机制,从数据结构、核心源码、执行流程、用户态实操、工具跟踪、问题排查到工程最佳实践,覆盖理论与落地全环节。

核心要点回顾:

  1. 任务组以树形结构组织,依靠task_group->parentcfs_rq->parent构建层级链路;
  2. 负载聚合的核心逻辑在propagate_load_up函数中,通过循环遍历父队列,将子组负载逐层向上累加;
  3. 进程入队、出队、状态切换是负载更新与传播的主要触发点;
  4. 该机制是 cgroup 资源隔离、容器调度、多核负载均衡、CPU 带宽限流的数据基础。

从工程落地角度,这套负载传播机制支撑着云计算、容器集群、服务器混部、嵌入式系统等主流场景。掌握该原理,不仅可以解决线上负载不均、容器资源限制失效、负载监控失真等问题,也能为内核裁剪、调度策略定制、性能优化、技术论文撰写提供底层理论支撑。

建议读者基于本文提供的源码、C 语言测试程序、ftrace 命令,在不同内核版本、不同任务组层级下反复实验,观察负载变化与内核函数调用差异。结合线上业务场景设计合理的任务组架构,将层次化负载聚合的理论知识真正落地到生产环境中。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/1 19:57:22

2026房地产三维动画制作公司推荐榜:五家实力派服务商深度评测

在数字化展示浪潮的推动下&#xff0c;房地产三维动画和建筑可视化技术已成为楼盘营销、招商展示和项目汇报的核心工具。通过UE5引擎、CIM系统和VR交互等技术的深度融合&#xff0c;三维动画能够将项目规划、建筑细节、园林景观和室内空间等信息整合为沉浸式的视觉体验&#xf…

作者头像 李华
网站建设 2026/6/1 19:57:20

便宜token

反正大家都要用ai 各种编程模型 几个便宜 官网1/5价格

作者头像 李华
网站建设 2026/6/1 19:56:40

猫抓浏览器插件:视频资源嗅探下载的终极解决方案

猫抓浏览器插件&#xff1a;视频资源嗅探下载的终极解决方案 【免费下载链接】cat-catch 猫抓 浏览器资源嗅探扩展 / cat-catch Browser Resource Sniffing Extension 项目地址: https://gitcode.com/GitHub_Trending/ca/cat-catch 还在为无法下载网页视频而烦恼吗&…

作者头像 李华
网站建设 2026/6/1 19:55:05

硬件霍夫曼解码器:为边缘AI模型实现70%无损压缩与单周期解压

1. 项目概述&#xff1a;为边缘AI“瘦身”的实时无损压缩解码器在边缘设备上跑深度神经网络&#xff08;DNN&#xff09;&#xff0c;比如让摄像头实时识别人脸、让智能音箱听懂指令&#xff0c;最头疼的问题是什么&#xff1f;不是算力&#xff0c;而是内存。一个经典的VGG19模…

作者头像 李华