Linux 组调度的 switched_from/switched_to:任务组切换处理
简介
在 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 回调逻辑无大幅改动) |
| CPU | x86_64 4 核及以上,推荐 8 核 16G 内存(方便多进程压测观测负载变化) |
| 编译依赖 | gcc-11、make、bison、flex、libncurses-dev、libelf-dev |
| 调试工具 | trace-cmd、ftrace、perf、gdb、cgroup-tools |
2.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_CGROUPS=y CONFIG_CGROUP_SCHED=y CONFIG_FAIR_GROUP_SCHED=y # CFS组调度开关(必须开启才会生效switched回调) CONFIG_DEBUG_KERNEL=y CONFIG_SCHED_DEBUG=y CONFIG_FTRACE=y # 用于跟踪switched_from/switched_to函数调用 CONFIG_CPU_FREQ=n # 关闭调频避免干扰负载测试编译安装内核:
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 # 设置分组权重:groupA=2048,groupB=1024,权重比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 实操步骤 2:ftrace 跟踪 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 实操步骤 3:perf 观测分组 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_rq=1)与休眠阻塞(on_rq=0),切换分组回调逻辑有区别?
答:on_rq=1 就绪运行态:switched_from 执行 dequeue 出旧队列,switched_to 执行 enqueue 入新队列;on_rq=0 休眠态:仅修改各级 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_to,task 绑定新 tg),顺序不能调换;若指针提前修改,switched_from 无法找到旧分组 cfs_rq,旧组负载无法剔除,引发统计 BUG。
Q5:DL/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 两个回调函数做线上容器资源迁移监控,把理论知识落地到真实服务器、嵌入式项目开发中。
