简介从 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_entitygroup 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调度实体简称 seCFS 的统一调度单元不管是单个 task 进程还是 task_group 任务组都封装为 sched_entity 参与红黑树调度核心字段vruntime(虚拟运行时间)、load(权重负载)、run_node(红黑树挂载节点)CFS 依据 vruntime 升序排序红黑树优先调度 vruntime 最小的实体。cfs_rqCFS 运行队列依附于 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 setask_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_rqtask_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 / Debian1164 位内核 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_DEBUG2.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 schedulingCONFIG_CGROUP_SCHEDy Kernel hacking → Compile-time checks and compiler options → Compile the kernel with debug info Kernel hacking → Tracers → Function tracer(CONFIG_FTRACEy)保存退出编译安装内核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.shares2.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_groupper-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 # 配置权重groupA2048groupB1024理论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 i0;i4;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步骤 4top 查看 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日志解读日志中cpu1的操作只会访问对应分组 se [1]、cfs_rq [1]cpu2访问 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 架构天然特性。Q2cgroup 配置 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 在线数量变更热插拔 CPUtask_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 内核裁剪、定制调度系统开发中。
Linux 组调度的多 CPU 支持:per-CPU 调度实体与运行队列
简介从 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_entitygroup 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调度实体简称 seCFS 的统一调度单元不管是单个 task 进程还是 task_group 任务组都封装为 sched_entity 参与红黑树调度核心字段vruntime(虚拟运行时间)、load(权重负载)、run_node(红黑树挂载节点)CFS 依据 vruntime 升序排序红黑树优先调度 vruntime 最小的实体。cfs_rqCFS 运行队列依附于 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 setask_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_rqtask_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 / Debian1164 位内核 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_DEBUG2.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 schedulingCONFIG_CGROUP_SCHEDy Kernel hacking → Compile-time checks and compiler options → Compile the kernel with debug info Kernel hacking → Tracers → Function tracer(CONFIG_FTRACEy)保存退出编译安装内核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.shares2.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_groupper-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 # 配置权重groupA2048groupB1024理论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 i0;i4;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步骤 4top 查看 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日志解读日志中cpu1的操作只会访问对应分组 se [1]、cfs_rq [1]cpu2访问 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 架构天然特性。Q2cgroup 配置 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 在线数量变更热插拔 CPUtask_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 内核裁剪、定制调度系统开发中。