3. vruntime 定义:虚拟运行时间的计算公式、nice 值与权重映射表

在 CFS 调度器中,虚拟运行时间(vruntime)是核心度量指标。它并非真实的时间,而是经过权重归一化后的“调度账本”,用于公平地决定下一个被调度执行的进程。CFS 通过维护一棵红黑树,始终选择 vruntime 最小的进程运行,从而实现“完全公平”。

核心思想:vruntime 将不同优先级的进程“拉平”到同一个公平标尺上。高优先级(低 nice 值)进程的 vruntime 增长慢,低优先级(高 nice 值)进程的 vruntime 增长快,从而在调度周期内获得不同的 CPU 时间份额。

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 增长越慢,进程在红黑树中的位置越靠左,越容易被调度器选中。

直观理解:假设两个进程 A(权重高)和 B(权重低)同时运行相同物理时间。A 的 vruntime 增量小,B 的 vruntime 增量大。调度器会优先选择 vruntime 较小的 A 运行,从而让 A 获得更多的 CPU 时间。

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 增长率的完整映射关系:

nice 值 → 权重 → vruntime 增长率映射 nice 值 → 权重 → vruntime 增长率映射 nice 值(-20 ~ 19) 权重映射表 权重(weight) vruntime 增长率 ∝ 1 / weight 关键关系: • nice 值越低 → 权重越大 → vruntime 增长越慢 • nice 值越高 → 权重越小 → vruntime 增长越快 示例: 进程 A(nice=-5, weight=11051)运行 10ms → vruntime 增加 ≈ 0.93ms 进程 B(nice=5, weight=335)运行 10ms → vruntime 增加 ≈ 30.57ms

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]
注意:权重值在 32 位和 64 位系统上保持一致。CFS 调度器在计算 vruntime 时使用 64 位整数运算,以避免高权重进程长时间运行导致的溢出问题。

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 确保了每次时钟中断都能精确地根据进程权重更新其虚拟运行时间,为下一次调度决策提供准确的依据。