news 2026/10/8 2:44:51

CFS调度器dequeue路径深度解析:从dequeue_task_fair到负载追踪

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CFS调度器dequeue路径深度解析:从dequeue_task_fair到负载追踪

咱们接着上一期继续聊CFS调度器的dequeue路径。如果没看过第一篇,问题也不大,这次直接聚焦在dequeue_task_fair这个入口上,我会从调用时机、内部逻辑、负载传递到调试方法一起串一遍,看到最后你基本能自己在源码里画出这条路径了。

1. 先把“出队”这件事摆到正确的位置上

1.1 一个进程被“请出”运行队列的几种场景

在CFS调度器里,“出队”不是只有进程睡眠这么一件事。很多人刚开始读内核源码时,以为sched_dequeue只发生在进程主动sleep,实际上它覆盖的场景非常广,我把最常见的四类列一下:

  • 进程调用sleep、nanosleep、cond_wait等主动让出CPU,进入阻塞态;
  • 进程被信号打断或者被同步机制唤醒后再次睡眠,即DEQUEUE_SLEEP语义;
  • 进程被迁移到其他CPU时,需要先把调度实体从当前rq摘除,再挂到目标rq;
  • 完全公平调度器面临带宽控制时,CFS运行队列被throttle,批量把实体摘除。

这几种场景在dequeue_task里通过不同的flags区分,最终都会汇集到dequeue_task_fair,但它内部对同一函数的处理差异很大。如果你只是大概看函数名,很容易误以为“dequeue就是rb_tree删除”,真实情况要复杂得多,因为它还承担了负载更新、兄弟调度器回调、带宽控制等多重职责。

1.2 为什么必须单独研究dequeue_task_fair

我经常看到有人只追dequeue_entity,把树操作研究完之后就结束了。这个思路不能说错,但会漏掉一半逻辑。dequeue_entity只能算“当前层级”的摘除,dequeue_task_fair才是一个完整的“自顶向下”的处理过程——它要先处理本CPU的CFS运行队列,如果当前调度实体属于某个task group,还要递归地把对父级调度实体的贡献也摘除掉。

也就是说,dequeue_task_fair处理的是调度实体嵌套的问题。在cgroup和task group场景下,一个进程属于某个group,group又挂在更高层的CFS rq上,你只删叶子节点,不更新中间节点的负载与权重,调度器就会算出错误的数据,最后导致CPU时间分配失衡。这套逻辑也是task group负载跟踪的关键路径,所以单独拉出来看很有必要。

1.3 入口参数与三个关键标志

先看函数原型:

static void dequeue_task_fair(struct rq *rq, struct task_struct *p, int flags)

一个rq表示当前CPU的就绪队列,p是被摘除的进程,flags则告诉调度器这次出队的性质。常用的有下面几个:

  • DEQUEUE_SLEEP:进程主动睡眠,这是最常见的路径;
  • DEQUEUE_MOVE:进程要被迁移到其他CPU,所有调度实体需要一并迁移;
  • DEQUEUE_IDLE:当前是idle线程上下文,要求为了照顾真实任务作特殊处理。

还有一个容易忽略的细节:在较新内核里,dequeue_task_fair会检查flags & DEQUEUE_SLEEP,如果为真,会在合适的时机把调度实体加入cfs_rq->curr的vruntime调整逻辑。这是因为睡眠进程不该被立即惩罚,否则它本来睡眠的时间还没补偿回来,醒来之后又要重新排队,和CFS的虚拟时间模型冲突。这一点后面具体展开。

2. dequeue_task_fair主流程拆解:从入口到rb_tree删除

2.1 调用链:scheduler_tick到dequeue_task的汇合点

先画一下调用链(不要嫌简单,链子是后面排查问题的基础):

  • 进程睡眠路径:do_nanosleep->schedule->__schedule->pick_next_task/deactivate_task->dequeue_task->dequeue_task_fair
  • 负载均衡路径:load_balance->detach_tasks->detach_one_task->deactivate_task->dequeue_task_fair,此时flags带DEQUEUE_MOVE。
  • 带宽控制路径:start_cfs_bandwidth/throttle_cfs_rq,直接操作CFS队列,但是也会调用一些回调函数,最终可能通过dequeue_task_fair的辅助逻辑。

deactivate_task是个调度类无关的封装,它拿到当前task所属的调度类,再调用对应类的dequeue_task方法。CFS的dequeue_task_fair就是在这里被间接调到的。理解了这一层,你就知道为什么调试ftrace时不能只挂dequeue_task_fair,还要同时挂deactivate_task来看调用方。

