4. 虚拟运行时间计算:vruntime 的核心机制

完全公平调度器(CFS)的核心思想是让每个进程都能公平地获得 CPU 时间。为了实现这一目标,CFS 引入了 虚拟运行时间(vruntime) 的概念。vruntime 不是真实的墙上时钟时间,而是经过权重修正后的“加权时间”,它直接决定了进程在红黑树中的排序位置——vruntime 最小的进程被优先调度。

核心原则: CFS 的目标是让所有可运行进程的 vruntime 尽可能相等。当进程的 vruntime 差值超过一定阈值(调度延迟或最小粒度)时,就会触发抢占。

4.1 vruntime 的计算公式

vruntime 的计算公式体现了“权重越大,虚拟时间增长越慢”的公平思想。其基本公式如下:

vruntime += delta_exec * (NICE_0_LOAD / se->load.weight)

其中:

  • delta_exec:进程实际运行的物理时间(单位:ns)。
  • NICE_0_LOAD:nice 值为 0 时的权重(固定值 1024),作为基准。
  • se->load.weight:当前进程调度实体的权重,由 nice 值映射而来。

从公式可以看出:

  • 当进程权重 se->load.weight 等于 NICE_0_LOAD(即 nice=0)时,vruntime 的增长速度等于物理时间。
  • 当进程权重更大(nice 值更低,优先级更高)时,NICE_0_LOAD / weight 比值小于 1,vruntime 增长变慢,从而获得更多 CPU 时间。
  • 当进程权重更小(nice 值更高,优先级更低)时,比值大于 1,vruntime 增长变快,CPU 时间被压缩。
直观理解: 可以把 vruntime 想象成“时间债务”。高优先级进程(低 nice 值)的“债务”积累得慢,因此可以“欠债”更久(运行更长时间);低优先级进程的“债务”积累得快,很快就会被替换。

4.2 nice 值与权重映射表

内核维护了一个全局的静态数组 sched_prio_to_weight,用于将进程的 nice 值(范围 -20 到 19)映射为对应的权重值。权重值的设计遵循了 每降低一个 nice 值,CPU 时间增加约 10% 的近似原则。

下表展示了部分关键 nice 值对应的权重:

nice 值 权重 (weight) 相对于 nice=0 的 CPU 时间比例 说明
-20 88761 ≈ 86.7 倍 最高优先级(实时进程除外)
-10 29154 ≈ 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 倍 最低优先级
注意: 权重值并非线性增长,而是呈指数级增长。从 nice=0 到 nice=-20,权重增加了约 86 倍,这确保了高优先级进程在竞争激烈时能获得压倒性的 CPU 时间份额。

内核中对应的数组定义(位于 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,
};

4.3 calc_delta_fair() 函数实现

calc_delta_fair() 是 CFS 中计算 vruntime 增量的核心函数。它接收进程的实际运行时间 delta_exec 和调度实体 se,返回经过权重缩放后的虚拟时间增量。

函数实现(位于 kernel/sched/fair.c)如下:

/*
 * delta_exec : 进程实际运行的物理时间(ns)
 * se         : 当前进程的调度实体
 * 返回值     : 虚拟运行时间增量(ns)
 */
static inline u64 calc_delta_fair(u64 delta_exec, struct sched_entity *se)
{
    if (unlikely(se->load.weight != NICE_0_LOAD))
        delta_exec = __calc_delta(delta_exec, NICE_0_LOAD, &se->load);

    return delta_exec;
}

函数逻辑解析:

  • 快速路径: 如果进程权重恰好等于 NICE_0_LOAD(nice=0),则直接返回 delta_exec,无需计算。
  • 慢速路径: 如果权重不同,则调用 __calc_delta() 进行精确的乘除运算。

__calc_delta() 函数实现了公式 delta_exec * NICE_0_LOAD / weight,并使用了 避免 64 位溢出的技巧:

static u64 __calc_delta(u64 delta_exec, unsigned long weight, struct load_weight *lw)
{
    u64 fact = scale_load_down(weight);  // 将 NICE_0_LOAD 缩放到合适范围
    int shift = 32;
    u64 tmp;

    tmp = (u64)delta_exec * fact;
    if (tmp < (u64)1 << 63) {
        /* 不会溢出,直接乘除 */
        return tmp / scale_load_down(lw->weight);
    } else {
        /* 可能溢出,先右移再乘除 */
        shift--;
        tmp = (u64)delta_exec * (fact >> 1);
        return ((tmp / scale_load_down(lw->weight)) << 1);
    }
}
工程细节: scale_load_down() 宏用于将权重值从“放大”状态还原为“原始”状态。内核为了在负载均衡时提高精度,会将权重放大(乘以 2^10),因此在计算 vruntime 时需要缩回。

4.4 vruntime 计算与调度流程

下面的 SVG 图展示了从进程 tick 中断到更新 vruntime,再到红黑树重排的完整流程:

vruntime 计算与 CFS 调度流程 vruntime 计算与 CFS 调度决策流程 时钟中断 (scheduler_tick) 获取 delta_exec (实际运行时间) calc_delta_fair(delta_exec, se) 权重缩放:delta_exec * NICE_0_LOAD / weight se->vruntime += delta 红黑树重排 (__enqueue_entity / __dequeue_entity) 检查是否需要抢占 (check_preempt_tick) 关键输入 delta_exec: 物理时间 se->load.weight: 进程权重 关键输出 vruntime 增量 红黑树键值更新

4.5 组调度中的 vruntime 计算

当启用 组调度(Group Scheduling) 时,vruntime 的计算会多一层抽象。每个调度组(task_group)本身也是一个调度实体,拥有自己的 se->load.weight。组内进程的 vruntime 计算不仅受自身 nice 值影响,还受所在组的权重影响。

组调度的 vruntime 计算遵循 层级缩放 原则:

// 组内进程的 vruntime 增量
vruntime += delta_exec * (NICE_0_LOAD / se->load.weight) * (group_weight / total_group_weight)

这种设计确保了:

  • 组与组之间按权重比例分配 CPU 时间。
  • 组内进程之间按各自 nice 值公平竞争组内的时间份额。
  • 组调度对上层透明,CFS 红黑树仍然按 vruntime 排序。
调优实践: 在容器化环境中(如 Docker、Kubernetes),组调度被广泛用于实现 CPU 份额限制(cpu.shares)。通过调整组的权重,可以精确控制不同容器或 cgroup 之间的 CPU 分配比例。

4.6 关键参数与调优建议

参数 默认值 影响 调优方向
sched_latency_ns 6,000,000 (6ms) 目标调度延迟,影响 vruntime 的抢占阈值 交互式负载可降低;批处理可适当提高
sched_min_granularity_ns 750,000 (0.75ms) 进程最小运行时间,防止频繁切换 CPU 密集型可提高;IO 密集型可降低
sched_nr_latency 8 调度延迟除以该值得到最小粒度 通常无需修改
重要提示: 直接修改 /proc/sys/kernel/sched_* 参数会影响系统全局调度行为。建议在测试环境验证后再应用到生产环境,并配合监控工具(如 perf、pidstat)观察效果。