3. 虚拟运行时(vruntime)详解

虚拟运行时(vruntime)是 CFS 调度器的核心抽象。CFS 不再使用固定时间片,而是通过 vruntime 衡量每个进程的“虚拟执行时间”,并始终选择 vruntime 最小的进程运行。本节深入解析 vruntime 的计算公式、nice 值与权重的映射关系,以及内核中关键的权重表。

3.1 vruntime 的计算公式

vruntime 的本质是进程实际运行时间经过权重归一化后的结果。其核心计算公式如下:

vruntime += 实际运行时间 * (NICE_0_LOAD / 进程权重)

其中:

  • 实际运行时间:进程在 CPU 上真实执行的时间(单位:ns)。
  • NICE_0_LOAD:nice 值为 0 时的基准权重(内核定义为 1024)。
  • 进程权重:由进程的 nice 值通过权重表映射得到。

公式的直观含义:

  • 权重越大的进程(nice 值越小),分母越大,vruntime 增长越慢,从而获得更多 CPU 时间。
  • 权重越小的进程(nice 值越大),分母越小,vruntime 增长越快,CPU 时间份额减少。
  • 当所有进程的 nice 值都为 0 时,权重均为 1024,vruntime 等于实际运行时间,实现完全公平。
关键理解: vruntime 不是真实时间,而是经过权重调整后的“虚拟时间”。CFS 通过比较 vruntime 来决定下一个运行谁,而不是通过固定时间片轮转。
vruntime 计算流程 实际运行时间 (Δ t) 进程权重 (weight) NICE_0_LOAD (1024) Δ t × NICE_0_LOAD vruntime 增量 更新 vruntime 权重越大,vruntime 增长越慢

3.2 nice 值与权重映射

Linux 内核使用 nice 值(-20 到 19)来表示进程的优先级。CFS 将每个 nice 值映射到一个权重值,权重决定了进程的 CPU 时间份额。nice 值与权重的映射关系如下:

nice 值 权重 相对 nice 0 的 CPU 占比 说明
-20 88761 约 86.7 倍 最高优先级,几乎独占 CPU
-10 29216 约 28.5 倍 显著高于默认
-5 9548 约 9.3 倍 明显高于默认
0 1024 1.0 倍 基准权重(NICE_0_LOAD)
5 335 约 0.33 倍 明显低于默认
10 110 约 0.11 倍 显著低于默认
19 15 约 0.015 倍 最低优先级,几乎不获得 CPU
重要规律: nice 值每相差 1,权重变化约 1.25 倍(即 10% 的 CPU 时间差异)。nice 值从 0 降到 -1,CPU 时间份额增加约 10%;从 0 升到 1,份额减少约 10%。这个比例在 nice 值 0 附近最精确,越往两端非线性越明显。
nice 值与权重映射关系 nice 值 -20 -10 -5 0 5 10 19 88761 29216 9548 1024 335 110 15 nice 值越低,权重越高,vruntime 增长越慢,获得更多 CPU 时间

3.3 权重表(sched_prio_to_weight)解析

内核使用一个预定义的权重表 sched_prio_to_weight 来快速查找 nice 值对应的权重,避免运行时计算。该表定义在 kernel/sched/core.c 中:

/*
 * Nice 值从 -20 到 19 对应的权重数组
 * 数组索引 0 对应 nice = -20,索引 39 对应 nice = 19
 */
static const int sched_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,
};

权重表的设计特点:

  • 数组长度 40:覆盖 nice 值 -20 到 19 共 40 个优先级。
  • 索引映射:索引 i 对应 nice 值 i - 20。例如 nice 0 对应索引 20,权重为 1024。
  • 近似等比数列:相邻 nice 值的权重比约为 1.25(即 5/4),保证每级 nice 值带来约 10% 的 CPU 时间差异。
  • 对称性:nice 值从 0 到 19 的权重下降速度与从 0 到 -20 的上升速度对称(取倒数关系)。
性能注意事项: 权重表是静态常量,存储在只读数据段。每次调度时,内核通过 nice 值计算数组索引直接查表,时间复杂度为 O(1)。不要尝试在运行时修改此表,这会导致调度行为不可预测。
sched_prio_to_weight 权重表结构 sched_prio_to_weight[40] 索引 0 (nice -20) ... 索引 20 (nice 0) ... 索引 39 (nice 19) 索引 0 nice = -20 索引 20 nice = 0 索引 39 nice = 19 88761 1024 15 权重表关键特征: • 数组长度 40,覆盖 nice 值 -20 到 19 • 索引 i 对应 nice 值 i - 20(索引 20 → nice 0 → 权重 1024) • 相邻 nice 值权重比 ≈ 1.25,保证每级约 10% 的 CPU 时间差异

3.4 权重与 vruntime 的协同工作

CFS 调度器通过以下步骤将权重和 vruntime 结合起来实现公平调度:

  1. 进程创建:新进程的 vruntime 初始化为当前运行队列的最小 vruntime(防止新进程抢占过多 CPU)。
  2. 时钟中断:每次时钟中断(通常 1ms 或 4ms),更新当前进程的 vruntime。
  3. 权重查表:根据进程的 nice 值,从 sched_prio_to_weight 表中获取权重。
  4. vruntime 更新:使用公式 vruntime += Δt * 1024 / weight 累加 vruntime。
  5. 调度决策:选择红黑树中最左节点(vruntime 最小)的进程运行。
生产环境调优启示: 在混合负载场景下,合理设置进程的 nice 值可以精确控制 CPU 分配比例。例如,将数据库进程设为 nice -5(权重 9548),后台日志进程设为 nice 10(权重 110),数据库将获得约 87 倍的 CPU 优先级。但需注意,过度使用低 nice 值可能导致交互式进程响应延迟。

3.5 权重与组调度(CFS Group Scheduling)

CFS 还支持组调度,允许对进程组(如 cgroup)进行整体权重控制。组调度的权重计算方式为:

组权重 = 组内所有进程权重之和
组 vruntime 增量 = Δt * 1024 / 组权重

组调度使得多个进程可以作为一个整体参与 CPU 竞争,适用于容器化环境和多租户系统。组权重同样使用 sched_prio_to_weight 表进行映射,但组的 nice 值通过 cpu.shares 接口配置(默认 1024,对应 nice 0)。

调度层级 权重来源 vruntime 计算 典型应用
单进程 nice 值 → 权重表 vruntime += Δt * 1024 / weight 普通进程优先级控制
进程组(cgroup) cpu.shares → 权重表 组 vruntime += Δt * 1024 / 组权重 容器 CPU 资源限制
多级嵌套组 逐层累加权重 每层独立计算 vruntime 复杂多租户资源隔离
实践建议: 在调优生产环境时,优先使用 cgroup 的 cpu.shares 进行组级别权重分配,而不是直接修改单个进程的 nice 值。组调度提供了更好的隔离性和可管理性,尤其适合 Kubernetes 等容器编排平台。