2.2 先更新CFS运行队列的时钟:update_curr

函数第一步不是直接删节点头,而是先更新当前运行实体的运行信息。update_curr(cfs_rq)会做这些事:

  • 取得当前cfs_rq->curr,即正在占用CPU的调度实体;
  • 计算从上次更新到现在的真实时间差delta_exec;
  • 把delta_exec折算成虚拟运行时间,加到当前实体的vruntime;
  • 如果当前位置没变,还会更新cfs_rq->min_vruntime的可能候选值。

为什么要先做这一步?因为CFS的公平性完全建立在vruntime上,而vruntime本身是逐步累计的。如果你在dequeue之前不更新,那么被删除实体在CPU上最后一段时间的运行成本就没算进去,树上的键值就会偏小,后续调度实体选择就会偏差。这里尤其值得注意的是,delta_exec是经过scale_load和权重加权的,测试代码或者性能分析时如果发现vruntime增长异常,多半要回到这里检查时钟源和负载贡献计算。

2.3 dequeue_entity:从树中摘除只是开始

dequeue_task_fair的主要工作其实是循环调用dequeue_entity。在4.19以后的内核里,dequeue_task_fair的循环结构很清晰:先从当前CPU的CFS队列开始,取到调度实体se = &p->se,然后不断把该实体从所属队列里摘除,同时向上回溯父实体。

来看dequeue_entity内部三个核心动作:

第一个动作是update_curr(se->cfs_rq),这里会重复一次vruntime更新。因为它可能在父层级改变了curr,所以子层级也要清楚自己当前的运行时间。

第二个动作是__dequeue_entity(cfs_rq, se),这才是真正的红黑树删除。CFS的__dequeue_entity会把se从rb_tree里拿走,同时更新cfs_rq->nr_running。这里有个容易误解的点:树节点被删除后,min_vruntime并不会自动跟随新的左子树节点,需要调用update_min_vruntime手动刷新。所以代码里几乎是立刻执行了这个函数。

第三个动作是负载贡献更新。dequeue_entity会根据se的负载值,从cfs_rq->avg.load_avg里扣掉对应贡献,同时更新rq->cpu_load[],这部分在负载均衡时会被用到。从内核5.4开始,这部分大量使用PELT(Per Entity Load Tracking),即周期性负载跟踪,dequeue 时要把se->avg.last_update_time等跳跃点处理对,否则后面的enqueue_entity会累加出飘移的负载。

2.4 向上递归:组调度实体的父链处理

如果你只用单个进程的CFS队列,那么一个dequeue_entity就结束了。但只要开了CONFIG_FAIR_GROUP_SCHED,事情就变得复杂。dequeue_task_fair在删完se后,会判断se->parent是否存在;如果存在,就把父级调度实体也作为se继续递归。

举个例子。进程A放在cgroup A组,A组作为调度实体挂在根CFS队列上。当进程A睡眠,我们需要:

  • 先把进程A从cgroup A的CFS rq中删掉;
  • 如果cgroup A在父队列上的se因为子进程减少而不再有可运行任务,也要从父队列上摘除;
  • 每次摘除父实体时,同样要更新父层的nr_running和负载贡献。

这个递归有个边界条件:如果某个父级实体还有别的子实体在运行,那么不能把它从父队列删掉,只更新负载,不执行树删除。

我在调试时经常用trace_printk或者bpf去打印se->parent和cfs_rq->nr_running,就是为了确认递归是否正确退出。实际遇到过一个case:cgroup的子任务早就退出,但父实体一直挂在树上,导致CPU带宽分配异常,最后定位到就是某个dequeue路径在父实体处置时因为curr == se的检查顺序写反了,属于内核历史版本的bug。

3. 带宽控制、睡眠补偿与idle特殊处理:三个被低估的细节

3.1 被throttle的CFS队列:dequeue_task_fair如何配合带宽控制

CFS并不是无限公平的,它受cpu.maxcgroup带宽控制约束。当一个cgroup的CPU配额用完,内核会调用throttle_cfs_rq把该队列的实体全部摘除,挂到throttled_list上。此时dequeue_task_fair虽然不会直接负责throttle,但它必须正确识别出当前CFS是否处于throttle状态,避免错误地修改nr_running。

代码里有一个非常经典的判断:

if (!se) { if (!cfs_rq->nr_running || cfs_rq->throttled) break; goto more; }

