虚拟运行时间(vruntime)概念
虚拟运行时间(vruntime)是 CFS 调度器的核心抽象,它将不同优先级的进程统一到一个公平的时间度量上。理解 vruntime 的定义及其与物理时间的转换关系,是掌握 CFS 调度原理的关键。
2.1 vruntime 定义
vruntime 是 CFS 为每个进程维护的一个虚拟时间戳,表示该进程在 CPU 上“应该”运行的时间量。CFS 通过红黑树按 vruntime 排序,总是选择 vruntime 最小的进程运行,从而实现完全公平。
核心原则: 所有进程的 vruntime 增长速率不同,但经过权重归一化后,它们在红黑树中的位置反映了“相对公平”的调度顺序。
| 概念 | 说明 | 关键特性 |
|---|---|---|
| vruntime | 进程的虚拟运行时间,单位纳秒 | 值越小,越优先被调度 |
| 物理时间 | 进程实际占用 CPU 的墙上时间 | 由硬件定时器测量 |
| 权重(weight) | 进程优先级对应的数值,nice 值决定 | 权重越大,vruntime 增长越慢 |
| 调度周期 | CFS 一次调度轮转的总时间 | 默认约 6ms~48ms 动态调整 |
面试高频点: vruntime 不是物理时间,而是经过权重归一化后的“公平时间”。两个权重不同的进程运行相同的物理时间,权重大的进程 vruntime 增长更慢,因此能获得更多 CPU 时间。
2.2 物理时间与虚拟时间的转换公式
CFS 在每次时钟 tick 或进程切换时,根据当前进程的权重将物理运行时间转换为 vruntime 增量。转换公式是 CFS 调度算法的数学基础。
2.2.1 标准转换公式
vruntime_delta = 物理运行时间 × (NICE_0_LOAD / 进程权重)
其中:
- 物理运行时间:进程本次实际占用 CPU 的时间(单位:ns)
- NICE_0_LOAD:nice 值为 0 时的标准权重(内核定义为 1024)
- 进程权重:由进程的 nice 值查表得到(见下表)
2.2.2 nice 值与权重对照表
| nice 值 | 权重(weight) | 相对 nice=0 的 CPU 占比 |
|---|---|---|
| -20 | 88761 | 约 86.7 倍 |
| -10 | 29216 | 约 28.5 倍 |
| -5 | 9548 | 约 9.3 倍 |
| 0 | 1024 | 1.0 倍(基准) |
| 5 | 335 | 约 0.33 倍 |
| 10 | 110 | 约 0.11 倍 |
| 19 | 15 | 约 0.015 倍 |
注意: 权重值在 Linux 内核中通过
sched_prio_to_weight 数组预计算,避免运行时浮点运算。实际转换使用定点数乘法与移位操作,保证性能。
2.2.3 转换示例
假设两个进程 A 和 B 同时运行 10ms 物理时间:
- 进程 A:nice = 0,权重 = 1024
- 进程 B:nice = 5,权重 = 335
进程 A 的 vruntime 增量 = 10ms × (1024 / 1024) = 10ms
进程 B 的 vruntime 增量 = 10ms × (1024 / 335) ≈ 30.57ms
结果:权重较小的进程 B 的 vruntime 增长更快,在红黑树中会向右移动,让出 CPU 给权重更大的进程 A。
2.3 vruntime 更新流程
CFS 在以下时机更新 vruntime:
- 时钟 tick 中断:周期性更新当前运行进程的 vruntime
- 进程切换:在 schedule() 调用时更新被换出进程的 vruntime
- 唤醒进程:被唤醒的进程可能获得 vruntime 补偿(避免被饿死)
2.4 vruntime 与红黑树的关系
下图展示了 vruntime 如何驱动 CFS 红黑树的调度决策:
2.5 关键代码实现
Linux 内核中 vruntime 更新的核心函数位于 kernel/sched/fair.c:
/*
* 更新进程的 vruntime
* @se: 调度实体
* @delta_exec: 本次运行的物理时间增量
*/
static void update_curr(struct cfs_rq *cfs_rq)
{
struct sched_entity *curr = cfs_rq->curr;
u64 delta_exec;
if (unlikely(!curr))
return;
delta_exec = curr->exec_start - rq_clock_task(rq_of(cfs_rq));
if (unlikely((s64)delta_exec <= 0))
return;
curr->exec_start = rq_clock_task(rq_of(cfs_rq));
/* 核心转换:物理时间 → vruntime */
curr->vruntime += calc_delta_fair(delta_exec, curr);
}
/*
* 权重归一化计算
* @delta: 物理时间增量
* @se: 调度实体(包含权重信息)
*/
static inline u64 calc_delta_fair(u64 delta, struct sched_entity *se)
{
if (unlikely(se->load.weight != NICE_0_LOAD))
delta = __calc_delta(delta, NICE_0_LOAD, &se->load);
return delta;
}
面试追问点: 为什么 CFS 不直接使用物理时间排序?
答:因为不同优先级的进程对 CPU 时间的“需求”不同。权重大的进程应该获得更多 CPU 时间,如果直接用物理时间排序,高优先级进程会很快被低优先级进程抢占,无法体现公平。vruntime 通过权重归一化,让所有进程在同一个公平尺度上竞争。
答:因为不同优先级的进程对 CPU 时间的“需求”不同。权重大的进程应该获得更多 CPU 时间,如果直接用物理时间排序,高优先级进程会很快被低优先级进程抢占,无法体现公平。vruntime 通过权重归一化,让所有进程在同一个公平尺度上竞争。
2.6 常见面试问题
- Q:vruntime 会溢出吗? A:内核使用 64 位无符号整数,溢出需要数百年,实际不会发生。但 CFS 在红黑树比较时使用
s64有符号差值,避免回绕问题。 - Q:新进程的 vruntime 初始值是多少? A:新进程的 vruntime 初始化为当前红黑树中最左节点的 vruntime 值,避免新进程抢占过多 CPU。
- Q:休眠进程唤醒后 vruntime 如何处理? A:休眠进程的 vruntime 会进行补偿(减去休眠时间),使其在唤醒后能较快获得 CPU,但不会超过调度周期的限制。