- why
- what
- how
extern const struct sched_class stop_sched_class;
extern const struct sched_class dl_sched_class;
extern const struct sched_class rt_sched_class;
extern const struct sched_class fair_sched_class;
extern const struct sched_class idle_sched_class;
cfs virtual time
- CFS 的 virtual time 是对进程实际运行时间的 “公平加权映射”,目的是让所有进程按权重公平分配 CPU,而非直接用物理时间判断优先级。CFS 全称 Completely Fair Scheduler(完全公平调度器),是 Linux 内核默认的进程调度器,它的核心思想是 “让所有可运行进程都能公平地占用 CPU”,而 virtual time 就是实现这个 “公平” 的核心载体。
- 物理时间:CPU 真实流逝的时间(比如 jiffies、ns 级时间戳),进程实际占用 CPU 的物理时长是真实消耗,但直接按物理时间调度会不公平(比如高优先级进程和低优先级进程同等对待)。
- 进程权重(weight):CFS 用权重表示进程优先级(对应 nice 值),nice 值越小(优先级越高),weight 越大;反之则 weight 越小。核心规则:权重越大,应分配到更多 CPU 时间。
要理解Linux CFS调度的virtual time(虚拟时间),核心先记住一句话:CFS的virtual time是对进程实际运行时间的“公平加权映射”,目的是让所有进程按权重公平分配CPU,而非直接用物理时间判断优先级。
CFS全称Completely Fair Scheduler(完全公平调度器),是Linux内核默认的进程调度器,它的核心思想是“让所有可运行进程都能公平地占用CPU”,而virtual time就是实现这个“公平”的核心载体。
先铺垫2个前提,理解virtual time会更顺畅:
- 物理时间:CPU真实流逝的时间(比如jiffies、ns级时间戳),进程实际占用CPU的物理时长是真实消耗,但直接按物理时间调度会不公平(比如高优先级进程和低优先级进程同等对待)。
- 进程权重(weight):CFS用权重表示进程优先级(对应nice值),nice值越小(优先级越高),weight越大;反之则weight越小。核心规则:权重越大,应分配到更多CPU时间。
一、 为什么需要virtual time?(核心痛点)
如果不用virtual time,直接按物理时间调度会有2个问题:
- 无法体现优先级:高优先级和低优先级进程占用相同物理时间,不符合调度需求。
- 公平性失衡:多核/多进程场景下,无法精准衡量“谁该继续运行、谁该让出CPU”。
CFS的解决思路是:不看进程跑了多少物理时间,看它跑了多少“加权后的虚拟时间”,让所有进程的virtual time尽可能持平——谁的virtual time少,谁就先跑,这就是CFS的“公平”核心。
二、 什么是CFS的virtual time?
1. 定义
virtual time(虚拟时间)是进程实际运行物理时间,按自身权重做“反比缩放”后得到的时间值,单位是纳秒(ns),核心公式(简化版):
virtual_time+=物理运行时间×基准权重进程自身权重virtual\_time += \frac{物理运行时间 \times 基准权重}{进程自身权重}virtual_time+=进程自身权重物理运行时间×基准权重
- 基准权重(NICE_0_LOAD):默认值1024,对应nice=0的进程权重,是权重的参考基准。
2. 关键特性(决定CFS调度逻辑)
- 权重与virtual time增速成反比:权重越大,virtual time跑得越慢;权重越小,跑得越快。
- 例1:高优先级进程(nice=-2,weight=1566),物理运行10ns,virtual time增加
10*1024/1566 ≈ 6.5ns - 例2:低优先级进程(nice=2,weight=717),物理运行10ns,virtual time增加
10*1024/717 ≈ 14.3ns
👉 核心效果:高优先级进程跑相同物理时间,虚拟时间涨得少,能获得更多CPU运行机会。
- 例1:高优先级进程(nice=-2,weight=1566),物理运行10ns,virtual time增加
- virtual time是累计值:进程创建时virtual time初始化为0,每次占用CPU都会累加,让出CPU时保留当前值,下次唤醒后继续累计。
- 调度队列按virtual time排序:CFS的可运行队列(rbtree红黑树),按virtual time从小到大排序,树最左节点就是virtual time最小的进程,也是下一个要调度的进程(CFS的“选下一个进程”逻辑极简)。
三、 CFS中virtual time的核心关联概念(必须懂)
virtual time不是孤立的,要结合2个核心概念理解,才能串起CFS调度流程:
1. vruntime:进程的虚拟时间(per-task)
每个进程的task_struct结构体中,有一个字段vruntime,就是该进程的virtual time。
- 进程未运行时:vruntime不变;
- 进程运行时:按上述公式实时累加vruntime;
- 进程睡眠/阻塞(比如wait IO):唤醒时会调整vruntime(避免睡眠进程vruntime“落后太多”,醒来后抢占所有CPU),调整逻辑:用当前队列中最小vruntime作为基准,让唤醒进程的vruntime向这个基准对齐(保证公平)。
2. min_vruntime:调度队列的最小虚拟时间(per-rq)
每个CPU的调度队列(runqueue),有一个min_vruntime字段,记录当前队列中所有可运行进程的最小vruntime。
- 作用1:作为队列的“公平基准线”,所有进程的vruntime都围绕它波动;
- 作用2:进程唤醒时的vruntime调整参考(避免睡眠进程vruntime过低);
- 作用3:计算进程的“公平剩余时间”,判断是否需要调度切换。
四、 virtual time如何驱动CFS调度?(核心流程)
CFS的调度逻辑,本质就是围绕vruntime的“追平”逻辑,3步就能看懂:
- 选下一个进程:CPU空闲时,直接选runqueue红黑树最左节点(vruntime最小的进程),切换到该进程运行。
- 运行中更新vruntime:进程运行时,内核按物理时间流逝,实时计算并累加其vruntime(按权重缩放)。
- 触发调度切换:当当前运行进程的vruntime,追上甚至超过队列的min_vruntime时,触发调度(schedule()),重新选vruntime最小的进程运行。
✅ 终极公平目标:让所有可运行进程的vruntime尽可能接近min_vruntime,实现“按权重比例共享CPU”。
五、 关键补充:virtual time的精准计算(内核实际实现)
前面给的是简化公式,内核中virtual time的计算要更精准,核心是2点:
1. 避免浮点运算(内核尽量不用浮点)
内核用固定点运算替代浮点,把公式转化为整数运算,避免精度损失和性能开销:
vruntime += (物理时间 << 32) * NICE_0_LOAD / weight
- 左移32位相当于乘以2³²,用高位表示整数部分,低位表示小数部分,实现高精度固定点计算。
2. 物理时间的精准来源
内核用sched_clock()获取高精度时间(ns级),而非jiffies(毫秒级,精度不够),保证vruntime的累加精准度,适配高频调度场景。
六、 举例:直观理解virtual time的公平性
假设有2个进程,CPU单核(无其他进程),基准权重1024,物理总时间20ns:
- 进程A:nice=0,weight=1024;
- 进程B:nice=-1,weight=1215(优先级更高)。
按virtual time规则计算:
- 核心约束:最终A和B的vruntime要尽量相等(公平);
- 设A物理运行时间t₁,B物理运行时间t₂,t₁+t₂=20ns;
- 公平条件:
t₁*1024/1024 = t₂*1024/1215→t₁ = t₂*1024/1215; - 计算得:t₂≈11.0ns,t₁≈9.0ns。
👉 结果:高优先级的B,分配到更多物理时间(11ns vs 9ns),但两者的vruntime均≈9ns(持平),实现了“按权重公平分配”——这就是virtual time的核心价值。
七、 常见误区澄清
- 误区1:virtual time是“虚拟CPU的时间”?❌
正解:是物理时间的加权映射,和虚拟CPU无关,是CFS的调度算法抽象。 - 误区2:vruntime越小,进程优先级越高?✅ 但要注意:睡眠唤醒的进程会调整vruntime,不是绝对的。
- 误区3:CFS是“时间片调度”?❌
正解:CFS没有传统固定时间片,而是靠vruntime动态判断是否切换,权重决定vruntime增速,间接决定“实际占用时间”。
延伸小知识点(贴合你关注的内核底层)
- CFS的vruntime是per-task、per-cpu的,多核场景下通过
load_balance均衡各CPU队列的min_vruntime,避免跨核公平性失衡; - 实时进程(SCHED_FIFO/SCHED_RR)不参与CFS调度,不受vruntime约束,会抢占CFS进程;
- 内核5.0+后,CFS对vruntime的计算做了优化(比如针对多核负载均衡的vruntime同步),但核心逻辑不变。
要不要我帮你梳理CFS中vruntime与task_struct、runqueue的核心字段关联,并补充一段查看进程vruntime的内核调试命令?
为什么将P通过dequeue_task移除了还将P通过put_prev_task又放呢?
queued = task_on_rq_queued(p);
running = task_current(rq, p);
if (queued)
dequeue_task(rq, p, queue_flags);
if (running)
put_prev_task(rq, p);
但这之前不是用dequeue_task函数移除queue了吗?
核心结论先行
queued 和 running 是两个互斥的状态。一个进程在某个CPU上,不可能同时处于“正在运行”和“在队列中排队”两种状态。
因此,if (queued) 和 if (running) 这两个条件永远只会有一个为真,dequeue_task 和 put_prev_task 永远只会有一个被调用。
一、 进程在CPU上的三种状态
要理解这个逻辑,我们首先要明确一个进程在某个CPU上可能存在的三种状态:
-
状态一:正在运行 (Running)
- 条件:
task_current(rq, p)为true。 - 含义:进程
p是当前CPU上正在执行的进程。它的task_struct地址与rq->curr相等。 - 队列状态:它不在任何运行队列的子队列(如CFS的红黑树、RT的优先级队列)中。它是“独占”CPU的。
- 条件:
-
状态二:在队列中排队 (Queued)
- 条件:
task_on_rq_queued(p)为true。 - 含义:进程
p的状态是TASK_RUNNING,并且它正在某个运行队列的子队列中等待被调度执行。 - 队列状态:它在子队列中。
- 条件:
-
状态三:既不运行也不排队 (Not on rq)
- 条件:
task_on_rq(p)为false。 - 含义:进程
p处于睡眠状态(TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE),或者正在被创建、销毁、迁移到其他CPU。 - 队列状态:它不在任何运行队列中。
- 条件:
关键点:对于一个特定的CPU和进程,状态一和状态二是互斥的。一个进程要么正在CPU上跑,要么就在队列里等,不可能同时在两个地方。
二、 代码逻辑详解
现在我们来看这段代码,它的目的是在修改进程 p 的调度属性之前,将其置于一个安全的、静止的状态。
queued = task_on_rq_queued(p);
running = task_current(rq, p);
if (queued)
dequeue_task(rq, p, queue_flags); // 如果在排队,就移出队列
if (running)
put_prev_task(rq, p); // 如果在运行,就从CPU上“放下”
这段代码处理了两种可能的情况:
情况 A:进程 p 正在队列中排队 (queued 为 true)
running必然为false。if (queued)条件成立,调用dequeue_task(rq, p, ...)。dequeue_task的作用是将进程p从它所属调度类的子队列(如CFS红黑树)中移除。if (running)条件不成立,put_prev_task不会被调用。- 结果:进程
p不再属于任何队列,进入“既不运行也不排队”的状态。
情况 B:进程 p 正在CPU上运行 (running 为 true)
queued必然为false。if (queued)条件不成立,dequeue_task不会被调用。if (running)条件成立,调用put_prev_task(rq, p)。put_prev_task的作用是为当前运行的进程p记账(例如,更新CFS进程的vruntime),然后将rq->curr置为NULL,表示CPU上现在没有正在运行的进程了。- 注意:对于CFS进程,
put_prev_task_fair内部可能会调用__enqueue_entity,但这只是更新它在队列中的位置,而不是将一个“不在队列中”的进程加入队列。它的前提是进程已经在队列中了。 - 结果:进程
p被从CPU上“放下”,但它仍然在子队列中(对于CFS)。它现在处于“在队列中排队”的状态。
三、 为什么需要这样做?
在修改一个进程的调度策略(如从 SCHED_NORMAL 改为 SCHED_FIFO)时,必须确保该进程在修改期间是“静止”的,不能在任何队列中,也不能在CPU上运行。
- 如果进程在队列中(情况A),我们必须先把它从旧的队列中移除,才能把它加入新的队列。
- 如果进程在运行(情况B),我们必须先把它从CPU上“放下”,让它回到队列中,然后才能将它从旧队列移除,再加入新队列。
所以,这段代码的作用是一个统一的“抓取”步骤:无论进程当前是在跑还是在等,都先把它“抓”到一个可以被修改的安全状态。
总结与类比
您可以把CPU和它的运行队列想象成一个单人游乐场和它的排队区。
- CPU:是正在玩游戏的玩家。
- 运行队列:是排队区。
- 进程
p:是一个想玩游戏的人。
这个人 p 只有两种可能:
- 正在玩游戏 (
running = true):他在CPU上,不在排队区。 - 在排队 (
queued = true):他在排队区,不在CPU上。
现在,游乐场管理员(内核)想把他从“普通玩家”升级为“VIP玩家”。管理员必须:
- 如果他正在玩,就等他玩完一个回合,然后对他说:“你先下来,我给你换个VIP牌子。” (
put_prev_task) - 如果他在排队,就直接把他从普通队伍里拉出来,说:“你先出队,我给你换个VIP牌子。” (
dequeue_task)
管理员绝不会傻到去排队区里找一个正在玩游戏的人,也不会去游戏区里找一个正在排队的人。这就是 queued 和 running 互斥的含义。
因此,dequeue_task 和 put_prev_task 是针对两种不同初始状态的预处理操作,它们是互斥执行的,共同为后续的修改操作(__setscheduler)准备了一个“干净”的进程。
851

被折叠的 条评论
为什么被折叠?



