Linux 组调度的 task_fork:子任务的组归属继承
2026/6/4 5:29:57 网站建设 项目流程

简介

在 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.hkernel/sched/fair.c。关键组成:

  1. tg_cfs_rq:每个 CPU 绑定一个分组专属 CFS 运行队列,实现分层调度,分组 CPU 配额、权重全部挂载在此队列;
  2. shares:分组 CPU 权重,对应用户态cpu.shares(cgroup v1)/cpu.weight(cgroup v2);
  3. css:cgroup 资源关联句柄,绑定 cgroup 层级,是任务归属分组的标识;
  4. 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 内核调试最佳实践

  1. 排查分组继承异常优先使用 ftrace 跟踪cpu_cgroup_fork、sched_change_group,区分是 fork 原生继承异常还是用户态主动迁移 cgroup;
  2. 内核定制开发时,禁止直接修改sched_change_group(TASK_SET_GROUP)分支逻辑,改动会破坏全系统 fork 继承规则,如需自定义分组策略在 attach_task_cfs_rq 后二次修改。

6.2 容器与业务开发优化

  1. 容器启动时优先在容器入口进程设置 cgroup,后续业务 fork 的所有子进程自动入组,避免业务代码中手动控制进程分组;
  2. 高频 fork 的服务(日志、脚本)不要频繁迁移进程 cgroup,每次迁移触发分组队列重挂载,产生少量调度开销,利用 fork 默认继承统一初始化分组。

6.3 cgroup 资源管控优化

  1. cgroup v2 环境下依托 fork 继承特性,仅将容器主进程写入 cgroup.procs 即可,子进程自动纳入,省去批量写入子 PID 的运维脚本;
  2. 限制 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 继承底层逻辑,落地到容器底层优化、嵌入式调度裁剪项目中。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询