Linux 组调度的多 CPU 支持:per-CPU 调度实体与运行队列
Linux内核从2.6.38版本引入per-CPU分层调度架构,解决了CFS完全公平调度器在多核环境中的全局锁竞争问题。该架构通过为每个task_group分配独立的per-CPU调度实体(groupse)和运行队列(cfs_rq),实现CPU资源的本地化管理和无锁竞争,显著提升了容器、云服务器等场景下的调度性能。文章深入解析了该架构的核心设计、源码实现、应用场景及调试方法,并提供了详细的用户态c
简介
从 Linux 2.6.23 合入 CFS 完全公平调度器开始,内核逐步引入task_group 组调度机制,依托 cgroup 完成 CPU 资源配额、权重隔离,是容器、云服务器、嵌入式多业务分区的底层调度底座。早期组调度原型采用全局 task_group 调度实体,所有 CPU 共用同一个 group sched_entity,多核 SMP 环境下修改组负载、vruntime、红黑树入队时必须争抢全局调度锁,在数百容器并发的服务器场景中,锁自旋损耗会占到 CPU 耗时 15% 以上,高并发下系统吞吐断崖下跌。
为解决多核锁竞争瓶颈,内核从 v2.6.38 版本落地per-CPU 分层调度架构:每个 task_group 针对系统内每一颗 CPU,独立分配一组 sched_entity(group se)与专属 cfs_rq 运行队列,单个 CPU 上的任务、同组进程仅操作本 CPU 私有的调度数据,操作时只需要持有本地 rq 自旋锁,彻底消灭跨 CPU 全局锁争抢问题。
这套 per-CPU 分层设计如今是 Docker、K8s 容器 CPU 限额、云主机租户资源隔离、工控多业务分区、安卓前台 / 后台进程资源划分的核心实现逻辑。对于 Linux 内核开发、云计算运维、嵌入式实时开发人员,吃透 per-CPU se 与 cfs_rq 的层级关系、源码维护逻辑、负载分摊规则,是排查容器 CPU 抢占异常、调度抖动、cgroup 配额失效的必备技能,同时也是撰写调度优化论文、内核裁剪定制的核心参考内容。本文从基础概念、环境搭建、内核源码拆解、用户态 cgroup 实操、ftrace 内核跟踪、故障排查、工程最佳实践完整落地实战内容。
一、核心概念与术语解析
1.1 CFS 基础调度组件
- sched_entity(调度实体,简称 se):CFS 的统一调度单元,不管是单个 task 进程,还是 task_group 任务组,都封装为 sched_entity 参与红黑树调度,核心字段
vruntime(虚拟运行时间)、load(权重负载)、run_node(红黑树挂载节点),CFS 依据 vruntime 升序排序红黑树,优先调度 vruntime 最小的实体。 - cfs_rq(CFS 运行队列):依附于 CPU 物理运行队列
struct rq,单个 CPU 的 CFS 就绪容器,内置红黑树根rb_root_cached,挂载所有就绪调度实体,维护队列总负载、队列任务计数、最小 vruntime 缓存指针。 - task_group(任务组):cgroup cpu 子系统对应的资源分组,一个组内可包含数十上百个进程,支持通过
cpu.shares配置权重、cpu.cfs_quota_us配置 CPU 最大配额,组调度的顶层管理结构。
1.2 per-CPU 组调度核心设计定义
1.2.1 per-CPU group se
task_group结构体内部定义数组struct sched_entity **se;,se [cpu] 代表该任务组在第 cpu 号 CPU 上专属的组调度实体,系统有 N 颗 CPU 则数组长度为 N,每个 CPU 的 group se 独立管理本 CPU 上属于该组的所有子进程 se,互不干扰。
- 进程绑定在 CPU0:进程 se 挂载到 CPU0 对应 task_group->se [0] 下属 cfs_rq;
- 进程迁移到 CPU1:先从 CPU0 的 group se 出队,再入队 CPU1 的 group se。
1.2.2 per-CPU cfs_rq
task_group配套struct cfs_rq **cfs_rq;数组,cfs_rq[cpu]是任务组在对应 CPU 上的私有 CFS 运行队列,形成分层嵌套队列结构:
CPUx物理rq->rq.cfs_rq(根CFS队列)
↓挂载多个task_group->se[x](组调度实体)
↓每个group se挂靠task_group->cfs_rq[x](组内私有队列)
↓挂载组内所有普通进程task se
这是分层调度的关键:普通进程先入本组 per-CPU cfs_rq,组汇总负载后由 group se 作为整体,挂入 CPU 顶层根 cfs_rq 参与系统 CPU 时间分片竞争。
1.3 锁优化核心原理
未做 per-CPU 改造前:全 CPU 共用 1 个 group se,任意 CPU 上进程启停都要加全局 group 锁修改 se 负载; 改造后:CPU0 修改本组 se [0] 只拿 CPU0 本地 rq 锁,CPU1 修改 se [1] 拿 CPU1 本地锁,无跨 CPU 锁争抢,SMP 多核扩展性大幅提升,CPU 核数越多收益越明显。
1.4 配套调试工具清单
- 用户态:
cgcreate/cgset(cgroup v1)、mount -t cgroup2(cgroup v2)、taskset(进程绑核)、stress-ng(CPU 压力生成) - 内核调试:ftrace (函数跟踪)、perf sched (调度采样)、gdb/kgdb (内核结构体在线查看)
二、环境准备
2.1 软硬件环境配置
| 分类 | 参数明细 |
|---|---|
| 操作系统 | Ubuntu22.04 LTS / Debian11(64 位),内核 5.15/6.1 LTS(主流商用内核,源码结构一致) |
| CPU 硬件 | x86_64 4 核及以上处理器(推荐 8 核,便于多 CPU 负载观测 per-CPU 隔离效果) |
| 内存 | ≥4GB,编译内核 + 压测预留资源 |
| 编译依赖 | gcc11+、make、libncurses-dev、bison flex、libelf-dev |
| 内核配置 | 开启 CONFIG_CGROUP_CPUACCT、CONFIG_CGROUP_SCHED、CONFIG_FTRACE、CONFIG_SCHED_DEBUG |
2.2 环境部署步骤
步骤 1:安装编译与 cgroup 依赖
# 更新源并安装全套依赖,命令一键复制执行
sudo apt update -y
sudo apt install build-essential libncurses-dev bison flex libssl-dev libelf-dev stress-ng cgroup-tools -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 menuconfig
在图形配置界面开启关键选项:
General setup → Control Group support → CPU group scheduling(CONFIG_CGROUP_SCHED=y)
Kernel hacking → Compile-time checks and compiler options → Compile the kernel with debug info
Kernel hacking → Tracers → Function tracer(CONFIG_FTRACE=y)
保存退出,编译安装内核:
make -j$(nproc)
sudo make modules_install
sudo make install
sudo update-grub
重启系统,在 grub 菜单选择新编译内核启动。
步骤 3:挂载 cgroup v1 cpu 子系统(实操用)
sudo mkdir -p /sys/fs/cgroup/cpu
sudo mount -t cgroup -o cpu none /sys/fs/cgroup/cpu
# 验证挂载成功
ls /sys/fs/cgroup/cpu | grep cpu.shares
2.3 源码路径定位
组调度 per-CPU 核心源码全部在如下文件:
kernel/sched/sched.h // task_group、cfs_rq、sched_entity结构体定义
kernel/sched/fair.c // per-CPU se创建、入队出队、负载更新核心函数
三、应用场景(302 字)
该 per-CPU 分层架构是云数据中心容器资源隔离的底层基石,K8s/Docker 创建业务容器时,每个业务 Namespace 对应独立 task_group,容器内进程自动归入对应分组,依托 per-CPU cfs_rq 实现单 CPU 内权重分配。在 8 核云主机上部署数十个业务容器,不同业务进程分散绑定不同 CPU,各自修改本组对应 CPU 的 se 与 cfs_rq,无全局锁冲突,保障多租户 CPU 配额精准生效。同时在工业嵌入式 Linux 多分区场景,工控机多 CPU 分别运行伺服控制、人机交互、数据采集三组业务,每组创建独立 task_group,依靠 per-CPU 调度实体隔离 CPU 算力,避免前台重载业务抢占后台采集任务。此外在安卓手机系统中,前台 APP、后台服务、系统内核进程分属不同 task_group,per-CPU 架构保证前台进程优先获取 CPU,优化整机调度流畅度。
四、实际案例与步骤(内核源码 + 用户态实操 + 调试代码)
4.1 内核源码解析:task_group per-CPU 结构体定义
截取sched.h原版代码,附带详细注释,可对照内核源码核对:
// kernel/sched/sched.h 节选,Linux6.1原生代码
#ifdef CONFIG_CGROUP_SCHED
struct task_group {
struct cgroup_subsys_state css;
struct task_group *parent; // 父任务组,支持层级嵌套分组
/* per-CPU核心数组:每个CPU对应一个group调度实体 */
struct sched_entity **se;
/* per-CPU核心数组:每个CPU对应本组私有CFS运行队列 */
struct cfs_rq **cfs_rq;
unsigned long shares; // 组默认权重,映射cpu.shares
struct cfs_bandwidth cfs_b; // CPU带宽配额,对应cfs_quota
};
#endif
// CFS运行队列cfs_rq结构体
struct cfs_rq {
struct rb_root_cached tasks_timeline; // 红黑树根,挂载进程se
struct sched_entity *curr; // 当前正在CPU运行的调度实体
struct load_weight load; // 当前队列总负载权重
unsigned int nr_running; // 就绪任务数量
struct task_group *tg; // 归属的任务组
};
// 调度实体sched_entity通用结构(进程/组共用)
struct sched_entity {
struct load_weight load;
u64 vruntime; // 虚拟运行时间,CFS排序关键字
struct rb_node run_node; // 红黑树挂载节点
struct cfs_rq *cfs_rq; // 当前所属cfs_rq队列
struct sched_entity *parent; // 组调度时指向父group se
};
代码说明:se[]与cfs_rq[]是 per-CPU 设计的核心载体,内核创建 task_group 时根据在线 CPU 数量动态分配数组内存,每个 CPU 独占一组 se/cfs_rq,天然隔离不同 CPU 的数据访问。
4.2 task_group 创建时 per-CPU 资源分配源码(fair.c)
创建新 cgroup 任务组时,内核alloc_task_group()动态分配 per-CPU se 与 cfs_rq 数组:
// kernel/sched/fair.c 简化源码
static struct task_group *alloc_task_group(struct task_group *parent)
{
struct task_group *tg;
int cpu;
// 获取系统在线CPU总数
int nr_cpu = num_possible_cpus();
tg = kzalloc(sizeof(*tg), GFP_KERNEL);
// 动态分配per-CPU se数组
tg->se = kcalloc(nr_cpu, sizeof(struct sched_entity *), GFP_KERNEL);
// 动态分配per-CPU cfs_rq数组
tg->cfs_rq = kcalloc(nr_cpu, sizeof(struct cfs_rq *), GFP_KERNEL);
// 逐个CPU初始化本组se与私有cfs_rq
for_each_possible_cpu(cpu) {
// 分配单个CPU对应的group se
tg->se[cpu] = alloc_sched_entity();
// 分配单个CPU对应的组私有cfs_rq
tg->cfs_rq[cpu] = alloc_cfs_rq(tg);
// 绑定se与自身cfs_rq
tg->se[cpu]->cfs_rq = tg->cfs_rq[cpu];
}
tg->parent = parent;
tg->shares = DEFAULT_SHARES; // 默认权重1024
return tg;
}
逻辑讲解:for_each_possible_cpu遍历所有 CPU,逐个初始化对应下标资源,CPU0 对应数组下标 0、CPU1 下标 1,进程在哪颗 CPU 就绪,就使用该下标对应的 se 与 cfs_rq,实现物理 CPU 与调度资源一一绑定。
4.3 进程入队分层挂载逻辑源码
普通进程唤醒入队,先挂载到本组 per-CPU cfs_rq,再由 group se 挂载到 CPU 顶层根 cfs_rq:
// 进程se入队函数简化
static void enqueue_task_fair(struct rq *rq, struct task_struct *p, int flags)
{
struct sched_entity *se = &p->se;
struct task_group *tg = task_group(p); // 获取进程所属task_group
int cpu = rq->cpu;
// 第一层:进程se入本组当前CPU私有cfs_rq
enqueue_entity(tg->cfs_rq[cpu], se, flags);
// 向上回溯父组,逐层将group se入上层cfs_rq,最终挂入CPU根队列
for (; tg; tg = tg->parent) {
struct sched_entity *group_se = tg->se[cpu];
enqueue_entity(rq->cfs_rq, group_se, flags);
}
}
关键优化:整个入队过程只持有rq->lock(当前 CPU 本地锁),全程不触碰其他 CPU 的 task_group 资源,无跨 CPU 锁竞争。
4.4 用户态实操:cgroup 分组 + CPU 压力测试(可直接复制运行)
实操目标:创建两个 task_group 分组,配置不同 shares 权重,验证 per-CPU 权重隔离效果
步骤 1:创建两个 cgroup 任务组 groupA、groupB
# 进入cpu子系统挂载目录
cd /sys/fs/cgroup/cpu
# 创建分组
sudo mkdir groupA groupB
# 配置权重:groupA=2048,groupB=1024,理论CPU占用2:1
echo 2048 | sudo tee groupA/cpu.shares
echo 1024 | sudo tee groupB/cpu.shares
步骤 2:编写 CPU 压测程序 stress.c,生成死循环吃满 CPU 的进程
// stress.c 编译:gcc stress.c -o stress -pthread
#define _GNU_SOURCE
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <sched.h>
// 死循环消耗单核CPU
void *cpu_load(void *arg)
{
while(1){}
return NULL;
}
int main()
{
pthread_t tid;
// 创建4个线程占用CPU
for(int i=0;i<4;i++){
pthread_create(&tid,NULL,cpu_load,NULL);
}
pause();
return 0;
}
编译命令:
gcc stress.c -o stress -pthread
步骤 3:分别把进程加入两个分组
# 启动进程放入groupA
sudo ./stress &
PID_A=$!
echo $PID_A | sudo tee groupA/cgroup.procs
# 再启动进程放入groupB
sudo ./stress &
PID_B=$!
echo $PID_B | sudo tee groupB/cgroup.procs
步骤 4:top 查看 CPU 占比,验证 2:1 权重分配
top
# 按P按CPU排序,groupA进程CPU占用约66%,groupB约33%
步骤 5:进程绑核到 CPU1,观测 per-CPU 独立队列生效
# 将groupA所有进程绑定CPU1
sudo taskset -p 0x02 $PID_A
# groupB进程绑定CPU2
sudo taskset -p 0x04 $PID_B
# 查看进程所在CPU
ps -eo pid,psr,cmd | grep stress
现象:CPU1 只有 groupA 负载、CPU2 只有 groupB 负载,两个分组分别操作各自 CPU 对应的 per-CPU se/cfs_rq,互不干扰,锁完全隔离。
4.5 Ftrace 跟踪 per-CPU 调度函数,观测内核调用链路
通过 ftrace 抓取enqueue_entity、alloc_task_group、update_cfs_rq_load,直观查看 per-CPU 资源更新时机:
# 挂载debugfs
sudo mount -t debugfs none /sys/kernel/debug
cd /sys/kernel/debug/tracing
# 清空跟踪缓存
echo > trace
# 配置需要跟踪的内核函数
echo enqueue_entity >> set_ftrace_filter
echo dequeue_entity >> set_ftrace_filter
echo update_cfs_rq_load >> set_ftrace_filter
# 开启函数跟踪
echo function > current_tracer
echo 1 > tracing_on
# 新开终端重启压测进程,触发入队出队
killall stress;./stress &
# 关闭跟踪
echo 0 > tracing_on
# 查看日志,可看到不同CPU编号对应不同tg->se[cpu]操作
cat trace
日志解读:日志中cpu=1的操作只会访问对应分组 se [1]、cfs_rq [1],cpu=2访问 se [2],完美印证 per-CPU 资源隔离。
4.6 perf 调度采样分析
# 采样10秒调度事件,分析组调度实体运行情况
sudo perf sched record -g sleep 10
sudo perf sched latency
五、常见问题与解答
Q1:修改某一个 CPU 上分组进程的 cpu.shares,其他 CPU 同分组负载不受影响?
答:是的。shares 权重作用于每个 CPU 的 per-CPU group se,修改权重只会刷新当前 CPU 对应 cfs_rq 负载,其他 CPU 的 se 与 cfs_rq 独立存在,参数互不联动。实操中把进程从 CPU0 迁移到 CPU1,权重计算自动切换为 CPU1 分组配置,这是 per-CPU 架构天然特性。
Q2:cgroup 配置 cpu.cfs_quota_us 配额不生效,单个 CPU 超限但多 CPU 总和超限额?
答:cfs_quota 是per-CPU 配额,配额数值是单 CPU 最大可用时间,内核按每个 CPU 的 cfs_rq 单独统计运行时长。若要限制全组总 CPU,需要绑定所有进程到同一颗 CPU;排查用 ftrace 跟踪throttle_cfs_rq函数,该函数触发即代表对应 CPU 的 per-CPU cfs_rq 被限流。
Q3:进程跨 CPU 迁移时,组调度的 se 如何变更?
答:进程从 CPU3 迁移到 CPU5,内核先在 CPU3 调用dequeue_entity从 tg->cfs_rq [3]、tg->se [3] 出队;再在 CPU5 执行enqueue_entity挂载到 tg->cfs_rq [5] 与 tg->se [5],两端分别操作各自 CPU 私有队列,仅持有对应 CPU 本地 rq 锁,无全局锁。
Q4:系统 CPU 在线数量变更(热插拔 CPU),task_group 的 se/cfs_rq 数组会自动扩容吗?
答:主流内核 5.15 + 支持 CPU 热插拔回调,内核在 CPU_UP 回调中动态扩容 task_group 的 per-CPU 数组,分配新增 CPU 对应的 se 与 cfs_rq;老旧内核不支持,新增 CPU 上分组进程会落入 root_group 调度队列。
Q5:为什么高并发容器场景下关闭组调度会大幅提升锁开销?
答:关闭 CONFIG_CGROUP_SCHED 后,所有进程统一使用 root_task_group 全局 se,所有 CPU 进程操作共用同一个 group se,每次入队出队争抢全局锁,CPU 核数越高自旋损耗越大,打开 per-CPU 组调度即可规避。
六、实践建议与最佳实践
6.1 内核调试最佳技巧
- 结构体在线查看:使用 kgdb 调试内核,通过
print tg->se[0]查看 CPU0 组调度实体,print tg->cfs_rq[0]->load查看单 CPU 分组负载,精准定位负载统计异常。 - 故障定位顺序:cgroup 配额失效排查顺序→确认进程绑核 CPU 编号→核对对应 cpu 下 tg->cfs_rq [cpu] 参数→ftrace 跟踪 throttle_cfs_rq 触发时机。
6.2 业务部署优化方案
- 容器绑核部署:生产环境容器进程固定绑定 CPU,避免频繁跨 CPU 迁移,减少 per-CPU se 反复入队出队带来的红黑树平衡开销,云主机部署推荐按 NUMA 节点分组绑定。
- 分层 cgroup 设计:按业务大类创建父 task_group,细分业务创建子 task_group,依托 per-CPU 分层队列逐级分摊 CPU 权重,适配 K8s 命名空间资源管控。
- 避免超大分组:单个 task_group 不要在同一 CPU 挂载上千进程,会拉高单颗 CPU cfs_rq 红黑树平衡耗时,拆分多个子分组分摊负载。
6.3 内核定制优化建议
自研调度改造时,禁止删除 per-CPU se/cfs_rq 数组设计;如需新增分组统计项,在 per-CPU 结构体内部新增字段,保持单 CPU 数据本地化,避免引入全局共享变量造成锁回退。
七、总结与应用延伸
本文完整拆解 CFS 组调度 per-CPU 架构的设计思想、结构体定义、内核源码逻辑、用户态 cgroup 落地实操与调试手段,per-CPU se 与 cfs_rq 数组是 Linux 解决 SMP 多核锁竞争的经典设计,核心思想是数据跟着 CPU 走,单个 CPU 的调度资源本地私有化,从架构上消除跨 CPU 全局锁瓶颈。分层嵌套的队列结构既满足 cgroup 层级资源隔离需求,又保障 CFS 公平调度权重精准分摊。
工程落地层面,这套架构支撑云计算容器资源隔离、工控多业务分区、移动端进程资源管控三大主流场景;内核研发与论文写作层面,该设计是 SMP 调度扩展性优化的经典案例,可用于调度算法优化、多核锁优化方向的论文论据。
建议读者基于本文测试代码,自行修改内核alloc_task_group源码,打印每个 CPU se 的内存地址,直观验证 per-CPU 内存独立分配;修改进程绑定不同 CPU,用 perf 与 ftrace 跟踪负载变化,加深对分层调度与 per-CPU 隔离的理解,将这套设计思路落地到嵌入式 Linux 内核裁剪、定制调度系统开发中。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)