一、前言在多核 Linux 系统中任务迁移和负载均衡是调度器的核心职责之一。当任务因负载均衡、CPU 热插拔或其他被动原因被迁移到新的 CPU 上时调度器需要以一种公平的方式重新安置这些任务避免它们因 vruntime虚拟运行时间的差异而获得不当的调度优势或劣势。本文深入剖析 CFSCompletely Fair Scheduler调度器中的被动唤醒wakeup_passive机制重点讲解place_entity()函数如何调整被迁移任务的 vruntime确保调度公平性。阅读提示本文基于 Linux 6.x 内核源码进行分析涉及kernel/sched/fair.c中的核心调度逻辑。建议读者配合内核源码阅读以获得最佳理解效果。二、核心概念解析2.1 什么是被动唤醒在 CFS 调度器中任务进入运行队列enqueue的场景主要分为两类场景类型触发原因标志位主动唤醒任务从睡眠状态被唤醒如 I/O 完成、信号到达ENQUEUE_WAKEUP被动唤醒/迁移负载均衡、CPU 热插拔、fork 后的任务放置ENQUEUE_MIGRATED被动唤醒特指那些并非由任务自身主动请求而是被调度器被动迁移到其他 CPU 的场景。这类任务往往已经在某个 CPU 上运行过一段时间其 vruntime 是基于原 CPU 的min_vruntime计算的。如果直接将其插入新 CPU 的运行队列而不做任何调整就会导致严重的公平性问题。2.2 vruntime 与 min_vruntimevruntime虚拟运行时间是 CFS 调度器的核心度量指标它根据任务的 nice 值权重对实际运行时间进行加权计算Δvruntime Δdelta_exec × NICE_0_LOAD / curr-load.weightmin_vruntime是 CFS 运行队列cfs_rq中所有任务的最小 vruntime 值它单调递增代表了该 CPU 上最慢的虚拟时钟。每个 CPU 都有自己的min_vruntime不同 CPU 之间可能存在显著差异。2.3 为什么需要 vruntime 归一化假设 CPU A 的min_vruntime为 1000CPU B 的min_vruntime为 5000。如果一个 vruntime1200 的任务从 CPU A 迁移到 CPU B不做调整该任务在新队列中 vruntime1200远小于 CPU B 的 min_vruntime5000它将长期霸占 CPU导致其他任务饥饿。做归一化通过调整使任务的 vruntime 相对于新队列的 min_vruntime 保持合理位置维护公平性。CFS 采用归一化Normalization机制解决此问题任务出队时减去原队列的min_vruntime入队时加上新队列的min_vruntime。三、环境准备3.1 硬件与软件环境项目要求操作系统Linux 5.10推荐 6.1 LTS 或 6.6架构x86_64 或 ARM64多核系统更佳内核源码建议下载与运行版本匹配的源码调试工具perf,ftrace,trace-cmd,bpftool编译依赖build-essential,libncurses-dev,bc3.2 获取内核源码# 下载与当前运行版本匹配的源码 uname -r wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.6.tar.xz tar -xf linux-6.6.tar.xz cd linux-6.6 # 或者使用 git 获取最新源码 git clone https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git cd linux3.3 关键源文件定位# CFS 核心实现 kernel/sched/fair.c # 主要调度逻辑 kernel/sched/core.c # 核心调度器 kernel/sched/sched.h # 数据结构定义 # 重点关注函数 grep -n place_entity kernel/sched/fair.c grep -n ENQUEUE_MIGRATED kernel/sched/sched.h grep -n ENQUEUE_WAKEUP kernel/sched/sched.h四、应用场景分析4.1 典型被动唤醒场景在实际生产环境中被动唤醒主要发生在以下场景场景一周期性负载均衡Periodic Load Balance当系统某个 CPU 负载过重时load_balance()函数会被触发从最繁忙的 CPU 拉取任务到当前 CPU。这是最常见的被动迁移场景发生在scheduler_tick触发的软中断上下文中。场景二新空闲负载均衡New Idle Balance当 CPU 即将进入空闲状态时会执行newidle_balance()尝试从其他 CPU 拉取任务。这种场景对延迟敏感要求快速决策。场景三NOHZ 空闲均衡在 tickless 模式下空闲 CPU 的时钟中断被关闭。当 busy CPU 检测到负载不均时会通过 IPI核间中断唤醒 idle CPU 执行均衡。这种场景涉及跨 CPU 的被动迁移。场景四任务 fork/exec 后的放置新创建的任务或执行exec()后的任务可能被调度器选择到其他 CPU 上运行。select_task_rq_fair()会根据负载和亲和性选择目标 CPU。4.2 被动唤醒 vs 主动唤醒的区别特性主动唤醒ENQUEUE_WAKEUP被动唤醒ENQUEUE_MIGRATED触发源任务自身状态变化睡眠→就绪调度器决策负载均衡vruntime 处理调用place_entity()进行补偿进行归一化不额外补偿抢占行为可能触发唤醒抢占通常不触发立即抢占exec_start 处理保留原值通常重置为 0表示非热任务五、源码深度解析被动唤醒的处理流程5.1 入口函数enqueue_entity()当任务被迁移到新 CPU 时最终会调用enqueue_entity()将其插入 CFS 运行队列。这是处理被动唤醒的核心函数// kernel/sched/fair.c static void enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags) { /* * 关键判断是否需要进行 vruntime 归一化 * 条件非唤醒场景或者是迁移场景 */ bool renorm !(flags ENQUEUE_WAKEUP) || (flags ENQUEUE_MIGRATED); bool curr cfs_rq-curr se; /* * 如果当前任务就是正在入队的任务罕见场景 * 需要在 update_curr() 之前重新归一化 */ if (renorm curr) se-vruntime cfs_rq-min_vruntime; /* 更新当前任务的运行统计同时推进 min_vruntime */ update_curr(cfs_rq); /* * 对于非当前任务的入队操作在 update_curr 之后归一化 * 这样确保任务被放置在当前时间点而不是过去的某个随机时刻 */ if (renorm !curr) se-vruntime cfs_rq-min_vruntime; /* 更新负载统计、组调度权重等 */ update_load_avg(cfs_rq, se, UPDATE_TG | DO_ATTACH); update_cfs_group(se); enqueue_runnable_load_avg(cfs_rq, se); account_entity_enqueue(cfs_rq, se); /* * 主动唤醒场景调用 place_entity 进行补偿 * 被动迁移场景跳过此步骤仅完成归一化即可 */ if (flags ENQUEUE_WAKEUP) place_entity(cfs_rq, se, 0); /* 迁移标志处理重置 exec_start表示该任务不再是热任务 */ if (flags ENQUEUE_MIGRATED) se-exec_start 0; /* 将任务插入红黑树如果不是当前任务 */ if (!curr) __enqueue_entity(cfs_rq, se); se-on_rq 1; }5.2 归一化逻辑详解归一化的核心在于renorm变量的判断逻辑bool renorm !(flags ENQUEUE_WAKEUP) || (flags ENQUEUE_MIGRATED);这个逻辑可以拆解为flags 组合renorm 结果说明ENQUEUE_WAKEUPfalse主动唤醒不进行归一化ENQUEUE_WAKEUP | ENQUEUE_MIGRATEDtrue唤醒且迁移需要归一化无标志新任务 forktrue新任务需要归一化ENQUEUE_MIGRATED单独true纯迁移需要归一化为什么要归一化看这段注释/* * MIGRATION * * dequeue * update_curr() * update_min_vruntime() * vruntime - min_vruntime * * enqueue * update_curr() * update_min_vruntime() * vruntime min_vruntime * * this way the vruntime transition between RQs is done when both * min_vruntime are up-to-date. */5.3 出队时的归一化dequeue_entity与入队对应任务出队时也需要进行反向归一化static void dequeue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags) { update_curr(cfs_rq); /* * 非睡眠场景即迁移场景需要减去 min_vruntime * 睡眠任务保留原始 vruntime用于后续补偿计算 */ if (!(flags DEQUEUE_SLEEP)) se-vruntime - cfs_rq-min_vruntime; /* ... 其余处理 ... */ }5.4 唤醒时的补偿逻辑place_entity虽然被动唤醒不进行补偿但了解主动唤醒的补偿机制有助于理解两者的差异static void place_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int initial) { u64 vruntime cfs_rq-min_vruntime; /* 新任务惩罚增加一个时间片的 vruntime */ if (initial sched_feat(START_DEBIT)) vruntime sched_vslice(cfs_rq, se); /* 唤醒任务补偿减去调度周期的一半或全部 */ if (!initial) { unsigned long thresh sysctl_sched_latency; /* GENTLE_FAIR_SLEEPERS 特性开启时补偿减半 */ if (sched_feat(GENTLE_FAIR_SLEEPERS)) thresh 1; vruntime - thresh; /* 奖励vruntime 变小更容易被调度 */ } /* 防止 vruntime 倒退确保公平性 */ se-vruntime max_vruntime(se-vruntime, vruntime); }关键区别被动迁移任务不会调用place_entity()因此不会获得额外的 vruntime 补偿。它们仅通过归一化保持相对位置。5.5 远程唤醒的特殊处理migrate_task_rq_fair当任务在唤醒过程中被迁移到远程 CPU 时发生在try_to_wake_up路径有一个特殊的预处理static void migrate_task_rq_fair(struct task_struct *p, int new_cpu) { if (READ_ONCE(p-__state) TASK_WAKING) { struct sched_entity *se p-se; struct cfs_rq *cfs_rq cfs_rq_of(se); u64 min_vruntime; /* 读取原 CPU 的 min_vruntime */ min_vruntime cfs_rq-min_vruntime; /* 提前减去原队列的 min_vruntime为后续 enqueue 做准备 */ se-vruntime - min_vruntime; } /* 重置负载统计的 last_update_time强制重新附着 */ se-avg.last_update_time 0; /* 标记为已迁移不再是热任务 */ se-exec_start 0; }这个预处理确保了当任务最终在目标 CPU 上执行enqueue_entity时vruntime cfs_rq-min_vruntime能够正确完成归一化。六、实际案例与调试技巧6.1 使用 ftrace 观察任务迁移# 启用调度相关 tracepoint echo 1 /sys/kernel/debug/tracing/events/sched/sched_migrate_task/enable echo 1 /sys/kernel/debug/tracing/events/sched/sched_wakeup/enable echo 1 /sys/kernel/debug/tracing/events/sched/sched_enqueue/enable # 查看 trace cat /sys/kernel/debug/tracing/trace_pipe # 典型输出示例 # idle-0 [003] d.h. 1234.567890: sched_migrate_task: commhackbench pid12345 prio120 orig_cpu3 dest_cpu56.2 使用 bpftrace 监控 vruntime 变化#!/usr/bin/bpftrace #include linux/sched.h kprobe:enqueue_entity { $se (struct sched_entity *)arg1; $flags arg2; $comm curtask-comm; $pid curtask-pid; // 检查 ENQUEUE_MIGRATED 标志 (0x40) if ($flags 0x40) { printf(MIGRATED: %s[%d] vruntime%lu flags0x%x\n, $comm, $pid, $se-vruntime, $flags); } // 检查 ENQUEUE_WAKEUP 标志 (0x01) if ($flags 0x01) { printf(WAKEUP: %s[%d] vruntime%lu flags0x%x\n, $comm, $pid, $se-vruntime, $flags); } }6.3 观察 /proc/sched_debug# 查看各 CPU 的 min_vruntime 差异 grep -A 5 cfs_rq\[ /proc/sched_debug # 典型输出 # cfs_rq[0]:/ # .exec_clock : 1234567.890123 # .MIN_vruntime : 0.000001 # .min_vruntime : 9876543.210987 -- CPU 0 的基准 # .max_vruntime : 0.000001 # # cfs_rq[1]:/ # .min_vruntime : 8765432.109876 -- CPU 1 的基准与 CPU 0 不同6.4 负载均衡触发观察# 查看负载均衡统计 grep -A 20 load_balance /proc/schedstat # 或者使用 perf 记录调度事件 perf record -e sched:sched_migrate_task -a sleep 10 perf script七、常见问题与解答Q1: 为什么被动迁移的任务exec_start要重置为 0答exec_start记录任务最近一次获得 CPU 的起始时间用于计算任务的热度cache hot。迁移后的任务 cache 已经变冷重置exec_start0表示这不是一个热任务在后续的负载均衡中更容易被选中迁移。Q2: 如果两个 CPU 的min_vruntime差异很大迁移会不会导致不公平答归一化机制正是为了解决这个问题。通过vruntime - old_min_vruntime和vruntime new_min_vruntime任务在新队列中的相对位置与其在原队列中的相对位置保持一致。差异会被抵消公平性得以维护。Q3: 为什么唤醒任务有补偿而迁移任务没有答唤醒任务的 vruntime 在睡眠期间冻结而其他任务的 vruntime 持续推进如果不补偿唤醒任务会获得不当优势。迁移任务的 vruntime 是实时的它一直在随原 CPU 的时钟推进因此只需要归一化保持相对位置不需要额外补偿。Q4:GENTLE_FAIR_SLEEPERS特性有什么影响答当开启时唤醒任务的补偿从sysctl_sched_latency减半为latency/2。这使得唤醒任务获得较小的优势防止过度抢占对交互式任务更友好。某些实时优化场景会关闭此特性以获得更快响应。八、最佳实践与性能调优8.1 调度参数调优# 查看当前调度参数 sysctl kernel.sched_latency_ns # 默认 6ms * (1 ilog(ncpus)) sysctl kernel.sched_min_granularity_ns # 默认 0.75ms * (1 ilog(ncpus)) sysctl kernel.sched_wakeup_granularity_ns # 默认 1ms * (1 ilog(ncpus)) # 调整唤醒粒度影响唤醒抢占的敏感度 # 值越小唤醒任务越容易抢占当前任务 sudo sysctl kernel.sched_wakeup_granularity_ns500000 # 500us # 调整调度特性需谨慎 # 查看当前特性 cat /sys/kernel/debug/sched/features # 关闭 GENTLE_FAIR_SLEEPERS某些实时场景 echo NO_GENTLE_FAIR_SLEEPERS /sys/kernel/debug/sched/features8.2 多核系统优化建议CPU 亲和性设置对于缓存敏感型任务使用taskset或sched_setaffinity()绑定到特定 CPU减少被动迁移带来的 cache 失效。调度域理解理解系统的调度域层级MC domain、DIE domain 等合理配置sched_domain参数控制不同层级的均衡频率。监控负载均衡开销通过/proc/schedstat监控alb_count主动均衡次数和lb_failed均衡失败次数评估均衡策略的有效性。8.3 调试技巧总结问题现象调试方法关注指标任务响应延迟高ftracesched_wakeup唤醒到执行的时间间隔负载不均/proc/sched_debug各 CPU 的nr_running过度迁移perf schedsched_migrate_task频率vruntime 异常bpftrace探针se-vruntime变化值九、总结Linux CFS 调度器的被动唤醒机制通过精巧的vruntime 归一化设计确保了任务在 CPU 间迁移时的调度公平性。核心要点包括归一化公式vruntime vruntime - old_min_vruntime new_min_vruntime保持任务在新队列中的相对位置。标志位区分ENQUEUE_MIGRATED标志触发归一化而不触发补偿ENQUEUE_WAKEUP标志触发补偿逻辑。冷热分离迁移任务重置exec_start0标记为冷任务便于后续均衡决策。与主动唤醒的区别被动迁移保持 vruntime 的连续性主动唤醒给予额外补偿以优化响应性。理解这些机制对于排查多核系统的调度异常、优化实时应用性能、以及进行内核调度相关的学术研究都具有重要价值。建议读者结合实际内核源码和ftrace工具在测试环境中进行更深入的分析。参考资料Linux Kernel Source:kernel/sched/fair.c,kernel/sched/core.cDocumentation:Documentation/scheduler/sched-domains.rst相关论文Stoica Abdel-Wahab, Earliest Eligible Virtual Deadline First, 1995