这段逻辑的含义是:当cfs_rq->throttled为真时,即使队列里还挂着实体,也不要继续执行父级删除了,因为throttle状态下的队列本来就不参与调度。否则你会把父实体的nr_running改小,导致整个CPU认为系统空转,进入idle的提前唤醒流程,最终产生调度延迟。

另一个容易被坑的点是sub_nr_running与cfs_b->nr_running的同步。带宽控制场景中,CFS带宽管理有一个独立的计数器,throttle 时递增,unthrottle 时递减。dequeue_task_fair要保证没有重复递减。如果你用strace或者感知到cpu quota耗尽后任务状态异常,建议先检查cfs_rq->throttled_clock_task与throttled_count,这两个字段能快速确认是不是带宽控制路径触发了额外dequeue。

3.2 DEQUEUE_SLEEP的vruntime补偿:睡眠进程不该被惩罚

DEQUEUE_SLEEP是dequeue最核心的场景,但对vruntime的处理却很微妙。在老的O(1)调度器里,进程睡眠会把vruntime直接重置或者保持不变;CFS则更进一步,想尽量保证睡眠进程醒来后仍能根据它的虚拟时间公平竞争。

dequeue_task_fair里对DEQUEUE_SLEEP的处理主要在dequeue_entity前。你会在代码中看到这样的逻辑:

if (flags & DEQUEUE_SLEEP) { se->vruntime -= cfs_rq->min_vruntime; se->vruntime += cfs_rq->min_vruntime; }

看着有点脱裤子放屁对吧?实际上这是为了处理vruntime归一化。在CONFIG_FAIR_GROUP_SCHED下,不同CPU的min_vruntime可能不一样,而实体在睡眠期间其虚拟时间可能需要偏移到当前的min_vruntime基准上,以保证唤醒后不会被立即抢占。

但这里有个我们要特别注意的地方:DEQUEUE_SLEEP不能随意移动vruntime,否则就会破坏“组内虚拟时间单调性”。比如一个进程在睡眠期间,同组任务一直运行,min_vruntime推进了,如果不对se的vruntime做偏移,进程唤醒后会显得“很新”,抢到大量CPU,形成sleeping catch-up效应。这其实是有意设计的,但也需要配合place_entity的唤醒前插入位置计算一起看。建议看完dequeue_task_fair后立刻去看enqueue_task_fair和place_entity,三者联动才能理解睡眠补偿的完整范围。

3.3 idle与DEQUEUE_IDLE:防止调度器逻辑混乱

还有一种特殊路径是idle线程触发的dequeue。系统在进入idle之前,调度器会用pick_next_task选择idle线程,但某些特殊场景下,idle线程也会参与dequeue操作。此时dequeue_task_fair会检查DEQUEUE_IDLE标志,调整cfs_rq->idle_h_nr_running这类计数。

idle处理容易出问题的地方在于调度类选择逻辑。CFS的dequeue_task_fair里有一个变量is_idle_task,它会通过task_css_is_root或者task_sched_class判断当前任务是否为idle。如果判断不准,就可能把一个idle线程错误地推入CFS队列,导致idle线程抢占普通任务。这个场景在CPU hotplug或者suspend/resume过程中比较常见,表现为系统进入idle后唤醒延迟变高。

从调试角度看,这类问题很少直接表现在dequeue_task_fair的栈上,更多是表现为调度的“幽灵唤醒”。建议用/proc/sched_debug查看每个CFS队列的nr_running是否为0,如果nr_running不为0但CPU一直在idle,说明dequeue路径没有完全摘除干净,要重点查idle_h_nr_running。

4. 源码实践:用ftrace追踪dequeue_task_fair,以及几个真实故障

4.1 用ftrace看一条完整的dequeue调用链

光看代码不如动手追踪。我在长期调试中习惯用ftrace的function_graph配合filter挂到dequeue_task_fair上,这样能直观看到函数的嵌套调用。示例命令:

echo 'dequeue_task_fair' > /sys/kernel/tracing/set_graph_function echo function_graph > /sys/kernel/tracing/current_tracer echo 1 > /sys/kernel/tracing/tracing_on sleep 1 echo 0 > /sys/kernel/tracing/tracing_on cat /sys/kernel/tracing/trace | grep -A 30 'dequeue_task_fair'

输出里你能看到类似结构:

1) 0.500 us | dequeue_task_fair(); 1) 0.200 us | update_curr(); 1) 0.100 us | __dequeue_entity(); 1) 0.150 us | update_min_vruntime(); 1) 0.200 us | update_cfs_group();

