简介在 Linux 内核 CFS 组调度框架下基于cgroup/cpu子系统实现任务分组 CPU 资源隔离是容器、云主机、服务器业务资源配额管控的底层基石而switched_from、switched_to作为调度类预留的回调钩子是任务从旧任务组迁出、迁入新任务组时内核完成调度实体、运行队列、负载统计平滑变更的核心入口。从业务落地视角K8s 容器资源配额调整、虚拟机 CPU 限流、业务进程分组资源隔离本质都是用户态修改 cgroup.procs 触发任务跨任务组迁移内核最终经由sched_move_task→task_change_group调用两个回调函数完成组上下文切换。在未合理维护switched_from/switched_to逻辑的定制内核中极易出现组负载统计错乱、CPU 配额失效、调度周期时间片计算异常、同组任务抢占紊乱等线上故障也是云厂商容器调度故障高频排查点。对于内核研发、嵌入式实时工程师、云计算底层开发人员吃透两个回调的调用时机、入参含义、组负载修正逻辑是定位 cgroup 限流不准、调度抖动、组负载均衡异常的必备能力同时也是基于 BPF、自定义调度策略二次开发组调度扩展的理论基础。本文从概念、环境搭建、源码拆解、实操复现、故障排查全链路落地全部代码可编译复现支撑学术论文数据调研与工程项目落地验证。一、核心概念与术语解析1.1 任务组 task_group 与组调度基础Linux 开启CONFIG_FAIR_GROUP_SCHED后启用 CFS 组调度struct task_group对应一个cpu cgroup分组内核为每个 CPU 维护独立的cfs_rq组运行队列root_task_group系统根任务组所有未加入自定义 cgroup 的进程默认归属该分组group se组调度实体每个 task_group 在单 CPU 上对应一个组调度实体struct sched_entity代表整个分组参与 CPU 时间片分配分组内所有任务调度实体依附于该组 se 向上逐级参与权重分配task se任务调度实体单个进程对应的sched_entity挂靠在所属 task_group 的 cfs_rq 红黑树中cfs_rq分组在单 CPU 的就绪任务运行队列维护分组总权重、负载、运行计数、周期统计数据是switched_from/switched_to修改的核心对象。1.2 switched_from /switched_to 回调定义与作用两个回调挂载在sched_class调度类结构体中CFS 调度类sched_fair_class实现了对应钩子RT/DL 调度器也各自实现实时分组场景下的回调逻辑struct sched_class { void (*switched_from)(struct rq *rq, struct task_struct *p); void (*switched_to)(struct rq *rq, struct task_struct *p); /* 其余调度类函数enqueue/dequeue/pick_next等省略 */ }switched_from(rq, p)调用时机任务即将离开原有旧任务组前核心职责从旧分组各级cfs_rq逐层移除任务调度实体逐级递减分组负载、总运行权重修正旧分组 cfs_rq 的运行统计、周期剩余时间、队列 nr_running 计数清理任务在旧组的调度缓存、运行时标记避免旧组负载残留。switched_to(rq, p)调用时机任务绑定新任务组后、正式入队就绪队列前核心职责将任务 se 挂载至新 task_group 对应 CPU 的 cfs_rq自底向上逐级更新新分组各级 cfs_rq 总权重、负载数据刷新新组时间片分配基准保证新加入任务可以按照新分组权重参与 CPU 资源分配。1.3 任务跨组切换触发链路用户态把 PID 写入 cgroup.procs → cpu_cgroup_attach → sched_move_task → sched_change_group → 旧组调用 switched_from → 更新 task 的 task_group 指针 → 新组调用 switched_to → 任务重新入队新分组运行队列。1.4 关键配套术语shares分组权重cgroup.cpu.shares决定分组间 CPU 分配占比任务切换分组后自动按新组 shares 重算权重group_load分组累计负载值switched_from/to会实时增减该值影响调度器负载均衡决策hierarchy 层级cgroup 支持父子嵌套分组切换时需从任务 se 向上遍历所有父 task_group 逐级修正队列数据。二、环境准备2.1 软硬件环境清单项目版本参数操作系统Ubuntu22.04 LTS x86_64内核源码Linux 6.1.63 LTS稳定长期版switched 回调逻辑无大幅改动CPUx86_64 4 核及以上推荐 8 核 16G 内存方便多进程压测观测负载变化编译依赖gcc-11、make、bison、flex、libncurses-dev、libelf-dev调试工具trace-cmd、ftrace、perf、gdb、cgroup-tools2.2 内核编译配置必须开启分组调度与调试选项# 安装编译依赖 sudo apt update sudo apt install build-essential libncurses-dev bison flex libssl-dev libelf-dev cgroup-tools # 下载6.1内核源码 wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.1.tar.xz tar xvf linux-6.1.tar.xz cd linux-6.1 # 复用当前系统内核配置 cp /boot/config-$(uname -r) .config make menuconfigmenuconfig 勾选核心配置项CONFIG_CGROUPSy CONFIG_CGROUP_SCHEDy CONFIG_FAIR_GROUP_SCHEDy # CFS组调度开关必须开启才会生效switched回调 CONFIG_DEBUG_KERNELy CONFIG_SCHED_DEBUGy CONFIG_FTRACEy # 用于跟踪switched_from/switched_to函数调用 CONFIG_CPU_FREQn # 关闭调频避免干扰负载测试编译安装内核make -j$(nproc) sudo make modules_install sudo make install sudo update-grub # 重启系统进入新编译内核 sudo reboot2.3 cgroup 挂载配置# 临时挂载cpu子系统cgroup永久配置写入/etc/fstab sudo mkdir -p /sys/fs/cgroup/cpu sudo mount -t cgroup -o cpu none /sys/fs/cgroup/cpu # 建立两个测试分组groupA、groupB用于后续任务跨组迁移实验 sudo mkdir /sys/fs/cgroup/cpu/groupA /sys/fs/cgroup/cpu/groupB # 设置分组权重groupA2048groupB1024权重比2:1 echo 2048 | sudo tee /sys/fs/cgroup/cpu/groupA/cpu.shares echo 1024 | sudo tee /sys/fs/cgroup/cpu/groupB/cpu.shares2.4 源码路径定位kernel/sched/fair.c // CFS实现switched_from/switched_to完整源码 kernel/sched/sched.h // sched_class、task_group、sched_entity结构体定义 kernel/sched/core.c // sched_move_task、sched_change_group上层调用逻辑三、应用场景302 字switched_from/switched_to 是容器云资源管控的底层支撑在 K8s 集群节点资源调度场景中运维通过修改 Pod 归属 namespace 对应的 cgroup 实现业务进程分组迁移当业务从低配资源分组扩容至高配分组时kubelet 修改 cgroup.procs 触发进程跨组内核通过两个回调完成旧分组负载剥离、新分组资源接入自动按新分组 cpu.shares 重新分配 CPU 占比。在服务器机房业务隔离场景数据库进程、web 服务进程分别归入不同 cgroup夜间批量任务临时迁入闲置分组回调保证原分组负载精准回落避免批量任务抢占在线业务算力。嵌入式工控多任务分组调度中控制逻辑进程与日志进程分组隔离动态迁移进程时依靠回调平滑修正分组运行队列保障硬实时业务时间片不受迁移扰动杜绝因负载统计异常引发调度卡顿。四、实际案例与步骤、代码示例4.1 内核源码CFS 调度类 switched_from 源码解析附带详细注释文件kernel/sched/fair.c/* 任务离开旧分组时调用switched_from回调实现 */ static void switched_from_fair(struct rq *rq, struct task_struct *p) { struct sched_entity *se p-se; struct cfs_rq *cfs_rq; /* 关闭抢占防止修改运行队列过程被调度打断 */ lockdep_assert_held(rq-lock); /* 1、逐层向上遍历所有父分组cfs_rq从各级队列剥离调度实体 */ for_each_cfs_rq_hierarchy(cfs_rq, se) { /* 若任务当前在就绪队列执行出队操作递减分组运行计数 */ if (se-on_rq) { dequeue_entity(cfs_rq, se, DEQUEUE_SAVE); cfs_rq-nr_running--; } /* 递减分组累计负载修正组负载统计核心旧分组负载剔除当前任务 */ sub_cfs_load(cfs_rq, se_load(se)); /* 修正分组有效权重 */ cfs_rq-tg_load - se-load.weight; } /* 重置任务调度实体分组缓存标记 */ se-my_q NULL; }代码使用场景用户把进程 PID 从 groupA/cgroup.procs 移出瞬间触发逐层剥离任务在 groupA 及其父分组的 cfs_rq 队列数据削减旧分组负载与权重若缺失此逻辑旧分组负载持续偏高调度器仍会按包含该任务的权重分配 CPU造成配额错乱。4.2 switched_to_fair 新分组接入源码/* 任务迁入新分组回调switched_to实现 */ static void switched_to_fair(struct rq *rq, struct task_struct *p) { struct sched_entity *se p-se; struct cfs_rq *cfs_rq; lockdep_assert_held(rq-lock); /* 1、逐层向上遍历新分组全层级cfs_rq加入调度实体 */ for_each_cfs_rq_hierarchy(cfs_rq, se) { /* 任务处于就绪态则入队新分组红黑树递增nr_running */ if (se-on_rq) { enqueue_entity(cfs_rq, se, ENQUEUE_SAVE); cfs_rq-nr_running; } /* 新分组累加任务负载、权重更新分组资源基准 */ add_cfs_load(cfs_rq, se_load(se)); cfs_rq-tg_load se-load.weight; } /* 绑定se与新分组cfs_rq关联指针 */ se-my_q task_cfs_rq(p); /* 重新计算任务在新分组的虚拟运行时间适配新组调度周期 */ update_cfs_group(se); } /* CFS调度类挂载两个回调函数 */ const struct sched_class sched_fair_class { .switched_from switched_from_fair, .switched_to switched_to_fair, /* enqueue/dequeue/pick_next_task等成员省略 */ };代码说明task_group 指针修改完成后立即执行从任务所在分组向上遍历所有父 cgroup 分组逐级更新队列、负载、权重update_cfs_group函数重新基于新分组 shares 换算任务时间片实现资源平滑切换。4.3 上层触发函数 sched_change_group 核心逻辑core.c// kernel/sched/core.c static void sched_change_group(struct task_struct *tsk, int task_on_rq) { struct rq_flags rf; struct rq *rq task_rq(tsk); rq_lock(rq, rf); /* 第一步离开旧任务组调用旧调度类switched_from */ if (task_on_rq) tsk-sched_class-switched_from(rq, tsk); /* 修改task的task_group指针切换分组绑定关系 */ set_task_group(tsk); /* 第二步接入新任务组调用新调度类switched_to */ if (task_on_rq) tsk-sched_class-switched_to(rq, tsk); rq_unlock(rq, rf); }调用链路总结用户态写入 cgroup.procs → cpu_cgroup_attach → sched_move_task → sched_change_group → switched_from → set_task_group → switched_to。4.4 用户态测试程序生成持续 CPU 压测进程用于跨组迁移实验文件名cpu_worker.c代码可直接复制编译#include stdio.h #include unistd.h #include stdlib.h /* 死循环消耗CPU持续占用单核算力 */ int main(void) { printf(Worker PID %d\n, getpid()); while(1) { /* 空循环占满CPU */ } return 0; }编译运行命令gcc cpu_worker.c -o cpu_worker ./cpu_worker # 记录打印出的PID后续用于移入groupA再迁移至groupB4.5 实操步骤 1进程加入 groupA# 替换下方PID为程序运行输出的PID echo 1234 | sudo tee /sys/fs/cgroup/cpu/groupA/cgroup.procs # 查看当前进程归属分组 cat /proc/1234/cgroup4.6 实操步骤 2ftrace 跟踪 switched_from/switched_to 调用关键复现代码新开终端执行跟踪脚本可直接全量复制# 挂载debugfs sudo mount -t debugfs none /sys/kernel/debug # 清空跟踪缓存 echo /sys/kernel/debug/tracing/trace # 配置需要跟踪的两个回调函数 echo switched_from_fair /sys/kernel/debug/tracing/set_ftrace_filter echo switched_to_fair /sys/kernel/debug/tracing/set_ftrace_filter # 开启函数跟踪 echo function /sys/kernel/debug/tracing/current_tracer echo 1 /sys/kernel/debug/tracing/tracing_on # 另一个终端执行进程从groupA迁移至groupB触发回调 echo 1234 | sudo tee /sys/fs/cgroup/cpu/groupB/cgroup.procs # 停止跟踪 echo 0 /sys/kernel/debug/tracing/tracing_on # 查看跟踪日志可看到两次回调调用记录 cat /sys/kernel/debug/tracing/trace日志解读日志会先打印switched_from_fair旧组 groupA 卸载再打印switched_to_fair新组 groupB 加载验证内核调用顺序和源码逻辑完全一致。4.7 实操步骤 3perf 观测分组 CPU 占用变化# 持续监控CPU占用迁移前后对比占比groupA权重2048、groupB1024理论CPU占比2:1 sudo perf top -p 1234进程在 groupA 时 CPU 分配占比约 66%迁入 groupB 后自动降至 33%正是switched_from/to修正分组权重后调度器重新分配资源的直观结果。五、常见问题与解答Q1进程写入 cgroup.procs 后CPU 配额不生效、分组 shares 不生效是什么原因答优先排查内核是否开启CONFIG_FAIR_GROUP_SCHED未开启时 switched_from/switched_to 不会挂载执行任务跨组不会修正负载与权重其次通过 ftrace 查看两次回调是否被调用若日志无回调记录说明sched_change_group未执行大概率 cgroup 挂载异常、cpu 子系统未正确挂载。Q2任务正在 CPU 上运行on_rq1与休眠阻塞on_rq0切换分组回调逻辑有区别答on_rq1 就绪运行态switched_from 执行 dequeue 出旧队列switched_to 执行 enqueue 入新队列on_rq0 休眠态仅修改各级 cfs_rq 的负载、tg_load 数值不执行入队出队任务唤醒时直接在新分组队列入队源码中通过se-on_rq分支区分处理。Q3嵌套多级 cgroup父 group→子 group任务从子分组迁移switched_from 会遍历所有父分组吗答for_each_cfs_rq_hierarchy宏会从任务当前 se 向上递归遍历全层级父 task_group 对应的 cfs_rq逐级修改负载这也是 cgroup 层级资源分配的关键若缺失上层遍历父分组负载统计失真、上层配额异常。Q4修改 task_group 指针为什么放在 switched_from 和 switched_to 中间答先从旧分组卸载switched_from此时 task 还绑定旧 tg→修改指针→新分组加载switched_totask 绑定新 tg顺序不能调换若指针提前修改switched_from 无法找到旧分组 cfs_rq旧组负载无法剔除引发统计 BUG。Q5DL/RT 实时调度任务跨 cgroup 分组也会触发 switched_from/to 吗答开启CONFIG_RT_GROUP_SCHED/CONFIG_DL_GROUP_SCHED后RT/DL 调度类各自实现专属回调逻辑和 CFS 类似旧 rt_rq/dl_rq 卸载、新分组运行队列挂载实时带宽配额随分组切换同步变更。六、实践建议与最佳实践6.1 内核调试排错规范cgroup 配额异常排查顺序ftrace 跟踪 switched_from_fair/switched_to_fair 是否调用 → 查看 rq 锁是否正常持有日志出现 lock 异常代表队列修改被打断→ 对比迁移前后 cfs_rq-tg_load 数值变化定位负载修正异常点定制内核二次开发时禁止删除、绕过 switched_from/switched_to 回调如需扩展分组规则在回调内部追加逻辑不要替换原有出队入队流程。6.2 线上业务 cgroup 运维优化避免高频动态迁移进程跨分组每次迁移触发两次回调 多级 cfs_rq 遍历海量进程频繁切换分组会产生内核调度开销容器批量资源变更尽量批量写入 cgroup.procs高并发容器集群同业务进程统一归入同一 cgroup减少零散跨组迁移次数降低内核队列修改开销。6.3 性能调优技巧CPU 绑核 分组隔离将整个 cgroup 分组绑定固定 CPU任务跨组仅修改单个 CPU 的 cfs_rq避免多核层级队列遍历带来的性能损耗关闭不必要 cgroup 层级扁平化分组结构减少for_each_cfs_rq_hierarchy遍历层数缩短 switched 回调执行耗时。6.4 内核修改避坑自定义调度策略扩展时若新增分组资源限制逻辑必须在 switched_from 中回收旧分组限制资源、switched_to 中初始化新分组资源否则会出现资源泄漏、配额超限。七、总结与落地应用延伸全文围绕switched_from/switched_to从概念定义、内核源码实现、用户态实操复现、故障排查、工程优化完整拆解了任务跨任务组切换的全链路逻辑两个回调是 Linux 组调度资源平滑迁移的核心枢纽switched_from 负责旧分组资源、负载、队列数据的清理回收switched_to 负责新分组资源接入与调度参数初始化中间修改 task_group 指针完成分组归属切换三者有序配合实现进程跨 cgroup 分组时 CPU 资源配额无抖动切换。从工程落地层面该机制是 Docker、K8s 容器 CPU 资源限制、云主机租户资源隔离、嵌入式工控多业务分组限流的底层实现从学术研究角度源码中分层遍历 cfs_rq、负载增减、调度实体入出队逻辑是撰写 Linux 组调度论文、EDF 扩展调度研究的关键素材依托本文测试代码可完成定量负载测试与数据采集。建议读者自行修改内核源码在 switched_from/to 中增加自定义打印重新编译内核观测分组负载变化或基于 BPF hook 两个回调函数做线上容器资源迁移监控把理论知识落地到真实服务器、嵌入式项目开发中。