2、CFS调度器核心原理:虚拟运行时间(vruntime)、红黑树调度队列、调度周期与时间片计算
完全公平调度器(CFS,Completely Fair Scheduler)是 Linux 内核默认的 CPU 调度器,其设计哲学是“让每个进程都能公平地获得 CPU 时间”。在容器化环境中,CFS 通过 cgroup CPU 子系统实现组调度,确保多个容器或控制组按权重比例共享 CPU 资源。理解 CFS 的核心原理是配置容器 CPU 资源限制的基础。
2.1 虚拟运行时间(vruntime)
CFS 摒弃了传统的时间片轮转和优先级队列,引入了一个关键概念——虚拟运行时间(vruntime)。vruntime 是 CFS 调度决策的核心依据,它衡量了一个进程在理想公平模型下应获得的 CPU 时间。
- 定义:vruntime 是进程实际运行时间经过优先级加权后的结果。优先级越高的进程,其 vruntime 增长越慢;优先级越低的进程,其 vruntime 增长越快。
- 计算公式:
vruntime += 实际运行时间 × (NICE_0_LOAD / 进程权重)。其中NICE_0_LOAD是 nice 值为 0 时的标准权重(1024),进程权重由 nice 值映射表决定。 - 调度目标:CFS 总是选择 vruntime 最小的进程来运行,从而保证所有进程的 vruntime 尽可能接近,实现“公平”共享 CPU。
| nice 值 | 权重 | vruntime 增长速率(相对 nice=0) |
|---|---|---|
| -20 | 88761 | 极慢(约 1.15%) |
| -10 | 9548 | 慢(约 10.7%) |
| 0 | 1024 | 基准(100%) |
| 10 | 335 | 快(约 305%) |
| 19 | 15 | 极快(约 6827%) |
权重越高,vruntime 增长越慢,进程获得更多 CPU 时间。
2.2 红黑树调度队列
CFS 使用红黑树(Red-Black Tree)作为就绪队列的数据结构,用于高效管理所有可运行进程的 vruntime 排序。红黑树是一种自平衡二叉查找树,其插入、删除和查找操作的时间复杂度均为 O(log n)。
- 键值:每个就绪进程的 vruntime 作为红黑树的键值。
- 最左节点:红黑树的最左侧节点(即 vruntime 最小的进程)是 CFS 下一次要调度的进程。
- 调度操作:当进程被调度运行时,它从红黑树中移除;当进程时间片用完或主动让出 CPU 时,更新其 vruntime 并重新插入红黑树。
- 组调度扩展:在 cgroup 组调度中,每个 cgroup 内部维护一个独立的红黑树,同时 cgroup 本身作为“调度实体”参与上层红黑树的排序。
2.3 调度周期与时间片计算
CFS 的调度周期和时间片计算与传统调度器有本质区别。CFS 不预先分配固定时间片,而是通过调度周期(sched_latency)和进程权重动态计算每个进程应获得的运行时间。
2.3.1 调度周期(sched_latency)
调度周期是 CFS 保证所有可运行进程至少被调度一次的时间窗口。默认值为 6ms(可通过内核参数 /proc/sys/kernel/sched_latency_ns 调整)。
- 目标延迟:在调度周期内,所有就绪进程都应获得一次 CPU 运行机会。
- 最小粒度:每个进程至少运行一个最小时间片(
sched_min_granularity,默认 0.75ms),防止频繁上下文切换。 - 进程数上限:当就绪进程数过多时,调度周期会按比例扩展,以保证每个进程获得的最小时间片不低于
sched_min_granularity。
2.3.2 时间片计算公式
每个进程在调度周期内获得的时间片由其权重占所有就绪进程总权重的比例决定:
进程时间片 = 调度周期 × (进程权重 / 所有就绪进程总权重)
示例:
- 调度周期 = 6ms
- 进程 A 权重 = 1024(nice=0)
- 进程 B 权重 = 335(nice=10)
- 总权重 = 1024 + 335 = 1359
进程 A 时间片 = 6ms × (1024 / 1359) ≈ 4.52ms
进程 B 时间片 = 6ms × (335 / 1359) ≈ 1.48ms
| 层级 | 实体 | 权重 | 时间片分配 |
|---|---|---|---|
| 系统层 | cgroup_A(容器A) | 2048 | 6ms × (2048/4096) = 3ms |
| 系统层 | cgroup_B(容器B) | 1024 | 6ms × (1024/4096) = 1.5ms |
| 系统层 | 其他进程 | 1024 | 6ms × (1024/4096) = 1.5ms |
| cgroup_A内 | 进程A1(nice=0) | 1024 | 3ms × (1024/2048) = 1.5ms |
| cgroup_A内 | 进程A2(nice=5) | 1024 | 3ms × (1024/2048) = 1.5ms |
2.4 CFS 在容器化环境中的关键参数
在容器化环境中,CFS 的行为可以通过以下内核参数和 cgroup 接口进行调优:
| 参数/接口 | 路径 | 说明 | 默认值 |
|---|---|---|---|
sched_latency_ns | /proc/sys/kernel/sched_latency_ns | 调度周期(纳秒) | 6000000(6ms) |
sched_min_granularity_ns | /proc/sys/kernel/sched_min_granularity_ns | 进程最小运行时间(纳秒) | 750000(0.75ms) |
sched_wakeup_granularity_ns | /proc/sys/kernel/sched_wakeup_granularity_ns | 唤醒抢占粒度(纳秒) | 1000000(1ms) |
cpu.shares | /sys/fs/cgroup/cpu/<group>/cpu.shares | cgroup 的 CPU 权重(相对值) | 1024 |
cpu.cfs_period_us | /sys/fs/cgroup/cpu/<group>/cpu.cfs_period_us | CFS 带宽控制周期(微秒) | 100000(100ms) |
cpu.cfs_quota_us | /sys/fs/cgroup/cpu/<group>/cpu.cfs_quota_us | CFS 带宽控制配额(微秒,-1 表示无限制) | -1 |
cpu.shares 对应 requests.cpu(权重保证),cpu.cfs_period_us 和 cpu.cfs_quota_us 对应 limits.cpu(硬限制)。理解 CFS 的时间片计算机制有助于合理设置容器 CPU 资源请求与限制。
2.5 组调度与 vruntime 继承
在 cgroup 组调度模式下,CFS 引入了调度实体(sched_entity)的概念,每个 cgroup 和每个进程都是一个调度实体。组调度的 vruntime 计算遵循以下规则:
- 组 vruntime:cgroup 的 vruntime 由其内部所有进程的 vruntime 加权平均得到,用于在父层级中与其他 cgroup 或进程竞争 CPU。
- 权重继承:cgroup 的权重(cpu.shares)决定了其在父层级中的时间片比例,内部进程再按各自权重分配组内时间片。
- 嵌套调度:CFS 递归遍历调度层级树,从根红黑树开始,逐层选择 vruntime 最小的调度实体,直到选中一个具体的进程。
2.6 总结与关键要点
| 概念 | 作用 | 容器化意义 |
|---|---|---|
| vruntime | 衡量进程公平性的虚拟时间 | 决定容器组内进程的 CPU 分配顺序 |
| 红黑树 | 高效管理就绪进程的 vruntime 排序 | 支持大规模容器场景下的 O(log n) 调度 |
| 调度周期 | 保证所有进程获得 CPU 的时间窗口 | 影响容器 CPU 延迟和吞吐量 |
| 时间片计算 | 按权重比例动态分配 CPU 时间 | 实现容器 CPU 请求(requests)的公平共享 |
| 组调度 | 支持 cgroup 层级嵌套调度 | 实现多租户容器环境的 CPU 资源隔离 |