这里我要强调一个经验:ftrace只是辅助,真正要判断性能问题,还得加kprobe或bpf trace,直接读se->vruntime和min_vruntime的最终值。比如可以用bpftrace:

kprobe:dequeue_task_fair { @vruntime[tid]=((struct task_struct *)arg1)->se.vruntime; }

不过参数在不同架构下偏移不同,不建议直接用,更稳的方式是使用/proc/sched_debug以及perf sched记录整个调度事件。

4.2 常见问题:min_vruntime更新滞后导致新进程饥饿

我在排查性能问题时碰到过一类经典现象:新创建的进程总是长时间得不到调度,而老进程几乎占满CPU。用sched_debug看,会发现新进程的vruntime非常小,但它没有进入树中;老进程的min_vruntime却远远大于新进程。

问题定位到dequeue路径的update_min_vruntime。CFS的update_min_vruntime只有在curr为NULL或curr->vruntime小于当前左子树节点时才把min_vruntime设为curr->vruntime。如果某个dequeue_entity后,curr被提前清空或者se->vruntime更新顺序不对,就会导致min_vruntime停滞在旧值。新进程在插入时基于不合理的min_vruntime计算vruntime,自然会被推到很远的位置。实际修复要结合内核版本,但排查顺序一定是先看dequeue路径有没有正确刷新min_vruntime。

4.3 进程卡在D状态,如何判断是不是dequeue路径的锅

很多时候进程进入不可中断睡眠后,看起来像dequeue卡住。我建议先查看/proc/<pid>/stack和wchan,确认是不是在do_exit或者schedule处停留。如果两者都指向__schedule,再用ftrace挂deactivate_task与dequeue_task_fair,看函数是否正常返回。

有一个真实案例:在某个5.10版本内核上,cgroup的cpu.max被限制为100ms/200ms,而进程调用了futex_wait频繁睡眠唤醒。由于dequeue_task_fair里的update_curr对delta_exec做了多次max夹取,导致在时钟节拍非常短的虚拟机里,delta_exec被拉大,CFS虚拟时间出现负值回退,进程卡在D状态。最后通过给该cgroup提高cpu.max并升级内核解决。这类问题很难通过阅读单一函数发现,但如果没有对dequeue路径的完整理解,你连排查方向都找不到。

4.4 内核“八股”复习点:dequeue路径的必考细节

很多Linux内核学习资料都喜欢把CFS调度器做成面试八股,我在这里把dequeue相关的几个“送分点”整理一下,方便面试前快速过一遍:

  • dequeue_task_fair由dequeue_task调度类函数指针调用,CFS在调度类初始化时注册该函数;
  • dequeue_entity负责层级内的摘除、负载更新和min_vruntime维护;
  • DEQUEUE_SLEEP会影响vruntime补偿,但不能直接修改min_vruntime;
  • 带宽控制时throttled状态会阻止父实体继续摘除;
  • 负载均衡时的DEQUEUE_MOVE不计算睡眠补偿,只执行迁移所需的最小操作。

这几个点如果自己能串起来讲清楚,说明已经基本掌握CFS出队逻辑了。

5. 我的内核源码阅读心得与后续扩展方向

5.1 阅读顺序建议:从dequeue_task_fair到enqueue_task_fair的对照

我一直推荐把dequeue_task_fair和enqueue_task_fair放在一起读,原因很简单:它们的逻辑是对称的,但细节完全不同步。比如enqueue_task_fair在插入树前有一个account_entity_enqueue,而dequeue_task_fair则要额外处理nr_running的递减。只看一半容易产生“插入和删除应该互逆”的错误直觉,实际上两者在带宽控制、负载跟踪、唤醒抢占等路径上差异极大。

我当时把两个函数打印出来并排对比后发现,dequeue侧明显更复杂的是group entity的“占用继承”处理。当父实体还在被其他进程使用时,dequeue不能删除它,只能更新shares和负载。而enqueue侧更多是考虑是否要抢占当前任务,以及是否需要重新计算组权重。把两侧放在一起,你就能自然理解为什么CONFIG_FAIR_GROUP_SCHED会显著增加调度器的复杂度。

5.2 从CFS到EEVDF:dequeue思想的演进

如果你现在用的是6.6以上内核,会发现CFS的vruntime模型已经从严格红黑树切换到EEVDF的延迟期望模型,但dequeue_task_fair这个名字并没有变。在EEVDF中,出队和入队仍然有dequeue_entity和enqueue_entity,只是树上的键值从vruntime调整为virtual deadline相关量。因此本篇的所有宏观概念仍然适用,但细节计算要重新理解。

