简介
在服务器虚拟化、容器云、边缘嵌入式集群场景中,多业务进程混部是线上环境常态。传统基于进程 nice 的权重调节仅能管控单进程 CPU 占比,无法实现批量业务整体资源隔离:例如一台物理机同时部署数据库、业务服务、日志采集三类进程,数据库突发满载时极易挤占其他业务 CPU 资源,引发接口超时、业务雪崩。Linux 在 CFS 完全公平调度器基础上依托CONFIG_FAIR_GROUP_SCHED配置开启组调度机制,通过 cgroup CPU 子系统把多个进程归集为 task_group 任务组,依靠shares相对权重完成整组 CPU 时间片分配控制。
用户层修改 cgroup 的cpu.shares(cgroup v1)/cpu.weight(cgroup v2)配置项时,内核底层统一由sched_group_set_shares作为核心入口函数完成权重落地,实时刷新全 CPU 运行队列中组调度实体权重,动态变更各组 CPU 资源分配比例,无需重启进程、无需重载内核即可在线调优资源配比。
从落地价值来看,云原生运维工程师依靠该机制完成容器资源配额动态扩容缩容;内核研发人员依托该函数源码理解 CFS 分层调度架构;嵌入式工程师在工控多任务场景实现核心任务资源优先保障。吃透sched_group_set_shares调用链路、源码实现、负载刷新逻辑,是容器资源管控、CFS 内核二次裁剪、线上 CPU 抢占异常排查的必备基础。本文从原理、环境、内核源码、实操验证、排错、工程优化全链路落地,内容可直接用于学术论文数据佐证、项目技术方案编写。
一、核心概念与术语解析
1.1 CFS 组调度基础架构
CFS 组调度打破单进程调度粒度,采用分层树形调度:根任务组root_task_group为系统顶层节点,所有自建 cgroup 任务组作为子节点挂靠在根节点下,子组还可继续嵌套创建下级分组,形成多级调度树。内核配置CONFIG_FAIR_GROUP_SCHED=y后组调度生效,对应关闭则退化为传统单进程 CFS 调度The Linux Kernel Archives。
1.2 task_group 任务组结构体
每一个 cgroup CPU 分组在内核映射一个struct task_group结构体,是组调度管理载体,关键成员定义:
// kernel/sched/sched.h struct task_group { /* 当前任务组基础shares权重,用户修改cpu.shares最终落地此字段 */ unsigned long shares; /* 每个CPU对应一个组调度实体se,task_group[n_cpu]->se[n_cpu] */ struct sched_entity **se; /* 每个CPU对应组CFS运行队列 */ struct cfs_rq **cfs_rq; /* 组带宽控制,用于cfs_quota硬限制,与shares相对权重无关 */ struct cfs_bandwidth cfs_bandwidth; };shares:组基准权重,默认新建任务组数值 1024,仅代表相对比例,不是固定 CPU 核数;se[]:调度实体,每个 CPU 维护一个组级sched_entity,代表本任务组在对应 CPU 上参与上层调度;cfs_rq[]:组私有 CFS 就绪队列,组内所有普通进程的 se 挂载到此队列,形成 “进程 se→组 cfs_rq→组 se→父组 cfs_rq” 分层调度链路。
1.3 shares 权重分配规则
shares 为相对权重,CPU 空闲时资源不做限制;多组同时满载争抢 CPU 时,各组占用 CPU 时长 = 本组 shares / 所有就绪组 shares 总和。 示例:GroupA=1024、GroupB=2048,两组满载时 A 占 1/3、B 占 2/3;若新增 GroupC=1024,总 shares=4096,A:B:C=1:2:1。
cgroup 版本换算:v1
cpu.shares(1~UINT_MAX,默认1024),v2cpu.weight(1~10000,默认100),内核内部统一换算为 shares:shares = weight*1024/100,最终全部流入sched_group_set_shares处理。
1.4 sched_group_set_shares 函数定位
用户写cpu.shares文件触发内核文件操作回调,最终调用__sched_group_set_shares,外层封装sched_group_set_shares作为对外统一接口,核心工作:
- 更新
task_group->shares基准权重; - 遍历整机所有 CPU,逐个刷新对应 CPU 上本组
se调度实体权重; - 调用
update_cfs_group()逐级向上刷新父组负载与权重,实时生效新配比。
1.5 关联辅助函数
update_cfs_shares():单 CPU 维度,依据 task_group 基准 shares、当前 CPU 组负载计算实际生效权重;reweight_entity():修改 sched_entity 权重,调整 CFS 红黑树排序位置,触发调度路径更新。
二、环境准备
2.1 软硬件环境清单
| 环境项 | 参数说明 |
|---|---|
| OS 系统 | Ubuntu20.04/22.04 x86_64(内核 5.15、6.1 LTS,主流云服务器标配内核) |
| CPU | x86_64 4 核及以上,方便多任务压测验证 CPU 占比 |
| 内存 | ≥4GB |
| 编译工具链 | gcc-9/gcc-11、make、bison、flex、libncurses-dev、libelf-dev |
| 调试工具 | ftrace、perf、trace-cmd、sysstat (iostat/mpstat)、stress-ng(压测进程) |
| 内核关键配置 | CONFIG_FAIR_GROUP_SCHED=y、CONFIG_CGROUP_CPU=y、CONFIG_FTRACE=y、CONFIG_SCHED_DEBUG=y |
2.2 环境部署步骤
步骤 1:安装依赖工具(可全量复制执行)
# 更新源并安装编译、调试、压测全套依赖 sudo apt update -y sudo apt install build-essential libncurses-dev bison flex libssl-dev libelf-dev stress-ng perf trace-cmd sysstat -y步骤 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 # 沿用当前系统内核配置 cp /boot/config-$(uname -r) .config # 打开配置界面 make menuconfigmake menuconfig中开启下述选项:
General setup → Control Group support → CPU controller for cgroups=y Kernel features → Enable the CFS group scheduling(CONFIG_FAIR_GROUP_SCHED)=y Kernel hacking → Tracers → Function tracer(CONFIG_FTRACE)=y Kernel hacking → Debug scheduler(CONFIG_SCHED_DEBUG)=y保存退出后编译安装内核:
make -j$(nproc) sudo make modules_install sudo make install sudo update-grub重启服务器,在 grub 菜单选中新编译内核进入系统。
步骤 3:挂载 cgroup v1 文件系统(实操使用 v1,接口直观)
# 临时挂载cpu子系统,永久生效写入/etc/fstab sudo mkdir -p /sys/fs/cgroup/cpu sudo mount -t cgroup -o cpu none /sys/fs/cgroup/cpu挂载完成后,/sys/fs/cgroup/cpu目录下自动生成cpu.shares、cpu.cfs_quota_us等配置文件。
2.3 源码目录定位
sched_group_set_shares及相关实现全部在:kernel/sched/fair.ctask_group 结构体:kernel/sched/sched.hcgroup 文件回调函数:kernel/sched/core.c
三、应用场景(302 字)
sched_group_set_shares驱动的动态权重能力是云平台混部资源管控的底层基石。容器 K8s 集群中,运维通过修改 Deployment 资源参数间接调整 pod 对应 cgroup 的 cpu.weight,内核经由该函数实时变更容器组 shares,实现生产业务容器权重高于测试容器,高峰期优先保障核心业务 CPU 资源;中小型数据库服务器混部场景,MySQL 分组 shares 设为 4096、日志采集分组 1024,业务峰值自动按 4:1 分配 CPU,避免日志进程抢占 SQL 算力;嵌入式工控多任务系统中,运动控制任务组调高 shares,后台日志任务降低权重,依靠动态调整满足设备硬实时调度需求;云主机弹性扩容场景,无需重启实例,在线上调业务 cgroup 权重即可瞬时提升 CPU 分配占比,全场景依托sched_group_set_shares完成权重热更新。
四、实际案例与步骤(内核源码 + 用户态实操双案例,全注释可复现)
案例一:内核源码深度解析 sched_group_set_shares 执行逻辑
4.1.1 顶层入口 sched_group_set_shares 源码
// kernel/sched/fair.c int sched_group_set_shares(struct task_group *tg, unsigned long shares) { int i; struct rq_flags rf; struct rq *rq; /* 入参合法性校验,shares最小不能为0,内核默认下限MIN_SHARES=1 */ if (shares < MIN_SHARES) return -EINVAL; /* 根任务组root_task_group不允许修改shares,返回非法参数 */ if (tg == &root_task_group) return -EINVAL; /* 上锁保护task_group成员,防止并发修改 */ mutex_lock(&tg->shares_mutex); /* 新权重与原有一致则直接返回,跳过无效刷新,优化性能 */ if (tg->shares == shares) { mutex_unlock(&tg->shares_mutex); return 0; } /* 第一步:更新task_group基准shares值,落盘组基础权重 */ tg->shares = shares; mutex_unlock(&tg->shares_mutex); /* 第二步:遍历系统全部CPU,逐个刷新每个CPU上本组调度实体se权重 */ for_each_possible_cpu(i) { /* 获取第i号CPU运行队列 */ rq = cpu_rq(i); rq_lock(rq, &rf); /* 调用更新函数,计算当前CPU环境下组实际生效权重 */ update_cfs_shares(tg->se[i], tg->cfs_rq[i]); rq_unlock(rq, &rf); } return 0; }代码说明:
- 用户
echo 2048 > cpu.shares→ cgroup 回调cpu_shares_write_u64解析数值→调用sched_group_set_shares(tg,2048); - 只有权重发生变化才执行全 CPU 遍历刷新,避免频繁写文件带来无效内核开销;
for_each_possible_cpu遍历所有配置 CPU,包括离线 CPU,保证 CPU 上下线后权重配置依然生效。
4.1.2 关键附属函数 update_cfs_shares & reweight_entity
static void update_cfs_shares(struct sched_entity *se, struct cfs_rq *cfs_rq) { struct task_group *tg = cfs_rq->tg; unsigned long new_weight; /* 根据组基准shares、当前队列总负载换算本CPU上se实际权重 */ new_weight = calc_group_shares(cfs_rq); /* 新旧权重不一致,修改调度实体权重,调整CFS红黑树位置 */ if (se->load.weight != new_weight) { reweight_entity(cfs_rq, se, new_weight); } /* 向上递归刷新父任务组权重,实现多级分组联动生效 */ if (cfs_rq_is_group_rq(cfs_rq)) update_cfs_group(se); } /* 重新设置se权重,内核CFS调度依据load.weight分配时间片 */ static void reweight_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, unsigned long weight) { /* 从红黑树临时摘除节点 */ if (se->on_rq) rb_erase(&se->rb_node, &cfs_rq->tasks_timeline); /* 更新调度实体权重 */ se->load.weight = weight; /* 重新插入红黑树,权重变化带来排序变更 */ if (se->on_rq) rb_add(&se->rb_node, &cfs_rq->tasks_timeline); }代码逻辑总结:修改 shares→更新 tg 基准值→全 CPU 刷新 se 权重→变更 se 在 CFS 红黑树位置→调度选任务时按新权重分配 CPU 时间,整套流程同步完成。
案例二:用户态实操:通过修改 cpu.shares 触发 sched_group_set_shares,压测验证资源占比
步骤 1:创建两个 cgroup 分组 group1、group2
# 进入cpu子系统挂载目录 cd /sys/fs/cgroup/cpu # 创建两个任务组目录,内核自动生成对应cpu.shares等文件 sudo mkdir group1 group2 # 查看默认shares(默认新建分组=1024) cat group1/cpu.shares cat group2/cpu.shares步骤 2:编写 CPU 满载测试小程序(无限消耗 CPU,可直接编译运行)
// cpu_stress.c 死循环占用单核CPU #include <stdio.h> int main(void) { while(1){ ; // 空循环持续消耗CPU算力 } return 0; }编译命令:
gcc cpu_stress.c -o cpu_stress步骤 3:启动两个测试进程,分别加入两个分组
# 后台启动第一个进程,获取PID ./cpu_stress & echo $! > group1/cgroup.procs # 后台启动第二个进程 ./cpu_stress & echo $! > group2/cgroup.procs步骤 4:修改 shares,触发内核 sched_group_set_shares 执行
# group1权重1024,group2权重3072,理论满载CPU占比1:3 sudo echo 1024 > group1/cpu.shares sudo echo 3072 > group2/cpu.shares写入文件瞬间,内核 cgroup 回调触发
sched_group_set_shares(group1,1024)、sched_group_set_shares(group2,3072),实时刷新权重。
步骤 5:perf+ftrace 跟踪 sched_group_set_shares 函数调用
# 挂载debugfs用于ftrace sudo mount -t debugfs none /sys/kernel/debug # 清空跟踪缓存 sudo echo > /sys/kernel/debug/tracing/trace # 配置需要跟踪的内核函数 sudo echo sched_group_set_shares > /sys/kernel/debug/tracing/set_ftrace_filter sudo echo update_cfs_shares >> /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 2048 > group1/cpu.shares # 关闭跟踪 sudo echo 0 > /sys/kernel/debug/tracing/tracing_on # 查看跟踪日志,验证函数被调用 cat /sys/kernel/debug/tracing/trace日志预期:日志中出现sched_group_set_shares调用栈,后续跟随update_cfs_shares、reweight_entity调用记录,验证修改 shares 真实走进目标内核函数。
步骤 6:mpstat 观测 CPU 占用比例
# 每秒输出一次CPU统计 mpstat -P ALL 1现象:group1 进程 CPU≈25%,group2≈75%,符合 1:3 权重配比;再次修改 shares 后,占比瞬时变化,验证动态调整生效。
步骤 7:清理测试环境
# 杀掉压测进程 killall cpu_stress # 删除测试分组 sudo rmdir group1 group2案例三:cgroup v2 权重转换验证(v2 cpu.weight 映射 shares)
# 挂载cgroup v2 sudo mkdir -p /sys/fs/cgroup2 sudo mount -t cgroup2 none /sys/fs/cgroup2 cd /sys/fs/cgroup2 sudo mkdir testgrp # v2默认weight=100,修改为200,内核自动换算shares=200*1024/100=2048 echo 200 > testgrp/cpu.weight写入后内核底层依旧调用sched_group_set_shares(tg,2048),v1/v2 用户接口不同,内核统一收敛至同一个权重设置函数。
五、常见问题与解答
Q1:修改 cpu.shares 后 CPU 占比没有按预期变化?
答:shares 是相对权重,仅在分组内进程同时满载争抢 CPU 时才按比例分配;CPU 空闲时系统会把空闲算力全部分配给运行进程,占比不受 shares 约束。排查:使用 stress-ng 打满两个分组 CPU 负载后再观察 mpstat,确认满载场景分配比例;其次用 ftrace 跟踪sched_group_set_shares是否正常触发,若函数无调用说明 cgroup 挂载异常、CONFIG_FAIR_GROUP_SCHED 未开启。
Q2:修改 shares 后 ftrace 看不到 sched_group_set_shares 调用?
答:①内核未开启 CONFIG_FTRACE、CONFIG_FAIR_GROUP_SCHED;②写入的 shares 数值和原有一致,内核直接 return 跳过函数执行;③cgroup cpu 子系统挂载失效,写入文件未走到内核回调。核对:zcat /proc/config.gz | grep FAIR_GROUP_SCHED确认配置 = y,重新挂载 cgroup 后复测。
Q3:多级嵌套 cgroup,子组修改 shares 为何会影响父组调度?
答:update_cfs_shares内部update_cfs_group会向上递归刷新父 task_group 权重,内核分层调度下子组负载变化会传导上层分组,是sched_group_set_shares设计的联动逻辑,符合树形组调度规范;如需隔离可拆分独立顶层分组。
Q4:shares 设置很大(65535),单个分组能否独占多核 CPU?
答:不能,shares 仅控制同 CPU 内分组时间片占比;单进程受限于 CPU 单核,多进程塞满分组内多线程才可占用多核,如需限制核数配合 cpuset 子系统绑定 CPU 核心。
Q5:根分组 root_task_group 为什么禁止调用 sched_group_set_shares 修改 shares?
答:根组承载系统所有未划入自定义 cgroup 的进程,内核固定 root_shares 配置,修改会破坏全系统基础调度基准,源码中if(tg==&root_task_group) return -EINVAL直接拦截入参。
六、实践建议与最佳实践
6.1 线上业务权重配置规范
- 基准配比:生产业务基准 shares=4096、测试环境 = 1024、日志采集 = 512,形成固定梯度,高峰期天然优先保障生产资源;禁止随意设超大 shares(>100000),避免权重溢出带来内核计算异常。
- 动态调优时机:业务流量上涨时在线
echo xxx > cpu.shares调用sched_group_set_shares热调权重,无需重启业务,凌晨低峰回落权重节约资源。
6.2 内核调试排错技巧
- 权重异常排查链路:mpstat 资源异常→ftrace 跟踪
sched_group_set_shares调用→gdb/kgdb 读取task_group->shares实际数值→查看对应 CPU 的sched_entity->load.weight,三层定位配置是否落进内核。 - 高频改 shares 优化:不要循环高频 echo 修改 cpu.shares,频繁触发
sched_group_set_shares全 CPU 遍历刷新,带来不必要调度开销;批量变更建议通过程序批量写入,单次修改目标值。
6.3 内核二次开发优化
- 自研调度策略时,尽量复用
sched_group_set_shares现有刷新逻辑,不要重写全 CPU 遍历代码,规避锁、队列同步 BUG;如需新增自定义权重字段,在 task_group 新增成员,沿用原有更新链路。 - 嵌入式精简内核:不需要组调度时关闭
CONFIG_FAIR_GROUP_SCHED,剔除 task_group 与 sched_group_set_shares 相关代码,缩减内核体积、降低调度开销。
6.4 K8s 容器落地优化
K8s 的 resources.cpu 配置最终转化为 cgroup cpu.weight,底层依托sched_group_set_shares,核心业务容器 requests.cpu 调高对应权重,非核心组件调低,依托内核原生机制实现混部隔离,无需额外第三方资源管控组件。
七、总结与应用延伸
本文从组调度架构、task_group 结构体、sched_group_set_shares源码实现、用户态 cgroup 实操、函数跟踪调试、线上优化六个维度完整拆解 Linux CFS 任务组权重动态调整机制。sched_group_set_shares是连接用户态 cgroup 配置与内核 CFS 调度的关键桥梁,核心设计思路为:用户配置→更新组基准 shares→全 CPU 遍历刷新组调度实体权重→修改 CFS 红黑树排序→调度器按新权重分配 CPU 时间,依托这套机制实现权重在线热变更,也是 cgroup CPU 资源相对配额的内核底层实现。
从技术落地层面,该函数是云原生容器资源调度、物理机多业务混部、嵌入式工控多任务资源隔离的底层依赖;从学术研究角度,本源码可用于 CFS 分层调度论文撰写、调度器性能优化课题的数据支撑。建议读者在现有环境基础上,修改sched_group_set_shares源码(例如增加自定义日志打印)重新编译内核,再次通过 ftrace 和压测程序验证修改效果,深度体会内核权重刷新全链路;工程落地中结合 cpuset+cpu.shares+cpu.cfs_quota 三类配置,组合实现 “CPU 核数绑定 + 相对权重分配 + 最大算力硬限流” 三位一体资源管控方案。