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 来决定下一个运行谁,而不是通过固定时间片轮转。
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 附近最精确,越往两端非线性越明显。
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)。不要尝试在运行时修改此表,这会导致调度行为不可预测。
3.4 权重与 vruntime 的协同工作
CFS 调度器通过以下步骤将权重和 vruntime 结合起来实现公平调度:
- 进程创建:新进程的 vruntime 初始化为当前运行队列的最小 vruntime(防止新进程抢占过多 CPU)。
- 时钟中断:每次时钟中断(通常 1ms 或 4ms),更新当前进程的 vruntime。
- 权重查表:根据进程的 nice 值,从
sched_prio_to_weight表中获取权重。 - vruntime 更新:使用公式
vruntime += Δt * 1024 / weight累加 vruntime。 - 调度决策:选择红黑树中最左节点(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 等容器编排平台。