Linux CFS 组调度:shares 权重与 CPU 时间的比例分配
Linux完全公平调度器(CFS)的组调度机制通过shares权重实现CPU资源分配,是服务器混合部署和容器化环境的核心技术。本文深入解析了CFS组调度的实现原理:shares作为相对权重参数,在CPU竞争时按比例分配时间片(如2048:1024即2:1);通过task_group结构体和cgroup层级管理实现分组调度;支持动态修改实时生效。文章结合内核源码分析、多场景实操案例(包括分组压测、层
简介
Linux 完全公平调度器(CFS)自 2.6.23 版本正式合入内核,取代传统 O (1) 调度器,成为系统中普通分时任务的核心调度实现。在单机多业务、容器集群、云主机、服务器混合部署等生产环境中,单一进程维度的 nice 权重调节已无法满足资源划分需求:例如一台服务器同时运行业务服务、日志采集、监控告警、定时任务等多类程序,若不做分组管控,高负载进程会抢占全部 CPU 资源,导致核心业务响应延迟、服务雪崩。
为此内核在 CFS 基础上延伸出组调度(Group Scheduling) 能力,结合 cgroup 实现进程分组管理,而 shares 就是 CFS 组调度中定义分组权重、分配 CPU 时间的核心参数。shares 采用相对权重机制,在多个任务组同时竞争 CPU 时,严格按照各组 shares 数值比例切割 CPU 时间片,保障不同业务组的资源配额与运行稳定性。
对于 Linux 运维工程师、内核开发人员、容器平台研发、云原生架构师而言,吃透 CFS 组调度 shares 的底层原理、层级传递规则、源码逻辑与实战调优,是排查 CPU 资源争抢、优化混合业务部署、定制容器资源策略、撰写内核相关论文与技术报告的必备技能。本文结合内核源码、实操命令、压测案例、问题排查全链路讲解,从基础概念到内核实现,再到线上落地最佳实践,完整拆解 shares 权重如何控制 CPU 分配比例,内容可直接用于工程实践、源码研读与学术调研。
一、核心概念与术语解析
1.1 CFS 基础核心知识
CFS 全称 Completely Fair Scheduler(完全公平调度器),核心设计理念是模拟理想多任务 CPU,让所有就绪任务按照权重公平分享 CPU 时间,核心依赖两个关键要素:
- 调度权重:每个调度实体拥有权重值,默认普通进程权重为
NICE_0_LOAD = 1024,nice 值越小(优先级越高),权重越大,能获取更多 CPU 时间。 - 虚拟运行时间 vruntime:CFS 不依赖固定时间片,而是通过
vruntime标记进程 “公平程度”。进程真实运行时,权重越大,vruntime增长越慢;调度器永远选择vruntime最小的进程执行。 计算公式:
vruntime += 真实运行时间 × (1024 / 进程权重)
1.2 CFS 组调度 与 task_group
普通 CFS 仅针对单进程做公平调度,组调度将一组进程视为一个整体调度实体,内核使用 struct task_group 描述一个任务组,每个 cgroup 对应一个 task_group。
- 组内所有进程的负载、运行时间统一汇总,参与全局调度;
- 组与组之间按照权重竞争 CPU,组内再按照普通 CFS 规则分配资源。
内核源码路径:kernel/sched/sched.h、kernel/sched/fair.c
1.3 shares 权重参数详解
shares 是 task_group 的核心权重字段,对应 cgroup 下 cpu.shares 文件,是组调度的核心:
- 默认值:所有新建任务组默认
shares = 1024,和普通进程基准权重一致; - 作用范围:仅在 CPU 存在竞争时生效。若系统 CPU 空闲,单个组内进程可占用全部 CPU,shares 限制失效;
- 分配规则:多个任务组同时争抢 CPU 时,各组获得的 CPU 占比 = 当前组 shares / 所有就绪组 shares 总和;
- 数据特性:纯相对权重,无绝对 CPU 百分比限制,区别于
cpu.cfs_quota_us硬配额限制。
举个基础示例:存在两个任务组 GroupA (shares=2048)、GroupB (shares=1024),两组满载竞争 CPU 时,CPU 分配比例为 2:1,GroupA 占用约 66.7%,GroupB 占用约 33.3%。
1.4 cgroup 层级传播规则
CFS 组调度支持树形层级结构,shares 权重会逐层传递计算,这是实战中最容易踩坑的点:
- 子组的可用 CPU 上限,由父组分配的资源决定;
- 同一父组下,多个子组按照自身 shares 比例瓜分父组资源;
- 层级越深,权重计算链路越长,分配比例受多级父组共同约束。
1.5 关键结构体释义
1.5.1 task_group 结构体(精简版)
// kernel/sched/sched.h
struct task_group {
/* 组调度权重,对应 cgroup cpu.shares */
unsigned long shares;
/* 每个CPU对应的组CFS运行队列 */
struct cfs_rq **cfs_rq;
/* 每个CPU对应的组调度实体 */
struct sched_entity **se;
/* cgroup 树形节点、带宽控制等扩展字段 */
struct cgroup_subsys_state css;
struct cfs_bandwidth cfs_bandwidth;
};
字段说明:
shares:当前任务组的权重值,可通过 cgroup 文件动态修改;cfs_rq:组专属 CFS 运行队列,存放组内所有就绪进程;se:任务组本身作为一个调度实体,挂载到父组的运行队列中,实现层级调度。
1.5.2 cfs_rq 组运行队列
每个 CPU 都会为每个 task_group 维护独立的 struct cfs_rq,负责管理组内进程的红黑树、总负载、累计运行时间,是组内进程调度的容器。
二、环境准备
2.1 软硬件环境要求
| 分类 | 版本 / 配置要求 |
|---|---|
| 操作系统 | Ubuntu 20.04 / 22.04、CentOS 7/8/9(主流 Linux 发行版均可) |
| 内核版本 | Linux 4.19、5.4、5.15、6.1(主流 LTS 版本,CFS 组调度逻辑完全兼容) |
| 硬件 | x86_64 架构,至少 2 核 4G 内存,保证可以模拟 CPU 压测 |
| 依赖工具 | stress-ng /stress(CPU 压力测试)、libcgroup(cgroup 操作工具)、gcc、make |
| 调试工具 | top、htop、perf、ftrace、gdb(内核调试与观测) |
2.2 环境初始化与依赖安装
2.2.1 安装基础工具与压测软件
以下命令全平台通用,可直接复制执行:
# Ubuntu/Debian 系列
sudo apt update && sudo apt install -y stress-ng libcgroup-tools htop perf
# CentOS/RHEL 系列
sudo yum install -y stress-ng libcgroup-tools htop perf
2.2.2 确认 cgroup v1 环境(本文基于 cgroup v1,生产环境主流)
现代 Linux 默认挂载 cgroup 文件系统,执行以下命令校验:
# 查看cpu子系统挂载状态
mount | grep cgroup | grep cpu
正常输出示例:
tmpfs on /sys/fs/cgroup type tmpfs (ro,nosuid,nodev,noexec,relatime,size=4096k)
cgroup on /sys/fs/cgroup/cpu type cgroup (rw,relatime,cpu)
若未自动挂载,手动执行挂载命令:
sudo mkdir -p /sys/fs/cgroup/cpu
sudo mount -t cgroup -o cpu cpu /sys/fs/cgroup/cpu
2.2.3 内核源码准备(源码阅读与调试使用)
如需跟踪内核源码,下载长期支持版内核:
# 以 Linux 5.15 为例
wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.15.tar.xz
tar -xf linux-5.15.tar.xz
cd linux-5.15
CFS 组调度核心源码路径:
- 结构体定义:
kernel/sched/sched.h - 组调度逻辑、shares 计算、入队出队:
kernel/sched/fair.c
三、应用场景
CFS 组调度与 shares 权重机制广泛应用在服务器混合部署、容器虚拟化、云主机、嵌入式集群等场景。在企业后端服务器中,常将核心交易服务、日志采集、监控脚本、离线计算划分为不同 cgroup 组,通过调整 shares 权重保障核心业务优先占用 CPU,避免离线任务拖垮在线服务。在 Docker、Kubernetes 容器平台中,cpu.shares 是容器默认的 CPU 资源相对限制方案,集群内不同业务容器通过权重划分资源,实现节点资源错峰复用。在云计算公有云场景,云厂商利用组调度为不同租户划分 CPU 权重,保证多租户之间资源隔离与公平竞争。此外,大数据集群、边缘计算网关等多进程常驻场景,也依靠 shares 分组调度,简化资源管控,提升整机资源利用率与系统稳定性。
四、实际案例、源码与实操步骤
本章节分为三部分:内核核心源码解析、命令行实操分组压测、自定义代码验证调度逻辑,所有代码、命令均可直接复制运行。
4.1 内核源码:shares 初始化与权重计算逻辑
4.1.1 task_group 初始化,默认 shares 赋值
内核创建新任务组时,会默认将 shares 设置为 1024,源码位于 fair.c:
// kernel/sched/fair.c
#define NICE_0_LOAD 1024UL
/* 创建新任务组时,初始化调度相关参数 */
int sched_create_group(struct task_group *tg)
{
int cpu;
/* 组调度权重默认赋值为标准值 1024 */
tg->shares = NICE_0_LOAD;
/* 遍历所有CPU,初始化每个CPU对应的组调度队列与调度实体 */
for_each_possible_cpu(cpu) {
struct cfs_rq *cfs_rq = tg->cfs_rq[cpu];
struct sched_entity *se = tg->se[cpu];
/* 初始化组调度实体,组本身作为一个调度单元参与上层调度 */
se->load.weight = tg->shares;
se->vruntime = 0;
}
return 0;
}
代码说明:
NICE_0_LOAD是基准权重 1024,所有新建分组默认继承该值;- 每个 CPU 独立维护组队列与调度实体,适配多核架构;
- 组的
se->load.weight直接绑定shares,这是组权重参与调度的核心关联点。
4.1.2 修改 cpu.shares 时的内核更新逻辑
当用户通过 echo 命令修改 cgroup 的 cpu.shares 文件时,内核触发回调函数,实时更新组权重:
// kernel/sched/fair.c
static int cpu_shares_write_u64(struct cgroup_subsys_state *css,
struct cftype *cft, u64 val)
{
struct task_group *tg = css_tg(css);
unsigned long new_shares = (unsigned long)val;
int cpu;
/* 合法性校验:shares 最小值限制,防止权重为0 */
if (new_shares < 2)
return -EINVAL;
/* 更新任务组的 shares 字段 */
tg->shares = new_shares;
/* 遍历所有CPU,刷新该组在各个CPU上的调度实体权重 */
for_each_possible_cpu(cpu) {
struct sched_entity *se = tg->se[cpu];
/* 更新调度实体权重,立刻生效 */
se->load.weight = new_shares;
/* 触发负载重新计算,重新参与调度排序 */
update_load_avg(se, 0);
}
return 0;
}
代码作用:
- 限制 shares 最小为 2,避免权重失效;
- 修改
cpu.shares后,全 CPU 刷新调度实体权重,实时生效无需重启进程; update_load_avg刷新负载统计,让组权重变化立刻体现在调度逻辑中。
4.1.3 组间 CPU 分配核心计算逻辑
CFS 在选择调度组时,会根据各组 shares 计算负载占比,简化核心逻辑:
// 伪代码:组调度CPU占比计算逻辑
unsigned long total_shares = 0;
struct task_group *tg;
/* 1. 统计当前CPU上所有就绪任务组的shares总和 */
for (遍历当前CPU所有就绪 task_group) {
total_shares += tg->shares;
}
/* 2. 单个组理论CPU占比 = 本组shares / 总shares */
double cpu_ratio = (double)tg->shares / total_shares;
这就是 shares 相对权重分配的底层数学依据。
4.2 实操案例一:命令行创建分组 + 压测验证权重比例
目标:创建两个 cgroup 分组,分别设置 shares=2048、shares=1024,模拟 CPU 满负载,验证 CPU 占比为 2:1。
步骤 1:创建两个任务组
# 进入cpu cgroup根目录
cd /sys/fs/cgroup/cpu
# 创建分组 group1、group2
sudo mkdir group1
sudo mkdir group2
步骤 2:配置两组的 shares 权重
# group1 设置为 2048
echo 2048 | sudo tee group1/cpu.shares
# group2 设置为 1024(默认值,可省略)
echo 1024 | sudo tee group2/cpu.shares
查看配置是否生效:
cat group1/cpu.shares
cat group2/cpu.shares
步骤 3:后台启动 CPU 压测进程,并加入对应分组
新开两个终端窗口分别执行:
# 终端1:在 group1 中启动4核CPU压测
sudo cgexec -g cpu:group1 stress-ng --cpu 4 --timeout 120 &
# 终端2:在 group2 中启动4核CPU压测
sudo cgexec -g cpu:group2 stress-ng --cpu 4 --timeout 120 &
参数说明:--cpu 4 模拟 4 个死循环进程打满 CPU,--timeout 120 压测持续 120 秒。
步骤 4:使用 htop /top 观测 CPU 占用比例
执行 htop,可以清晰看到:
- group1 内进程总 CPU 占用约 66%;
- group2 内进程总 CPU 占用约 33%; 严格符合 2048:1024 = 2:1 的权重比例。
步骤 5:动态修改 shares,验证热生效
在压测运行过程中,动态修改 group2 的 shares 为 2048:
echo 2048 | sudo tee group2/cpu.shares
再次观察 htop,两组 CPU 占用迅速变为 1:1,证明 shares 修改实时生效。
步骤 6:清理测试环境
# 终止压测进程
pkill stress-ng
# 删除测试分组
sudo rmdir /sys/fs/cgroup/cpu/group1
sudo rmdir /sys/fs/cgroup/cpu/group2
4.3 实操案例二:层级 cgroup 验证 shares 传递规则
本案例验证父子分组的权重传递,架构:
根分组
├─ parent_group (shares=1024)
│ ├─ child1 (shares=1024)
│ └─ child2 (shares=1024)
理论结果:父组占用整机 50% CPU,两个子组平分父组资源,各占整机 25%。
# 1. 创建父子层级分组
sudo mkdir -p /sys/fs/cgroup/cpu/parent_group/child1
sudo mkdir -p /sys/fs/cgroup/cpu/parent_group/child2
# 2. 设置父组权重
echo 1024 | sudo tee /sys/fs/cgroup/cpu/parent_group/cpu.shares
# 3. 设置两个子组权重
echo 1024 | sudo tee /sys/fs/cgroup/cpu/parent_group/child1/cpu.shares
echo 1024 | sudo tee /sys/fs/cgroup/cpu/parent_group/child2/cpu.shares
# 4. 根目录启动一组压测(对比组,shares=1024)
sudo cgexec -g cpu:/ stress-ng --cpu 2 --timeout 120 &
# 5. 两个子组分别启动压测
sudo cgexec -g cpu:parent_group/child1 stress-ng --cpu 2 --timeout 120 &
sudo cgexec -g cpu:parent_group/child2 stress-ng --cpu 2 --timeout 120 &
观测结果:根分组、父分组整体各占 50% CPU;父分组内部两个子组各占 25%,完全匹配层级权重计算规则。
4.4 实操案例三:C 语言代码实现进程加入 cgroup
编写简易 C 程序,创建死循环进程,并主动将自身加入指定 cgroup,验证组调度效果,代码可直接编译运行。
4.4.1 测试代码 cgroup_demo.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#define CGROUP_PATH "/sys/fs/cgroup/cpu/test_group"
/* 将当前进程PID加入指定cgroup */
int add_proc_to_cgroup(const char *cgroup_path)
{
char tasks_path[256];
FILE *fp;
pid_t pid = getpid();
// 拼接 tasks 文件路径
snprintf(tasks_path, sizeof(tasks_path), "%s/tasks", cgroup_path);
// 写入当前进程PID到cgroup任务列表
fp = fopen(tasks_path, "w");
if (!fp) {
perror("fopen failed");
return -1;
}
fprintf(fp, "%d", pid);
fclose(fp);
return 0;
}
int main(int argc, char *argv[])
{
// 1. 创建cgroup目录(命令行提前执行,代码仅做演示)
printf("Process PID: %d, join cgroup...\n", getpid());
// 2. 将当前进程加入 test_group
if (add_proc_to_cgroup(CGROUP_PATH) < 0) {
return -1;
}
// 3. 死循环模拟CPU密集型任务
while (1) {
// 空循环,持续占用CPU
}
return 0;
}
代码说明:
- Linux cgroup 通过
tasks文件管理组内进程,写入 PID 即可完成分组绑定; - 死循环模拟 CPU 密集业务,用于压测;
- 代码不依赖第三方库,标准 C 编译即可。
4.4.2 编译与运行
# 1. 提前创建分组并设置shares
sudo mkdir /sys/fs/cgroup/cpu/test_group
echo 1024 | sudo tee /sys/fs/cgroup/cpu/test_group/cpu.shares
# 2. 编译代码
gcc cgroup_demo.c -o cgroup_demo
# 3. 后台运行程序
sudo ./cgroup_demo &
执行 htop 查看进程归属与 CPU 占用,修改 test_group/cpu.shares 可实时观察 CPU 占比变化。
4.5 使用 ftrace 跟踪内核组调度函数
通过内核跟踪工具观测 shares 变更、组入队等内核函数,用于深度调试:
# 挂载debugfs
sudo mount -t debugfs none /sys/kernel/debug
# 清空跟踪缓存
echo > /sys/kernel/debug/tracing/trace
# 过滤跟踪组调度核心函数
echo cpu_shares_write_u64 >> /sys/kernel/debug/tracing/set_ftrace_filter
echo sched_create_group >> /sys/kernel/debug/tracing/set_ftrace_filter
# 开启跟踪
echo function > /sys/kernel/debug/tracing/current_tracer
echo 1 > /sys/kernel/debug/tracing/tracing_on
此时修改 cpu.shares,再执行以下命令查看调用栈:
cat /sys/kernel/debug/tracing/trace
可直观看到 cpu_shares_write_u64 函数被触发,验证内核执行链路。
五、常见问题与解答
Q1:设置了 cpu.shares=512,为什么进程依然能占满 100% CPU?
解答:cpu.shares 是相对权重,仅在多个分组 / 进程同时竞争 CPU时生效。如果系统内只有当前组进程运行,CPU 无争抢,shares 限制会失效。若需要硬性限制 CPU 使用率,需搭配 cpu.cfs_quota_us 做绝对配额。
Q2:父子层级 cgroup 中,子组 shares 总和大于父组,资源会溢出吗?
解答:不会。子组的所有资源上限由父组分配的 CPU 比例决定,子组之间仅在父组分配的资源池内按照自身 shares 比例划分,不会突破父组限制。层级调度是自上而下逐层分配。
Q3:修改 cpu.shares 后,为什么不需要重启进程就能生效?
解答:内核在 cpu_shares_write_u64 回调中,会立刻刷新该任务组所有 CPU 调度实体的权重,并更新负载统计。CFS 调度是动态轮询机制,权重变更会在下一次调度周期生效,无需重启进程。
Q4:shares 可以设置为 0 或者负数吗?
解答:不可以。内核源码中做了合法性校验,shares 最小值限制为 2,写入 0、负数会直接返回参数错误,配置不生效。
Q5:同一个 CPU 上多个进程属于同一个组,组内进程如何分配 CPU?
解答:组内部不再参考 shares,完全遵循标准 CFS 调度规则,根据进程 nice 值、虚拟运行时间 vruntime 做公平调度;shares 只作用于组与组之间的资源划分。
Q6:多核 CPU 场景下,shares 是按整机计算还是按单个核心计算?
解答:CFS 组调度以单个 CPU 核心为单位独立计算权重。多核环境下,每个核心单独执行组调度逻辑,整机 CPU 占用是所有核心的累加结果。
六、实践建议与最佳实践
6.1 线上业务分组规划建议
- 核心业务独立分组:将交易、支付、网关等核心服务单独划分 cgroup,调高 shares(如 2048、4096),优先保障资源;离线计算、日志解析、备份任务降低 shares,避免抢占。
- 分组粒度不宜过细:一台服务器分组数量建议控制在 10 个以内,过多分组会增加内核组调度、红黑树遍历开销,轻微影响系统性能。
- 区分相对权重与硬配额:高并发核心业务,
cpu.shares做优先级划分,同时搭配cpu.cfs_quota_us做兜底硬限制,防止异常进程打满 CPU。
6.2 调优与调试技巧
- 压测验证标准流程:上线前必做压测,多组满载场景下观测 CPU 占比是否符合预期权重,提前发现层级配置错误。
- 问题排查顺序:CPU 争抢异常 → 查看 cgroup 层级结构 → 核对各级 shares 数值 → 用 ftrace 跟踪内核调度函数 → 检查进程是否正确加入分组。
- 权限管理:生产环境禁止普通用户修改
cpu.shares,防止人为篡改导致资源失衡。
6.3 容器场景最佳实践
- Docker/K8s 中
--cpu-shares本质就是修改 cgroupcpu.shares,适合集群内容器优先级划分; - 容器密集节点,优先使用层级 cgroup,按业务租户划分父组,再按服务划分子组,简化管理。
6.4 内核定制与二次开发建议
- 二次开发调度策略时,不要修改
shares基础计算逻辑,会破坏 CFS 公平性;可基于现有组调度增加业务标签、优先级扩展。 - 高并发嵌入式设备中,若分组数量固定,可简化层级调度逻辑,减少负载计算开销,降低调度时延。
七、总结与应用延伸
本文从 CFS 基础原理、task_group 结构体、shares 内核源码、命令行实操、代码开发、层级规则、问题排查等维度,完整讲解了 Linux CFS 组调度中 shares 权重的工作机制与 CPU 分配逻辑。核心要点总结:
shares是相对权重,默认值 1024,仅在 CPU 竞争时生效,按各组权重比例分配 CPU 时间;- CFS 组调度支持树形层级结构,资源自上而下逐层分配,子组无法突破父组资源上限;
- 修改
cpu.shares实时生效,内核通过刷新调度实体权重、负载统计完成更新; - shares 用于优先级划分,无法做绝对 CPU 限制,需结合
cfs_quota实现软硬双重管控。
CFS 组调度与 shares 机制是现代 Linux 资源隔离的基石,当前几乎所有服务器混合部署、容器虚拟化、云主机、边缘计算、大数据集群都依赖这套能力。掌握其底层原理,不仅可以解决线上 CPU 资源争抢、业务卡顿等故障,还能支撑容器平台调度策略开发、内核裁剪、实时系统优化等深度工作。
建议读者结合本文源码与实操命令,在测试机中反复验证单组、多组、层级分组三种场景,尝试修改内核源码观察调度行为变化,真正做到从理论落地到实战。在实际项目中,根据业务重要程度合理规划分组与权重,让 Linux CPU 资源发挥最大利用率,同时保障核心业务的稳定性。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐



所有评论(0)