小米玄戒O1架构深度解析(二):多核任务调度策略详解

上篇文章中,就提到了小米玄戒O1的多核任务调度策略,但讲得不够详细,尤其是对于完全公平调度器能效感知调度 ,这次我们就深度剖析一下这两种调度策略。

目录

  • [1. 完全公平调度器(CFS)](#1. 完全公平调度器(CFS))
    • [1.1 完全公平调度基本原理](#1.1 完全公平调度基本原理)
    • [1.2 完全公平调度基本流程](#1.2 完全公平调度基本流程)
    • [1.3 完全公平调度中的负载均衡](#1.3 完全公平调度中的负载均衡)
      • [1.3.1 负载均衡原理与触发机制](#1.3.1 负载均衡原理与触发机制)
      • [1.3.2 负载均衡中的任务迁移机制](#1.3.2 负载均衡中的任务迁移机制)
      • [1.3.3 负载均衡的特点与意义](#1.3.3 负载均衡的特点与意义)
  • [2. 能效感知调度(EAS)](#2. 能效感知调度(EAS))
    • [2.1 核心目标与技术背景](#2.1 核心目标与技术背景)
      • [2.1.1 🚩核心目标](#2.1.1 🚩核心目标)
      • [2.1.2 💫技术背景](#2.1.2 💫技术背景)
    • [2.2 基本原理](#2.2 基本原理)
      • [2.2.1 能量建模](#2.2.1 能量建模)
      • [2.2.2 任务放置决策](#2.2.2 任务放置决策)
      • [2.2.3 频率选择](#2.2.3 频率选择)
      • [2.2.4 唤醒决策](#2.2.4 唤醒决策)
      • [2.2.5 负载均衡](#2.2.5 负载均衡)
    • [2.3 能效度量](#2.3 能效度量)
      • [2.3.1 能效计算](#2.3.1 能效计算)
      • [2.3.1 能效计算中的延迟惩罚](#2.3.1 能效计算中的延迟惩罚)
      • [2.3.3 能效度量的实际案例](#2.3.3 能效度量的实际案例)
  • [3. 完全公平调度 VS 能效感知调度](#3. 完全公平调度 VS 能效感知调度)
  • [4. 后记](#4. 后记)

1. 完全公平调度器(CFS)

完全公平调度器(Completely Fair Scheduler, CFS)是Linux内核中的一种进程调度算法,它通过红黑树数据结构来实现公平的CPU时间分配。

1.1 完全公平调度基本原理

CFS的核心思想是:给所有可运行进程提供公平的CPU时间份额。它不像传统调度器那样使用时间片的概念,而是基于以下原则:

  1. 虚拟运行时间(vruntime):记录每个进程已经获得的CPU时间,CFS总是选择vruntime最小的进程运行。
  2. 权重(nice值):不同优先级的进程获得不同的CPU时间权重。
  3. 红黑树:用于高效地找到vruntime最小的进程。

在多核系统中,每个CPU核心都有自己的运行队列(run queue),包含自己的红黑树和调度状态。这样可以减少核心间的竞争,提高并行效率。

为什么要使用红黑树?

CFS使用红黑树是因为:

  1. 高效查找:总能以O(log n)时间找到vruntime最小的进程(最左侧节点)
  2. 平衡性好:红黑树是自平衡的,插入删除操作后能保持较好的平衡
  3. 适合频繁更新:进程的vruntime经常变化,需要高效地重新插入

虚拟运行时间(vruntime)的计算(注意这里是delta值,也就是每次叠加的虚拟运行时间)

c 复制代码
delta_vruntime = (delta_exec * NICE_0_LOAD) / (task_weight * (cpu_capacity / max_capacity))

这样一来,实时的虚拟运行时间是这样的

c 复制代码
vruntime += delta_vruntime

其中,NICE_0_LOAD是基准权重(默认值为 1024)。
delta_exec:任务实际运行的时间(物理时间,单位ns)。
cpu_capacity: 当前CPU核心的计算能力。
max_capacity:系统中最强核心的容量(通常为 1024)。

权重task_weight是通过nice值转化而来:

例如,nice 值为 - 10 的进程权重约为 9548,而 nice 值为 19 的进程权重仅为 15。权重越高,vruntime 增长越慢,进程获得的 CPU 时间越多。

在实际的开发中,这种非线性运算相对来说是很消耗计算资源的,一般都会将这种映射保存到一个数组中,随用随取。

c 复制代码
// include/linux/sched/prio.h
static const int prio_to_weight[40] = {
    /* -20 */     88761,     71755,     56483,     46273,     36291,
    /* -15 */     29154,     23254,     18705,     14949,     11916,
    /* -10 */      9548,      7620,      6100,      4904,      3906,
    /*  -5 */      3121,      2501,      1991,      1586,      1277,
    /*   0 */      1024,       820,       655,       526,       423,
    /*   5 */       335,       272,       215,       172,       137,
    /*  10 */       110,        87,        70,        56,        45,
    /*  15 */        36,        29,        23,        18,        15,
};

CFS维护一个红黑树(cfs_rq),按 vruntime 升序排列所有可运行进程。每次调度时,选择 vruntime最小的进程运行,确保 "饥饿" 进程优先获得资源。例如,若进程 A 的 vruntime 为 10ms,进程 B 为 20ms,则 A 会优先被调度。

那么,权重是根据nice值来的,那么nice值又是哪里来的?

nice 值的完整生命周期应该是这样的:

  • 初始赋值:

    用户显式指定(nice/renice)或继承父进程值。

    系统根据进程类型设置默认值(如交互式进程为 - 5,批处理为 10)。

  • 内核处理:

    通过set_load_weight()将 nice 值转换为权重(影响vruntime增长速度)。

    将 nice 值映射为静态优先级(static_prio = 120 + nice×2)。

  • 动态调整:

    用户通过setpriority()系统调用修改 nice 值。

    内核根据进程行为自动微调(通过update_curr()接口)如 I/O 密集型进程优先级提升。

  • 调度决策:

    实时进程(SCHED_FIFO/SCHED_RR)优先于普通进程(SCHED_NORMAL)。

    普通进程在 CFS 队列中按vruntime排序,权重高的进程(nice 值低)优先调度。

严格意义上来说,内核并未修改任务的nice值,而是直接修改了任务的优先级。从这点来说,nice值是人为设定的。

1.2 完全公平调度基本流程

玄戒O1共有10个核,这样讲起来会非常复杂,为方便理解,我们简化整个架构,基本原理是不变的。

我们模拟一个具有2个大核(高性能)和2个小核(节能)的ARM架构系统,基本配置如下:

大核:计算能力为1024,调度队列为rq_big0和rq_big1

小核:计算能力为512,调度队列为rq_little0和rq_little1

任务集:6个任务(T1-T6),初始vruntime=0,权重如下:

T1(nice=0), T2(nice=0), T3(nice=0), T4(nice=0) T5(nice=0), T6(nice=0),

这6个任务的类型分别如下:

T1, T2:CPU密集型(持续运行)

T3, T4:I/O密集型(频繁睡眠)

T5, T6:后台任务(低优先级)

初始任务分配(假设没有任务亲和性限制)

各个核心维护的红黑树如下:

为了便于理解,我在即将处理的任务上方加上了一个解开锁的标志:

5ns

大核0:选择T1(0)运行 → vruntime += 5 → T1(5)

大核1:选择T3(0)运行 → vruntime += 5 → T3(5)

小核0:选择T5(0)运行 → vruntime += 10 → T5(10)

小核1:选择T6(0)运行 → vruntime += 10 → T6(10)

此时的红黑树变成了这样:

10ns

大核0:选择T2(0)运行 → vruntime += 5 → T2(5)

大核1:选择T4(0)运行 → vruntime += 5 → T4(5)

小核0:选择T5(10)运行 → vruntime += 10 → T5(20)

小核1:选择T6(10)运行 → vruntime += 10 → T6(20)

T3, T4是I/O密集型任务,会出现频繁的睡眠现象,那么我们假定T3(5)此时发生了睡眠,此时将其从红黑树中移除,直至下次唤醒。

此时的红黑树变成了这样:

15ns后(负载均衡迁移)

此时,由于T5任务的vruntime为30,触发了负载均衡,因此从小核0迁移到大核0中去处理。

同样地,T6任务的vruntime为30,触发了负载均衡,因此从小核1迁移到大核1中去处理。

20ns后(T3任务唤醒)

假设此时任务T3被重新唤醒,以前是在大核1中处理,现在我们仍然交由它进行处理。同时新进来了需要处理的新任务T7(0),当前两个小核是空闲的,我们将其放在小核0中进行处理。

25ns

继续执行当前需要执行的任务即可

1.3 完全公平调度中的负载均衡

在 CFS 调度器中,负载均衡触发与任务迁移的核心逻辑与vruntime(虚拟运行时间)和处理器能力的动态关系密切相关。以下从原理、迁移机制和特点三方面详细解释:

1.3.1 负载均衡原理与触发机制

vruntime 是 CFS 衡量任务 "公平性" 的关键指标,计算公式为:

bash 复制代码
vruntime = 实际运行时间 × (NICE值对应的权重 / 系统基准权重)

但在多核心场景下,还需考虑处理器能力的影响:

处理能力强的核心(如大核)在相同物理时间内,任务的 vruntime 增长更慢(因为等效于 "时间被压缩");

处理能力弱的核心(如小核)在相同物理时间内,任务的 vruntime 增长更快。

假设大核处理能力是小核的 2 倍(如题目中设定),则:

大核上运行 1ms,等效于小核上运行 2ms;

大核上任务的 vruntime 增长速度是小核的 1/2。

结构不同的核心,处理效率也会有所不同,这样经过相同的时间,不同核心中的任务就好像被"区别对待"了,所以当差距太大的时候,就需要从小核迁移到大核,进行一定的补偿。

1.3.2 负载均衡中的任务迁移机制

任务选择

优先迁移小核红黑树中vruntime 最高的任务,因为它最需要更多 CPU 资源来降低 vruntime

权重校准

在上述的例子中,为了方便理解,在将T5迁移到大核后,并没有进行"时间校准",这样会导致一个问题,就是T5可能会在较长时间内得不到处理。所以我们需要再任务迁移后进行时间校准,也就是与大核中原有的任务把时间线拉平。

权重比例校准的公式稍微复杂一些:

c 复制代码
校准比例 = src_weight / dst_weight = 512/1024 = 0.5
T5校准后vruntime = (30 - src_min) × 0.5 + dst_min
               ≈ 15 + dst_min  (假设min_vruntime差异可忽略)

大概相当于将T5的vruntime 减少到了原来的一半,也就是15。

插入大核红黑树

然后根据vruntime大小,将其插入大核的红黑树中即可。

1.3.3 负载均衡的特点与意义

  • 校准即公平:vruntime转换不是简单的数值搬运,而是通过权重比例进行时间维度转换
  • 未运行却接近:因校准公式主动缩放vruntime值,使迁移后任务立即适配新CPU的时间体系
  • 短期牺牲换长期收益:短暂等待后,任务将在高算力CPU上更高效地追赶进度

这种设计确保了 跨异构CPU的全局公平性,即使迁移初期有调度延迟,长期看仍是最优策略。


2. 能效感知调度(EAS)

完全公平和能效感知这两种调度器策略代表了现代操作系统(尤其是Linux内核)在处理任务时追求的不同核心目标:一个是公平性 优先,另一个是能效优先。

2.1 核心目标与技术背景

2.1.1 🚩核心目标

  • 在保证系统性能和响应性的前提下,最小化整个系统的能量消耗
  • 利用现代处理器的异构多核架构动态电压频率调节技术来节省功耗。

2.1.2 💫技术背景

异构多核系统: 现代移动设备CPU通常包含不同类型的内核:

"大核": 高性能核心,频率高,单线程性能强,但功耗也高(尤其是高频时功耗增长非线性)。

"小核": 高能效核心,频率较低,性能较弱,但单位性能功耗非常低,空闲功耗也极低。

动态电压频率调节: CPU可以根据负载动态调整工作电压和频率。低频低压时功耗显著低于高频高压。

CPU 空闲状态: CPU核心可以进入不同的休眠状态,关闭部分或全部电路以节省功耗,但唤醒需要时间和能量。(从上篇文章我们可以知道,小米玄戒O1的CPU核心,并非每个都可以进入睡眠状态,对于功耗小且需要处理"日常"事务的A520核心来说,就无需睡眠)。

2.2 基本原理

2.2.1 能量建模

EAS的核心是建立一个系统能量模型。这个模型描述了:

不同CPU核心在不同工作频率下执行任务时的功耗

不同CPU核心在不同工作频率下执行任务的性能/能力

核心在不同休眠状态下的功耗和进入/退出该状态的延迟与能耗。

以下图为例,就是某CPU核心的频率-功耗曲线。

这样就同时涵盖了CPU的暂态稳态性能。

2.2.2 任务放置决策

当有新任务唤醒或需要迁移时,EAS 不仅仅看哪个CPU最空闲,而是预测将任务放在哪个CPU核心上运行,以及该核心运行在什么频率下,能使完成该任务所需的"能量延迟积"最小化或满足性能约束下的能耗最低。从这点就可以看出来,能效感知调度明显要更加复杂,且更加适用于手机处理器,毕竟对于移动设备来说,续航能力🚗直接决定了使用感受。

✔️偏好小核: 对于轻量级、后台或不紧急的任务,EAS 会优先将其调度到能效核心上运行。因为这些任务在小核上就能满足性能要求,且功耗远低于在大核上运行。

✔️适时用大核: 对于计算密集型、对延迟敏感或需要高吞吐量的任务,EAS 会将其调度到性能核心上运行,以确保性能达标。同时,它会尽量让大核运行在能满足任务需求的最低必要频率上。

✔️集群感知 : 考虑到CPU拓扑(如哪些核心共享L2缓存、电源域)。将相关联的任务调度到共享缓存的集群内核心上运行,可以减少缓存失效,提高性能并间接节能。避免不必要的跨集群迁移

2.2.3 频率选择

EAS 与 schedutil CPU频率调控器深度集成(这部分内容也非常多,后面有时间可以单独写一篇💪)。

传统的调控器可能只根据单个CPU的利用率调频。schedutil结合EAS的能量模型信息,做出更全局、更符合能效目标的频率决策。

例如,如果一个任务在小核上运行需要80%的利用率才能满足要求,而在大核上只需20%的利用率,EAS 结合 schedutil 可能会选择将其放在大核上运行在较低频率,因为这比让小核跑满频更省电(因为大核低频的单位性能功耗可能低于小核高频)。

2.2.4 唤醒决策

当任务从睡眠中被唤醒时,EAS 会评估:

哪个CPU核心最适合运行它(考虑能效模型、亲和性、缓存热度)。

唤醒该核心(如果目标核心在休眠)的能耗开销是否值得。

可能选择唤醒一个空闲的小核来运行轻量任务,而不是唤醒一个休眠的大核。

2.2.5 负载均衡

传统的负载均衡主要追求各CPU核心的负载均匀。EAS的负载均衡更注重能效均衡。

它可能允许小核集群负载较高(因为它们在负载下的能效依然好),而让大核集群保持较低负载甚至部分核心休眠,只要整体系统性能和响应性达标。

迁移任务时,优先考虑迁移到能效更高的核心集群,并评估迁移带来的能耗收益是否大于迁移操作本身的能耗开销。

2.3 能效度量

2.3.1 能效计算

能效感知调度的重要依据,就是CPU能耗,CPU能耗可根据下面的公式进行计算:

稳态运行能耗 :这个开头就提到了,直接从曲线就可以得到。
唤醒能耗 :同样可以通过曲线得到。
延迟惩罚 :用于衡量因延迟满足任务需求而导致的用户体验损失或系统效率下降。其本质是将"时间敏感度"转化为可计算的能耗等价量,使调度器能在性能与能效间做出数学上的最优决策

2.3.1 能效计算中的延迟惩罚

核心思想:时间价值量化

若为省电而延迟处理用户交互(如点击响应),会导致卡顿,传统调度器无法量化"卡顿"的成本。延迟惩罚将延迟时间映射为虚拟"能耗惩罚值",与其他能耗(如CPU功耗)统一量纲比较。

这个公式与上面的公式几乎是一样的,只是多了个延迟惩罚的敏感因子α。

公式我们知道了,那么延迟惩罚到底是怎么算出来的呢?

延迟惩罚总共有3种度量方法,分别是:

  1. 基于任务类型的分类度量

很简单,就是根据不同的任务类型,使用不同的惩罚函数,可以看到用户交互的惩罚函数是非线性的,超过阈值t

max后运算结果会指数上升。

  1. 基于硬件事件的动态度量

基于硬件事件的动态度量,指的是用性能监测单元(PMU)提供的数据计算延迟惩罚,延迟一般会有两种来源,分别是L2缓存未命中和前端停顿,当然,这个前端指的是CPU流水线的前端,前端负责获取和解码将要执行的指令。基于硬件事件的动态度量公式如下:

从上面的公式也可以看出来,惩罚来自缓存未命中和分支预测失败,毕竟分支预测失败是导致前端停顿的一个重要原因(关于分支预测我们会在下篇文章中详细讲解)因此公式中并未将其他前端停顿的因素包含进去。

至于小米玄戒O1这款SOC到底用的哪种度量方法,从我的角度来说,至少用了其中一种。而且很明显,这2种方法不会完全独立开来,硬件与软件是协同作用的。

2.3.3 能效度量的实际案例

假设有个用户点击图标启动APP这样一个场景,很明显是对延迟非常敏感的一个任务。基本属性配置如下:

类型:最高延迟敏感级📈

最大容忍延迟:80ms

权重系数:Kui=12

决策选项:

1️⃣唤醒小核(唤醒延迟 20ms,运行耗时 60ms → 总延迟 80ms)。

实际能耗:小核唤醒(5mJ) + 运行(1.2GHz×60ms=8mJ) = 13mJ

延迟惩罚:P**latency =12*(80-80)^2 = 0

总成本:13mJ

2️⃣唤醒大核(唤醒延迟 50ms,运行耗时 30ms → 总延迟 80ms)。

实际能耗:大核唤醒(20mJ) + 运行(2.0GHz×30ms=25mJ) = 45mJ

延迟惩罚:同样是0

总成本:45mJ

3️⃣等待小核空闲(预计延迟 150ms)。

实际能耗:小核唤醒(0mJ) + 运行(1.2GHz×60ms=8mJ) = 8mJ

延迟惩罚:P**latency =12*(150-80)^2 = 58800mJ

总成本:达到了惊人的58808mJ💥

所以最终选择方案1进行任务调度。

3. 完全公平调度 VS 能效感知调度

经比较,这两种调度算法有以下的一些不同。

完全公平调度 (CFS) 是调度器设计的经典范式,专注于在时间维度上公平地分配CPU资源。它在同构系统和强调公平性的场景中表现出色,但对现代异构多核处理器的能效潜力利用不足。

能效感知调度 (EAS) 代表了调度器设计的进化方向,将能量消耗提升为核心优化目标。它深度理解硬件特性(大小核、DVFS、空闲状态),通过联合优化任务放置和CPU频率,在满足性能要求的前提下,主动寻找能耗最低的执行路径。它是解决移动设备和数据中心能效瓶颈的关键技术。

4. 后记

小米玄戒O1架构的具体调度策略我们不得而知,以上仅仅是依据目前公开资料做的合理推测。

接下来的一篇帖子想更新一下分支预测方面的内容,毕竟对于十几级流水线深度的超大核来说,分支预测的准确度直接影响了处理任务的效率。敬请关注!😆

参考资料:

  1. 哔哩哔哩up主老石谈芯【全网最深度解读】小米玄戒O1:一场输不起的芯片战争
  2. 哔哩哔哩up主轩辕的编程宇宙Linux如何调度进程?大学老师不讲的,看完动画秒懂!
  3. 哔哩哔哩up主5c477超频预备知识 性能和功耗的关系 基础频率是怎么来的【爱玩数码 第三期】
  4. 红黑树在线生成网站Red/Black Tree Visualization

"潮平两岸阔,风正一帆悬",我们正处在科技飞速发展的时代,愿我们专注技术,顺势启航!⛵️

相关推荐
小李飞刀李寻欢6 分钟前
使用kubeadm部署Kubernetes(k8s)集群的步骤
linux·服务器·ubuntu·kubernetes·k8s
运维成长记17 分钟前
阿里云实践创建实例步骤
linux·运维·服务器·阿里云·云计算
THe CHallEnge of THe BrAve32 分钟前
Linux检验库是否安装成功
linux·运维·服务器
前端付豪43 分钟前
汇丰登录风控体系拆解:一次 FaceID 被模拟攻击的调查纪实
前端·后端·架构
敲代码的剑缘一心1 小时前
手把手教你学会写 Gradle 插件
android·gradle
掘金-我是哪吒1 小时前
分布式微服务系统架构第146集:JavaPlus技术文档平台
分布式·微服务·云原生·架构·系统架构
电报号dapp1191 小时前
中心化交易所(CEX)架构:高并发撮合引擎与合规安全体系
安全·架构·去中心化·区块链·智能合约
算家计算1 小时前
告别复杂文档解析噩梦!MonkeyOCR 本地部署教程:支持公式/表格多元素结构化
linux·人工智能·开源
青蛙娃娃1 小时前
漫画Android:动画是如何实现的?
android·android studio
aningxiaoxixi2 小时前
android 之 CALL
android