scheduler

  • 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会更顺畅:

  1. 物理时间:CPU真实流逝的时间(比如jiffies、ns级时间戳),进程实际占用CPU的物理时长是真实消耗,但直接按物理时间调度会不公平(比如高优先级进程和低优先级进程同等对待)。
  2. 进程权重(weight):CFS用权重表示进程优先级(对应nice值),nice值越小(优先级越高),weight越大;反之则weight越小。核心规则:权重越大,应分配到更多CPU时间。

一、 为什么需要virtual time?(核心痛点)

如果不用virtual time,直接按物理时间调度会有2个问题:

  1. 无法体现优先级:高优先级和低优先级进程占用相同物理时间,不符合调度需求。
  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调度逻辑)

  1. 权重与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运行机会。
  2. virtual time是累计值:进程创建时virtual time初始化为0,每次占用CPU都会累加,让出CPU时保留当前值,下次唤醒后继续累计。
  3. 调度队列按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步就能看懂:

  1. 选下一个进程:CPU空闲时,直接选runqueue红黑树最左节点(vruntime最小的进程),切换到该进程运行。
  2. 运行中更新vruntime:进程运行时,内核按物理时间流逝,实时计算并累加其vruntime(按权重缩放)。
  3. 触发调度切换:当当前运行进程的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规则计算:

  1. 核心约束:最终A和B的vruntime要尽量相等(公平);
  2. 设A物理运行时间t₁,B物理运行时间t₂,t₁+t₂=20ns;
  3. 公平条件:t₁*1024/1024 = t₂*1024/1215t₁ = t₂*1024/1215
  4. 计算得:t₂≈11.0ns,t₁≈9.0ns。

👉 结果:高优先级的B,分配到更多物理时间(11ns vs 9ns),但两者的vruntime均≈9ns(持平),实现了“按权重公平分配”——这就是virtual time的核心价值。

七、 常见误区澄清

  1. 误区1:virtual time是“虚拟CPU的时间”?❌
    正解:是物理时间的加权映射,和虚拟CPU无关,是CFS的调度算法抽象。
  2. 误区2:vruntime越小,进程优先级越高?✅ 但要注意:睡眠唤醒的进程会调整vruntime,不是绝对的。
  3. 误区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了吗?

核心结论先行

queuedrunning 是两个互斥的状态。一个进程在某个CPU上,不可能同时处于“正在运行”和“在队列中排队”两种状态

因此,if (queued)if (running) 这两个条件永远只会有一个为真dequeue_taskput_prev_task 永远只会有一个被调用

一、 进程在CPU上的三种状态

要理解这个逻辑,我们首先要明确一个进程在某个CPU上可能存在的三种状态:

  1. 状态一:正在运行 (Running)

    • 条件task_current(rq, p)true
    • 含义:进程 p 是当前CPU上正在执行的进程。它的 task_struct 地址与 rq->curr 相等。
    • 队列状态:它不在任何运行队列的子队列(如CFS的红黑树、RT的优先级队列)中。它是“独占”CPU的。
  2. 状态二:在队列中排队 (Queued)

    • 条件task_on_rq_queued(p)true
    • 含义:进程 p 的状态是 TASK_RUNNING,并且它正在某个运行队列的子队列中等待被调度执行。
    • 队列状态:它子队列中。
  3. 状态三:既不运行也不排队 (Not on rq)

    • 条件task_on_rq(p)false
    • 含义:进程 p 处于睡眠状态(TASK_INTERRUPTIBLETASK_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 正在队列中排队 (queuedtrue)

  • running 必然为 false
  • if (queued) 条件成立,调用 dequeue_task(rq, p, ...)
  • dequeue_task 的作用是将进程 p 从它所属调度类的子队列(如CFS红黑树)中移除
  • if (running) 条件不成立,put_prev_task 不会被调用。
  • 结果:进程 p 不再属于任何队列,进入“既不运行也不排队”的状态。

情况 B:进程 p 正在CPU上运行 (runningtrue)

  • 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 只有两种可能:

  1. 正在玩游戏 (running = true):他在CPU上,不在排队区。
  2. 在排队 (queued = true):他在排队区,不在CPU上。

现在,游乐场管理员(内核)想把他从“普通玩家”升级为“VIP玩家”。管理员必须:

  • 如果他正在玩,就等他玩完一个回合,然后对他说:“你先下来,我给你换个VIP牌子。” (put_prev_task)
  • 如果他在排队,就直接把他从普通队伍里拉出来,说:“你先出队,我给你换个VIP牌子。” (dequeue_task)

管理员绝不会傻到去排队区里找一个正在玩游戏的人,也不会去游戏区里找一个正在排队的人。这就是 queuedrunning 互斥的含义。

因此,dequeue_taskput_prev_task 是针对两种不同初始状态的预处理操作,它们是互斥执行的,共同为后续的修改操作(__setscheduler)准备了一个“干净”的进程。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值