我建议学习者在读完CFS之后立刻去读EEVDF的补丁集,重点看entity_key和place_entity的变化,这会让你理解为什么新调度器不再需要CFS里那种花式的min_vruntime归一化。对于只做运维不写调度器的朋友,知道dequeue路径还在、名字没变、主要流程仍由调度类函数指针触发就够了。

5.3 调试工具链的推荐组合

如果你要深入dequeue路径,我比较推荐这组工具组合:ftrace+/proc/sched_debug+crash来分析在线与离线问题。平时可以用perf sched record+perf sched latency看整体调度延迟,一旦出现异常再用bpftrace或内核动态插桩去抓dequeue_task_fair的运行时间和调用者。

我踩过几次坑之后学到的教训是:不要一上来就用复杂的追踪,优先看/proc/sched_debug和/sys/kernel/debug/sched/目录。大部分dequeue问题都可以从nr_running与nr_uninterruptible的差值上看出端倪。把基本状态确认好,再决定是否需要打动态补丁,这才是高性价比的排查方式。

最后给一点个人体会:其实每个调度器函数的难度不在于那几十行代码,而在于你必须同时理解rq、cfs_rq、task_group、sched_entity四个结构体之间的相互引用和计数关系。很多事故都是计数不同步造成的,而这些计数恰恰在dequeue路径里被大量修改。把dequeue_task_fair的每一步都和数据结构的实际变化对上号,比背十遍源码都管用。希望这篇拆解能帮你在读内核源码时少走点弯路。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/8 2:44:40

Windows虚拟桌面切换卡顿?一个DLL文件修复全搞定

前几天一个朋友跟我吐槽&#xff0c;说他在 Windows 10 上按 CtrlWin右箭头切换虚拟桌面时&#xff0c;画面总要卡个一秒多&#xff0c;偶尔还会直接黑屏闪一下。我第一反应是显卡驱动问题&#xff0c;结果 GPU、内存、磁盘占用跑了一圈&#xff0c;全都在正常范围&#xff0c;…

作者头像 李华
网站建设 2026/10/8 2:44:19

人工智能与发酵工程教学融合:从数据驱动到对话式学习的模式创新

简介&#xff1a;面向高校发酵工程类专业教师、课程建设者与教学研究者的专题文档&#xff0c;聚焦人工智能在传统发酵工程教学中的落地方式。内容先梳理发酵工程发展现状及教学瓶颈&#xff0c;说明AI在实时监控、数据分析、模式识别、参数优化与结果预测等方面的价值&#xf…

作者头像 李华
网站建设 2026/10/8 2:44:04

select多路IO转接:Linux高并发服务器的入门与实践

刚接触Linux网络编程的人&#xff0c;拿到"同时服务几十上百个客户端"这种需求时&#xff0c;第一反应往往是来一个连接就fork一个进程&#xff0c;或者起一个线程。我最早写的一版多客户端服务器也是这样&#xff1a;一个客户端对应一个线程&#xff0c;逻辑确实好写…

作者头像 李华
网站建设 2026/10/8 2:43:47

机械臂控制实战指南:从运动学、轨迹规划到PID调参与工程落地

机械臂控制这个领域&#xff0c;网上教程不少&#xff0c;但大多数要么是纯理论推导把人劝退&#xff0c;要么是ROS玩具级演示离落地差着十万八千里。我前前后后折腾过几套工业级和DIY级的机械臂&#xff0c;从六轴串联臂到SCARA都碰过&#xff0c;踩的坑比很多人想象的要多得多…

作者头像 李华
网站建设 2026/10/8 2:42:25

从宿主机到Docker容器:文件复制实战全解析

做容器化部署久了&#xff0c;你会发现最常用的运维操作往往不是构建镜像、编排服务&#xff0c;而是“往容器里塞文件”。排查问题需要替换配置文件、导出一个环境变量、往容器里丢一个数据包&#xff0c;这些瞬间需求都离不开从主机复制文件到 Docker 容器。如果你用过docker…

作者头像 李华
网站建设 2026/10/8 2:42:19

AI效率链路:模型选型、提示词工程与Agent工作流

简介&#xff1a;《AI效率手册&#xff1a;从ChatGPT开启高效能》是一份系统讲解AI落地应用的PDF资料&#xff0c;面向学生、职场新人及希望借助AI提升学习、工作与生活效率的读者。全书先厘清AI基础原理与主流工具选择&#xff0c;再重点拆解提示词工程、AI调教方法和多场景应…

作者头像 李华