3. vruntime 定义:虚拟运行时间的计算公式、nice 值与权重映射表
在 CFS 调度器中,虚拟运行时间(vruntime)是核心度量指标。它并非真实的时间,而是经过权重归一化后的“调度账本”,用于公平地决定下一个被调度执行的进程。CFS 通过维护一棵红黑树,始终选择 vruntime 最小的进程运行,从而实现“完全公平”。
3.1 虚拟运行时间计算公式
vruntime 的更新发生在每次时钟中断(tick)或进程状态切换时。其基本计算公式如下:
vruntime += delta_exec * (NICE_0_LOAD / se->load.weight)
其中:
- delta_exec:当前进程实际运行的物理时间(单位:ns)。
- NICE_0_LOAD:nice 值为 0 时的基准权重(内核中定义为 1024)。
- se->load.weight:当前进程调度实体(sched_entity)的权重,由进程的 nice 值映射得到。
从公式可以看出,vruntime 的增长速度与进程的权重成反比。权重越大(优先级越高),vruntime 增长越慢,进程在红黑树中的位置越靠左,越容易被调度器选中。
3.2 nice 值与权重映射表
Linux 内核使用 nice 值(范围 -20 到 19)来表示进程的优先级。nice 值越低,优先级越高。内核维护了一张预定义的权重映射表 sched_prio_to_weight,将每个 nice 值映射到一个具体的权重值。
下表展示了部分 nice 值与权重的对应关系:
| nice 值 | 权重 (weight) | 相对 nice 0 的 CPU 时间比例 | 说明 |
|---|---|---|---|
| -20 | 88761 | ≈ 86.7 倍 | 最高优先级,几乎独占 CPU |
| -10 | 29154 | ≈ 28.5 倍 | 高优先级 |
| -5 | 11051 | ≈ 10.8 倍 | 较高优先级 |
| 0 | 1024 | 1 倍(基准) | 默认优先级 |
| 5 | 335 | ≈ 0.33 倍 | 较低优先级 |
| 10 | 110 | ≈ 0.11 倍 | 低优先级 |
| 19 | 15 | ≈ 0.015 倍 | 最低优先级,几乎得不到 CPU |
权重值的设计遵循一个近似比例:每降低一个 nice 值,权重增加约 1.25 倍(即 10% 的 CPU 时间差异)。这种指数级映射确保了在大量进程竞争时,优先级差异能平滑地转化为 CPU 时间分配差异。
3.3 权重映射与 vruntime 的关系图
下面的 SVG 图展示了从 nice 值到权重,再到 vruntime 增长率的完整映射关系:
3.4 内核代码中的权重映射
在 Linux 内核源码 kernel/sched/core.c 中,权重映射表定义如下:
/*
* Nice 值到权重映射表。
* 数组索引为 nice + 20,即 nice=-20 对应索引 0,nice=19 对应索引 39。
*/
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,
};
内核通过以下宏或内联函数从 nice 值获取权重:
#define NICE_TO_WEIGHT(nice) sched_prio_to_weight[(nice) + 20]
3.5 vruntime 计算的实际应用
在时钟中断处理函数 scheduler_tick() 中,CFS 会调用 update_curr() 来更新当前进程的 vruntime。其核心逻辑如下:
static void update_curr(struct cfs_rq *cfs_rq)
{
struct sched_entity *curr = cfs_rq->curr;
u64 now = rq_clock_task(rq_of(cfs_rq));
u64 delta_exec;
if (unlikely(!curr))
return;
delta_exec = now - curr->exec_start;
if (unlikely((s64)delta_exec <= 0))
return;
curr->exec_start = now;
/* 核心计算:根据权重更新 vruntime */
curr->vruntime += calc_delta_fair(delta_exec, &curr->load);
/* ... 后续检查是否需要进行调度 ... */
}
/*
* calc_delta_fair 实现 vruntime 增量计算
*/
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 确保了每次时钟中断都能精确地根据进程权重更新其虚拟运行时间,为下一次调度决策提供准确的依据。