2. Linux CFS调度器原理:完全公平调度器(CFS)核心概念与数据结构

完全公平调度器(Completely Fair Scheduler, CFS)自 Linux 2.6.23 引入以来,一直是默认的进程调度器。其设计哲学颠覆了传统基于时间片轮转的调度模型,转而采用基于“虚拟运行时间”的无限小粒度调度,旨在为每个进程提供对 CPU 时间的“完全公平”分配。理解 CFS 是进行 Docker 容器 CPU 性能调优的理论基石。

2.1 核心设计理念:公平与虚拟运行时间

CFS 不直接分配固定时间片,而是维护一个称为 虚拟运行时间(vruntime) 的度量。每个可运行进程的 vruntime 按实际消耗的 CPU 时间加权(考虑优先级)增长。CFS 的调度决策极其简单:始终选择 vruntime 最小的进程来运行。

核心公式:
vruntime += 实际运行时间 * (NICE_0_LOAD / 进程权重)

其中 NICE_0_LOAD 是 nice 值为 0 时的标准权重(1024)。权重越高(优先级越高),vruntime 增长越慢,从而获得更多 CPU 时间。

关键洞察: CFS 的目标是让所有进程的 vruntime 尽可能接近。当一个进程运行足够长的时间后,它的 vruntime 会超过其他等待的进程,从而被换出,让给下一个“最饥饿”的进程。
CFS 调度决策循环 就绪队列 (rbtree) 选择最左节点 进程上下文切换 更新 vruntime 重新插入红黑树 循环

2.2 核心数据结构:红黑树与调度实体

CFS 使用 红黑树(Red-Black Tree) 作为就绪队列的核心数据结构,以 vruntime 为键值组织所有可运行进程。这使得“查找最小 vruntime 进程”的操作时间复杂度为 O(1)(直接取最左节点)。

数据结构 内核源码表示 作用与说明
调度实体 (sched_entity) struct sched_entity CFS 调度的基本单位。每个进程(task_struct)内嵌一个 se 实例,包含 vruntime、权重、负载贡献等关键字段。
CFS 就绪队列 (cfs_rq) struct cfs_rq 管理某个调度类(CFS)下所有可运行进程的队列。包含红黑树根节点 tasks_timeline、最小 vruntime min_vruntime、运行负载 load 等。
红黑树节点 (rb_node) struct rb_node 嵌入在 sched_entity 中,用于将 se 链接到 cfs_rq 的红黑树上。键值为 se->vruntime。
容器调优注意: 在 Docker 容器场景下,每个容器内的进程组(通过 CPU Cgroup)会被视为一个 组调度实体(task group)。CFS 在 cfs_rq 中同时管理普通进程实体和组实体,这使得容器级别的 CPU 份额控制成为可能。
CFS 核心数据结构关系 struct cfs_rq tasks_timeline (红黑树根节点) min_vruntime (单调递增基准值) struct sched_entity vruntime load (权重) rb_node

2.3 关键调度流程与参数

CFS 的调度周期由 sched_latency_ns(调度延迟)和 min_granularity_ns(最小粒度)两个核心参数控制。它们决定了进程在被抢占前可以运行的最短和最长时长。

  • 调度延迟(sched_latency_ns): CFS 期望在一个调度周期内,所有可运行进程至少被调度一次。默认值通常为 6ms(6000000)。
  • 最小粒度(sched_min_granularity_ns): 进程被抢占前至少运行的时间,防止过于频繁的上下文切换。默认值通常为 0.75ms(750000)。
  • 目标延迟(targeted_latency): 实际计算出的时间片。当进程数 N 较少时,时间片 = sched_latency_ns / N;当 N 较大时,时间片被限制在 min_granularity_ns 以上。
时间片计算示例(假设 sched_latency=6ms, min_granularity=0.75ms):
  • 2 个进程:每个进程时间片 = 6ms / 2 = 3ms
  • 10 个进程:每个进程时间片 = 6ms / 10 = 0.6ms,但低于 min_granularity,因此实际时间片 = 0.75ms

这意味着当运行队列很长时,CFS 会牺牲“完全公平”的周期,转而保证每个进程至少获得一个最小运行时间片。

CFS 时间片分配决策流程 获取可运行进程数 N 计算理想时间片 sched_latency / N 检查最小粒度 确定实际时间片 进程运行 & 更新 vruntime 理想时间片 < min_granularity? 是 否

2.4 与 Docker 容器调优的关联

Docker 通过 CPU Cgroup 子系统与 CFS 交互。每个容器对应一个 Cgroup,CFS 通过以下参数实现容器级别的 CPU 限制和权重分配:

Docker 参数 影响的 CFS 内核参数 调优效果
--cpus cpu.cfs_period_us 和 cpu.cfs_quota_us 限制容器在周期(默认 100ms)内最多使用的 CPU 时间。例如 --cpus=1.5 意味着每 100ms 最多使用 150ms 的 CPU 时间。
--cpu-shares cpu.shares (映射到 CFS 权重) 设置容器在 CPU 竞争时的相对权重。默认 1024。权重越高,在 CPU 繁忙时获得的份额越多。
--cpu-rt-runtime cpu.rt_runtime_us 限制实时进程的 CPU 时间,防止实时进程饿死 CFS 进程。
调优实践: 对于延迟敏感的容器(如 Web 服务器),可以适当减小 sched_min_granularity_ns(通过内核参数 kernel.sched_min_granularity_ns),让 CFS 更频繁地进行调度,减少进程的等待时间。但需注意,过小的粒度会增加上下文切换开销。