Linux 组调度的 task_fork:子任务的组归属继承
本文深入解析Linux CFS完全公平调度器中任务组(task_group)的fork继承机制。该机制是容器资源隔离、云服务器配额管控等场景的核心基础,确保子进程自动继承父进程的cgroup分组属性。文章详细拆解了内核关键数据结构、调度队列层级、task_fork调用链路,并通过源码分析(kernel 6.1)揭示了cpu_cgroup_fork->sched_change_group的分组
简介
在 Linux CFS 完全公平调度体系中,任务组(task_group)是实现 CPU 资源按业务分组隔离、配额管控、权重分配的核心载体,依托 cgroup-cpu 子系统落地,广泛应用在容器虚拟化、云服务器资源隔离、后台服务资源削峰、嵌入式多业务分区等生产场景。默认规则下,进程通过 fork/clone 创建子任务时,子进程会无条件继承父进程绑定的 task_group 分组属性,这条继承链路是保障组调度资源统计、配额限流、权重分摊完整性的底层基石。
从内核运行逻辑来看,若 fork 打破组继承规则,同一业务派生的批量子进程散落至系统默认根任务组,会直接导致 CPU 配额统计失真、预设的分组带宽限制失效,容器调度超配、业务抢占资源紊乱、线上服务 CPU 超限被限流等故障。对于内核研发、云平台运维、容器底层开发、嵌入式实时系统工程师而言,吃透 task_fork 阶段任务组的继承源码、触发条件、边界分支、异常修改逻辑,既是深入理解 CFS 组调度分层运行队列的必经之路,也是排查容器 CPU 配额失效、进程资源管控异常、定制自研调度分组策略的必备知识点。本文立足 Linux5.15/6.1 长期稳定内核,从概念拆解、环境编译、源码逐行剖析、用户态实操验证、问题排查到工程最佳实践全链路落地,源码与实操命令均可直接复现,适配学术论文撰写、项目技术调研与线上故障复盘。
一、核心概念与术语解析
1.1 task_group 任务组基础定义
struct task_group是内核组调度的管理单元,每一个 cgroup-cpu 目录对应一个 task_group 实例,内核源码路径kernel/sched/sched.h、kernel/sched/fair.c。关键组成:
- tg_cfs_rq:每个 CPU 绑定一个分组专属 CFS 运行队列,实现分层调度,分组 CPU 配额、权重全部挂载在此队列;
- shares:分组 CPU 权重,对应用户态
cpu.shares(cgroup v1)/cpu.weight(cgroup v2); - css:cgroup 资源关联句柄,绑定 cgroup 层级,是任务归属分组的标识;
- refcount:引用计数,fork 新增子任务时计数自增,进程退出时递减,用于分组资源生命周期管理。
所有进程默认归属根任务组 root_task_group,手动移入 cgroup 目录后切换分组。
1.2 CFS 组调度分层运行队列
原生 CFS 是单级运行队列,开启 CONFIG_FAIR_GROUP_SCHED 组调度配置后,调度队列变为全局 CPU 运行队列→task_group 分组队列→进程调度实体三级结构。同组所有父子进程挂载在同一分组 CFS 队列,CPU 时间片优先按分组权重分配,再在组内按进程 nice 权重二次分配,fork 继承分组就是保证新进程挂载至父进程所在分组队列。
1.3 task_fork 调用链路
fork 系统调用内核入口:sys_fork→_do_fork→copy_process→sched_fork→调度类task_fork回调,CFS 调度类对应回调函数为task_fork_fair,任务组继承逻辑绝大部分在 sched_fork 与 cpu_cgroup_fork 中完成,是子任务分组绑定的核心执行点。
1.4 cgroup v1/v2 分组规则差异
- cgroup v1:cpu 与 cpuacct 双子系统绑定 task_group,进程写入 tasks 文件即划入分组;
- cgroup v2:统一 cpu 子系统,进程写入 cgroup.procs 完成分组变更; 二者 fork 继承行为完全一致:新建子进程跟随父进程当前分组,不随文件系统挂载位置变动。
1.5 调度实体 sched_entity 分组关联
每个 task_struct 内嵌struct sched_entity se,se->cfs_rq 指向自身所属 task_group 的 CPU 分组队列,fork 继承本质就是复制父进程 se 关联的 task_group 与 cfs_rq 指针。
二、环境准备
2.1 软硬件环境清单
| 环境项 | 参数规格 |
|---|---|
| 操作系统 | Ubuntu22.04 LTS x86_64 |
| 内核版本 | Linux 6.1.30(LTS,源码逻辑通用) |
| 编译依赖 | gcc-11、make、libncurses-dev、bison、flex、libelf-dev |
| 调试工具 | ftrace、trace-cmd、perf、gdb、bpftrace |
| 硬件 | x86_64 4 核 CPU,8G 内存(内核编译 + 压测验证) |
2.2 内核源码下载与编译配置
1、安装编译依赖
sudo apt update -y
sudo apt install build-essential libncurses-dev bison flex libssl-dev libelf-dev trace-cmd -y
2、下载 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
必须开启内核配置项(组调度 + 调试)
CONFIG_FAIR_GROUP_SCHED=y # 开启CFS组调度(核心,关闭则无task_group)
CONFIG_CGROUP_CPU=y # 启用cpu cgroup子系统
CONFIG_DEBUG_KERNEL=y
CONFIG_SCHED_DEBUG=y
CONFIG_FTRACE=y # 函数跟踪,观测fork分组继承函数调用
CONFIG_CGROUP=y
保存退出,编译安装内核:
make -j$(nproc)
sudo make modules_install
sudo make install
sudo update-grub
重启主机,grub 菜单选择新编译内核进入。
2.3 cgroup 文件系统挂载(实操必备)
# cgroup v1挂载,用于后续分组测试
sudo mkdir -p /mnt/cgroup/cpu
sudo mount -t cgroup -o cpu,cpuacct none /mnt/cgroup/cpu
# cgroup v2可选挂载
sudo mkdir -p /mnt/cg2
sudo mount -t cgroup2 none /mnt/cg2
2.4 源码定位路径
kernel/fork.c // do_fork、copy_process主流程
kernel/sched/core.c // sched_fork、cpu_cgroup_fork分组继承核心函数
kernel/sched/fair.c // task_fork_fair CFS调度fork回调
kernel/sched/sched.h // task_group、sched_entity结构体定义
三、应用场景(302 字)
task_fork 分组继承是容器与云原生资源隔离的底层基础,在 K8s 容器场景中,Pod 启动进程被写入对应 cgroup 分组,容器内业务通过 fork 创建的子进程、多线程 pthread 派生的工作线程,依靠默认继承规则自动划入 Pod 专属 task_group,依托分组 cpu.weight 限制整 Pod 的 CPU 最大使用率,避免单个容器内 fork 炸弹耗尽整机算力。在云主机资源配额管控场景,服务商通过 cgroup 绑定租户整机分组,租户所有派生进程自动继承配额,实现 CPU 资源按量分配。工业嵌入式多分区系统中,实时业务与后台日志服务拆分不同 task_group,业务进程 fork 生成的子任务自动归属原分区,保障关键业务的 CPU 权重优先级。若无 fork 继承机制,运维需手动将每一个新建子进程移入分组,大规模集群场景管控成本呈指数上升。
四、实际案例与源码深度剖析
4.1 关键结构体源码(带详细注释,可对照内核)
// kernel/sched/sched.h 核心结构体节选
/* 任务组结构体 */
struct task_group {
#ifdef CONFIG_FAIR_GROUP_SCHED
/* 每个CPU对应的分组CFS运行队列 */
struct cfs_rq **tg_cfs_rq;
/* 分组CPU权重(对应cpu.shares/cpu.weight) */
unsigned long shares;
#endif
/* cgroup资源指针 */
struct css_set *css;
/* 分组引用计数 */
atomic_t refcount;
};
/* CFS调度实体,每个进程独有 */
struct sched_entity {
struct cfs_rq *cfs_rq; // 指向自身所属分组的CFS队列
struct rb_node run_node;
u64 vruntime;
};
/* 进程控制块关键调度字段 */
struct task_struct {
struct sched_entity se;
/* 进程所属task_group快捷指针 */
struct task_group *sched_task_group;
};
代码说明:sched_task_group字段直接标记进程归属分组,fork 继承本质是赋值该指针与 se->cfs_rq,完成分组绑定。
4.2 fork 分组继承内核主流程拆解
整体执行链路: copy_process→sched_fork→cpu_cgroup_fork→sched_change_group(子进程继承父task_group)
4.2.1 sched_fork 函数入口(kernel/sched/core.c)
sched_fork 是 fork 阶段调度初始化入口,调用 cpu_cgroup_fork 完成分组继承:
int sched_fork(unsigned long clone_flags, struct task_struct *p)
{
/* 初始化子进程调度优先级 */
__sched_fork(clone_flags, p);
/* 关键:cgroup-cpu分组继承主函数 */
cpu_cgroup_fork(p);
/* 调用对应调度类task_fork回调,CFS进入task_fork_fair */
if (p->sched_class->task_fork)
p->sched_class->task_fork(p);
return 0;
}
函数作用:fork 创建新任务时,统一完成调度参数与分组归属初始化,cpu_cgroup_fork是实现组继承的核心。
4.2.2 cpu_cgroup_fork 分组继承核心源码
static void cpu_cgroup_fork(struct task_struct *task)
{
struct rq_flags rf;
struct rq *rq;
/* 上锁当前任务运行队列 */
rq = task_rq_lock(task, &rf);
update_rq_clock(rq);
/* 核心函数:子进程跟随父进程current的task_group */
sched_change_group(task, TASK_SET_GROUP);
task_rq_unlock(rq, task, &rf);
}
代码解析:sched_change_group(TASK_SET_GROUP)在 TASK_SET_GROUP 标记下,逻辑固定为子任务继承 current(父进程)的 sched_task_group,不会切换至其他分组;仅用户手动写 cgroup.procs 时才会触发分组迁移。
4.2.3 sched_change_group 继承分支逻辑
截取关键分支:
static int sched_change_group(struct task_struct *tsk, int task_type)
{
struct task_group *tg;
/* TASK_SET_GROUP:fork场景,继承父进程分组 */
if (task_type == TASK_SET_GROUP) {
/* 直接取父进程current的任务组 */
tg = current->sched_task_group;
/* 任务组引用计数+1 */
atomic_inc(&tg->refcount);
/* 赋值子进程分组指针,完成继承 */
tsk->sched_task_group = tg;
/* 将子进程调度实体挂载到分组对应CPU的cfs_rq队列 */
attach_task_cfs_rq(tsk, tg);
return 0;
}
/* 非fork场景:用户手动迁移cgroup,走分组切换逻辑 */
// 省略手动迁移代码
}
核心逻辑总结:fork 创建子进程时,不新建 task_group、不默认归根分组,直接拷贝父进程分组指针 + 递增分组引用,attach_task_cfs_rq把子进程 se 绑定至分组 CFS 运行队列,实现调度层面同组管理。
4.2.4 CFS task_fork_fair 回调补充逻辑
static void task_fork_fair(struct task_struct *p)
{
struct sched_entity *se = &p->se, *curr;
struct rq *rq = task_rq(p);
struct cfs_rq *cfs_rq;
/* 子进程cfs_rq已经在cpu_cgroup_fork完成绑定(继承父分组) */
cfs_rq = se->cfs_rq;
curr = cfs_rq->curr;
/* 继承父进程当前分组队列的min_vruntime,优化子进程调度时机 */
if (curr)
se->vruntime = curr->vruntime;
se->vruntime -= cfs_rq->min_vruntime;
place_entity(cfs_rq, se, 1);
}
说明:task_fork_fair 不再处理分组归属,仅做调度实体 vruntime 初始化,分组绑定已在上游 cpu_cgroup_fork 完成。
4.3 用户态实操 1:验证 fork 默认继承父进程 cgroup 分组
1、编写测试代码 fork_test.c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
// 获取进程当前归属cgroup路径
void get_cgroup_path(pid_t pid)
{
char path[128];
FILE *fp;
char buf[256];
snprintf(path, sizeof(path), "/proc/%d/cgroup", pid);
fp = fopen(path, "r");
if (!fp) return;
while(fgets(buf, sizeof(buf), fp)){
// 筛选cpu子系统分组
if(strstr(buf, "cpu:")){
printf("PID:%d cpu cgroup: %s",pid,buf);
}
}
fclose(fp);
}
int main(void)
{
pid_t pid;
printf("父进程PID=%d\n",getpid());
get_cgroup_path(getpid());
pid = fork();
if(pid == 0){
// 子进程
sleep(1);
printf("\n子进程PID=%d\n",getpid());
get_cgroup_path(getpid());
exit(0);
}else if(pid >0){
wait(NULL);
}
return 0;
}
2、编译 + 创建自定义 cgroup 分组,把父进程移入分组后运行
# 编译
gcc fork_test.c -o fork_test
# 创建自定义分组cg1
mkdir /mnt/cgroup/cpu/cg1
# 获取当前shell PID,移入cg1
echo $$ > /mnt/cgroup/cpu/cg1/tasks
# 运行程序,父子进程应同属cg1分组
./fork_test
预期输出:父、子进程 cpu cgroup 路径一致,都在/mnt/cgroup/cpu/cg1,验证 fork 默认继承分组。
4.4 用户态实操 2:子进程 fork 后手动迁移分组
# 沿用上面程序,后台运行
./fork_test &
# 查到子进程PID后,移入新建cg2分组
mkdir /mnt/cgroup/cpu/cg2
echo 子PID > /mnt/cgroup/cpu/cg2/tasks
# 再次查看/proc/子PID/cgroup,分组变更,父进程仍在cg1
实操结论:fork 仅创建瞬间继承分组,子进程生命周期内可独立迁移至其他 task_group,父子分组后期无绑定关系。
4.5 ftrace 跟踪内核 fork 分组继承函数调用
# 挂载debugfs
sudo mount -t debugfs none /sys/kernel/debug
# 清空trace缓存
echo > /sys/kernel/debug/tracing/trace
# 筛选跟踪关键函数
echo cpu_cgroup_fork >> /sys/kernel/debug/tracing/set_ftrace_filter
echo sched_change_group >> /sys/kernel/debug/tracing/set_ftrace_filter
echo task_fork_fair >> /sys/kernel/debug/tracing/set_ftrace_filter
# 开启函数跟踪
echo function > /sys/kernel/debug/tracing/current_tracer
echo 1 > /sys/kernel/debug/tracing/tracing_on
# 新开终端执行测试程序
./fork_test
# 关闭跟踪
echo 0 > /sys/kernel/debug/tracing/tracing_on
# 查看调用栈,确认fork必触发cpu_cgroup_fork→sched_change_group继承分组
cat /sys/kernel/debug/tracing/trace
从跟踪日志可直观验证:每次 fork 生成子进程,内核固定调用cpu_cgroup_fork完成分组继承。
五、常见问题与解答
Q1:关闭 CONFIG_FAIR_GROUP_SCHED 后,task_group 与 fork 继承机制还存在吗?
答:配置关闭后内核不编译 task_group 相关代码,所有进程统一在全局根 CFS 队列调度,无分组概念,cgroup-cpu 失效,fork 不再做任何分组继承逻辑,生产容器环境必须开启该配置。
Q2:使用 pthread_create 创建线程(CLONE_THREAD),线程是否继承主线程 task_group?
答:完全继承。pthread 底层调用 clone 带 CLONE_THREAD 标识,最终仍走 do_fork→sched_fork→cpu_cgroup_fork 链路,TASK_SET_GROUP 分支生效,新线程和主线程同属一个 task_group,是容器内多线程资源统一管控的关键。
Q3:父进程被手动迁移 cgroup 后,已经 fork 出的子进程分组会不会同步变更?
答:不会。分组迁移仅修改当前操作进程的sched_task_group指针,已创建的子进程 task_group 引用不变;只有后续新 fork 的子进程跟随父进程最新分组,存量进程分组保留创建时的归属。
Q4:fork 继承分组时 task_group->refcount 为何自增,进程退出何时递减?
答:refcount 用来保护 task_group 内存不被提前释放,fork 子进程atomic_inc(&tg->refcount);进程调用 exit 退出、内核释放 task_struct 时,在 cpu_cgroup_exit 中执行 refcount 递减,计数归 0 才允许销毁 task_group。
Q5:部分业务 fork 出的进程不在父进程 cgroup 内,排查方向是什么?
答:1. 检查程序内部是否 fork 后执行 setns、写入 cgroup.procs 手动迁移分组;2. 排查 systemd、容器运行时 (runc/containerd) 钩子脚本自动迁移进程;3. ftrace 跟踪 sched_change_group,确认是否触发非 TASK_SET_GROUP 的分组切换分支。
六、实践建议与最佳实践
6.1 内核调试最佳实践
- 排查分组继承异常优先使用 ftrace 跟踪
cpu_cgroup_fork、sched_change_group,区分是 fork 原生继承异常还是用户态主动迁移 cgroup; - 内核定制开发时,禁止直接修改
sched_change_group(TASK_SET_GROUP)分支逻辑,改动会破坏全系统 fork 继承规则,如需自定义分组策略在 attach_task_cfs_rq 后二次修改。
6.2 容器与业务开发优化
- 容器启动时优先在容器入口进程设置 cgroup,后续业务 fork 的所有子进程自动入组,避免业务代码中手动控制进程分组;
- 高频 fork 的服务(日志、脚本)不要频繁迁移进程 cgroup,每次迁移触发分组队列重挂载,产生少量调度开销,利用 fork 默认继承统一初始化分组。
6.3 cgroup 资源管控优化
- cgroup v2 环境下依托 fork 继承特性,仅将容器主进程写入 cgroup.procs 即可,子进程自动纳入,省去批量写入子 PID 的运维脚本;
- 限制 fork 炸弹场景:通过 cgroup pids 子系统限制分组最大进程数,依靠 fork 继承让所有派生进程受统一 pid 配额管控。
6.4 内核编译与测试规范
自研调度模块测试时,保留 CONFIG_FAIR_GROUP_SCHED=y,测试 fork 继承逻辑先用本文 fork_test 程序快速验证分组归属,再做定制调度逻辑开发。
七、总结与应用延伸
全文从任务组数据结构、fork 内核调用链路、cpu_cgroup_fork继承源码、用户态实操验证、故障排查五个维度完整拆解 task_fork 子任务分组继承机制,核心本质:fork 新建任务在 sched_fork 阶段通过 TASK_SET_GROUP 分支,直接复用父进程 task_group 指针,完成分组归属与 CFS 队列绑定,这套默认继承规则是 Linux cgroup 资源隔离、CFS 分层组调度的设计基石。
从落地场景来看,该机制支撑 Docker/K8s 容器 CPU 配额隔离、云服务器租户资源划分、嵌入式多业务分区调度;从内核研发与学术角度,掌握 fork 分组继承逻辑,能够深入理解 cgroup 与调度器的耦合设计、分层 CFS 队列落地原理,可用于调度论文撰写、自研调度分组插件开发、线上容器 CPU 限流故障定位。
建议读者基于文中源码与 shell 命令,自行修改sched_change_group继承分支做定制实验(例如 fork 子进程默认归入指定分组),通过 ftrace+proc 文件系统观察分组变化,从实操层面吃透组调度 fork 继承底层逻辑,落地到容器底层优化、嵌入式调度裁剪项目中。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)