4. 虚拟运行时间计算:vruntime 的核心机制
完全公平调度器(CFS)的核心思想是让每个进程都能公平地获得 CPU 时间。为了实现这一目标,CFS 引入了 虚拟运行时间(vruntime) 的概念。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 时间被压缩。
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 倍 | 最低优先级 |
内核中对应的数组定义(位于 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,再到红黑树重排的完整流程:
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 排序。
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)观察效果。