第2章:Linux调度器基础——CFS调度器原理、vruntime与红黑树、调度类与调度策略

大家好,我是你们的芯片底层系统工程师。今天我们来聊聊Linux调度器里最核心的一块——CFS调度器。

说实话,我刚入行那会儿,对调度器的理解也就停留在“谁优先级高谁先跑”这个层面。直到有一次做手机SoC的功耗调优,发现某个后台任务疯狂抢CPU,前台应用卡成PPT,我才真正意识到:调度器不是简单的排队,而是一门平衡的艺术

核心观点:CFS调度器的目标,是让每个任务都能公平地获得CPU时间,而不是简单地按优先级排队。

2.1 CFS调度器:公平不是平均

CFS,全称Completely Fair Scheduler,完全公平调度器。这个名字听起来很理想主义,对吧?但实际做起来,远比想象中复杂。

我个人的理解是:CFS追求的是“加权公平”。什么意思呢?每个任务都有一个权重(weight),权重高的任务,分到的CPU时间就多。但CFS保证,在足够长的时间窗口内,每个任务都能得到它应得的那一份。

举个例子:

  • 任务A权重1024(普通优先级)
  • 任务B权重2048(高优先级)
  • 任务C权重512(低优先级)

如果三个任务同时运行,CPU时间分配大概是:A占28.6%,B占57.1%,C占14.3%。B拿到的比C多4倍,这就是“加权公平”。

避坑指南:我曾经在调试一个游戏场景时,发现后台下载任务抢走了大量CPU,导致游戏掉帧。后来发现是下载任务的nice值没设对,默认的0权重和前台游戏一样。把nice值调到10之后,问题就解决了。记住:权重决定份额,不是决定优先级

2.2 vruntime:调度器的“记账本”

CFS怎么知道每个任务跑了多少时间?靠的就是vruntime(虚拟运行时间)。

说白了,vruntime就是每个任务的“时间账本”。每次时钟中断,调度器都会更新当前任务的vruntime。但这里有个关键点:vruntime不是简单的物理时间,而是经过权重调整后的时间。

公式很简单:

vruntime += 实际运行时间 * (1024 / 任务权重)

你看,权重高的任务(比如2048),vruntime增长得慢;权重低的任务(比如512),vruntime增长得快。这样,CFS每次选择vruntime最小的任务来运行,就能保证公平。

我刚开始看这个公式时,总觉得有点绕。后来想明白了:权重高的任务,它的“时间”更值钱,所以同样的物理时间,它的vruntime增长更慢,自然就能多跑一会儿。

关键点:CFS每次调度时,选择vruntime最小的任务来运行。这就是“完全公平”的数学基础。

2.3 红黑树:高效的任务管理

好了,现在问题来了:系统里可能有几百个任务,每次调度都要找到vruntime最小的那个,怎么找最快?

答案是红黑树

红黑树是一种自平衡的二叉搜索树。CFS把所有可运行的任务(runnable任务)都挂在红黑树上,以vruntime为键值。这样,最左边的节点就是vruntime最小的任务。

我画了一张图,帮你理解这个结构:

CFS红黑树结构示意图 vruntime = 100ms vruntime = 80ms vruntime = 120ms vruntime = 60ms vruntime = 90ms vruntime = 110ms vruntime = 140ms ← 下次调度选我 红黑树特性:左子树所有节点vruntime < 根节点 < 右子树所有节点 最左节点 = vruntime最小 = 下一个被调度的任务

红黑树的优势很明显:

  • 插入/删除:O(log n)复杂度,几百个任务也就几次比较
  • 查找最小:O(1)复杂度,直接取最左节点
  • 自平衡:不会出现极端情况导致性能退化

个人经验:我在做手机性能调优时,经常需要查看当前红黑树的状态。通过/proc/sched_debug可以打印出每个CPU上的红黑树信息。有一次我发现某个CPU上的红黑树深度异常,排查后发现是某个内核线程的vruntime被错误地重置了。嗯,这种问题不常见,但一旦遇到,性能影响很大。

2.4 调度类与调度策略

CFS只是Linux调度器的一部分。实际上,Linux内核使用调度类(Scheduling Class)的架构,每个调度类负责管理一类任务。

常见的调度类有:

调度类 优先级 调度策略 典型用途
Stop调度类 最高 CPU停靠、热插拔
Deadline调度类 SCHED_DEADLINE 实时任务,有严格截止时间
RT调度类 SCHED_FIFO, SCHED_RR 实时任务,如音频处理
CFS调度类 SCHED_NORMAL, SCHED_BATCH, SCHED_IDLE 普通任务、后台批处理、空闲任务
Idle调度类 最低 空闲线程

每个调度类有自己的调度策略。CFS调度类支持三种策略:

  • SCHED_NORMAL:普通任务,大多数应用都用这个
  • SCHED_BATCH:批处理任务,适合CPU密集型的后台任务,调度器会尽量让它们连续运行,减少上下文切换
  • SCHED_IDLE:空闲任务,只有CPU完全空闲时才运行,适合低优先级的后台任务

我记得有一次做手机温控优化,发现某个后台数据同步任务总是和前台应用抢CPU。后来我把它的调度策略从SCHED_NORMAL改成SCHED_IDLE,问题就解决了。前台应用流畅了,后台同步虽然慢了点,但用户根本感觉不到。

注意事项:不要轻易把关键任务改成SCHED_IDLE。我曾经见过一个开发者把UI线程设成SCHED_IDLE,结果界面卡成幻灯片。记住:前台交互任务一定要用SCHED_NORMAL,后台非关键任务才考虑SCHED_IDLE。

2.5 调度器的工作流程

好了,我们把上面的知识串起来,看看一次完整的调度过程是怎样的:

  1. 时钟中断触发:每个tick(通常1ms或4ms),时钟中断处理函数会调用调度器
  2. 更新vruntime:当前任务的vruntime增加,公式是:vruntime += 实际运行时间 * (1024 / 权重)
  3. 检查是否需要抢占:如果当前任务的vruntime超过了红黑树中最左节点的vruntime,说明有更“饿”的任务在等待
  4. 选择下一个任务:从红黑树中取出最左节点(vruntime最小)
  5. 上下文切换:保存当前任务的寄存器状态,恢复新任务的寄存器状态
  6. 新任务开始运行:CPU开始执行新任务的指令

这个过程看起来简单,但实际实现中充满了各种优化。比如:

  • 组调度(Group Scheduling):把任务分组,每个组公平分配CPU时间
  • 负载均衡:多核之间迁移任务,保持各核负载均衡
  • HMP调度:大小核架构下的调度优化

这些内容我们后面会详细讲,今天先把基础打牢。

总结一下:

  • CFS调度器通过vruntime记录每个任务的“时间账本”
  • 红黑树高效管理所有可运行任务,最左节点就是下一个要运行的任务
  • 调度类架构让Linux能同时管理实时任务和普通任务
  • 调度策略决定了任务在CFS调度类中的行为

说实话,调度器这块内容,光看书是学不会的。我建议你动手做个小实验:写两个程序,一个CPU密集型的,一个IO密集型的,然后用chrt命令改变它们的调度策略和优先级,观察系统行为的变化。实践出真知,这话一点不假。

好了,今天就到这里。记住:调度器不是魔法,它只是用数据结构和算法,把CPU时间公平地分给每个任务。理解了这一点,后面的内容就好办了。


公众号:蓝海资料掘金营,微信deep3321