一、CFS 调度器概述与历史演进

Linux 内核的完全公平调度器(Completely Fair Scheduler,CFS)自 2.6.23 版本(2007 年)引入以来,一直是 Linux 内核的默认进程调度器。CFS 的设计理念颠覆了传统 O(1) 调度器的固定时间片模式,引入了一种基于虚拟运行时间(vruntime)的红黑树调度模型,实现了"完全公平"的 CPU 时间分配。

在 CFS 之前,Linux 使用的是 O(1) 调度器,它基于优先级数组和固定时间片分配,虽然时间复杂度为 O(1),但在交互式进程和批处理进程之间的权衡上存在明显不足。CFS 的目标是:让每个可调度实体获得的 CPU 时间与其权重成正比,实现真正的加权公平。

CFS 的核心思想极其优雅:不直接分配时间片,而是记录每个进程已经获得的"虚拟运行时间",每次选择 vruntime 最小的进程运行。这意味着运行时间最少的进程最先获得 CPU,从而实现公平性。

二、核心数据结构

CFS 的实现涉及几个关键数据结构:

1. sched_entity(调度实体):每个进程或调度组都内嵌一个 sched_entity 结构,其中包含 vruntime、权重、运行状态等关键信息。这意味着调度不仅可以针对单个进程,还可以针对整个进程组(cgroup)进行。

2. cfs_rq(CFS 运行队列):每个 CPU 都有一个 cfs_rq,其中的红黑树(rb-tree)以 vruntime 为键组织所有可调度实体。最左侧节点(最小 vruntime)就是下一个被调度的实体。

3. rq(运行队列):顶层结构,包含 CFS 调度器、RT 调度器、DL 调度器的队列,以及负载统计信息。

红黑树的选择是为了在 O(log n) 时间内完成插入和删除操作,同时在调度决策时(选择最小 vruntime 节点)可以达到 O(1)(通过缓存最左侧节点)。

三、虚拟运行时间(vruntime)计算模型

vruntime 是 CFS 的核心概念。其计算公式为:

vruntime += (实际运行时间 NICE_0_LOAD) / 实体权重

从公式可以看出:

  • 权重越大的进程(高优先级),vruntime 增长越慢,获得更多实际 CPU 时间
  • 权重越小的进程(低优先级),vruntime 增长越快,逐渐让出 CPU
  • NICE_0_LOAD 对应 nice 值为 0 的进程权重(1024)

权重与 nice 值的映射关系遵循"每差一个 nice 级别,CPU 吞吐量变化约 10%"的原则。nice 值为 -20 时权重约 88761,nice 值为 19 时权重约 15,差距接近 6000 倍。

内核中权重与 nice 值的转换通过 prio_to_weight 数组实现,该数组预计算好了从 -20 到 19 每个 nice 值对应的权重值。

四、调度延迟与粒度控制

CFS 通过两个关键参数控制调度延迟和粒度:

sched_latency(调度延迟):默认 6ms(/sys/kernel/debug/sched/latency_ns),表示所有可运行进程轮转一遍的目标时间。

min_granularity(最小粒度):默认 0.75ms,确保每个进程至少运行这么长时间,避免上下文切换开销过大。

每个进程的理论时间片计算公式为:

time_slice = sched_latency * (se.weight / cfs_rq.total_weight)

当可运行进程数量过多时(sched_latency / nr_running < min_granularity),CFS 会退化为轮转方式,保证每个进程至少有最小运行时间。

Linux 4.13 引入了 sched_nr_migrate 限制单次负载均衡时迁移的进程数,进一步减少了 CPU 缓存失效的问题。

五、唤醒抢占与调度点

CFS 在以下场景会触发调度的"调度点":

1. 周期性调度(tick-driven):时钟中断时检查当前进程的已运行时长是否超过其时间片,若超过则设置 need_resched 标志。

2. 唤醒抢占(wake-up preemption):当新进程被唤醒时,CFS 检查其 vruntime 是否显著小于当前运行进程的 vruntime(通过 sysctl_sched_wakeup_granularity 控制阈值),若是则抢占当前进程。CFS 对唤醒进程的 vruntime 给予一定补偿,以避免 I/O 密集型进程因等待 I/O 导致 vruntime 落后太多而被"饿死"。

3. yield 与显式让出:进程通过 sched_yield() 主动让出 CPU,scheduler() 被直接调用。

4. 中断返回:中断处理完毕后检查 need_resched 标志。

六、组调度与带宽控制

CFS 支持组调度(Group Scheduling),允许将 CPU 时间分配给不同用户组或 cgroup,在组内再进行公平分配。

CFS Bandwidth Control(CFS Throttling):通过 cpu.cfs_quota_us 和 cpu.cfs_period_us 参数,可以限制一个 cgroup 在单位时间内使用的最大 CPU 时间。当配额用尽时,该 cgroup 内的进程会被"限流"(throttled),直到下个周期恢复。

带宽控制的实现依赖于一个高精度定时器(rt-bw),每个 cfs_rq 跟踪其剩余配额。当配额耗尽时,所有可运行实体出队限流;当下个周期到来时,定时器触发"解除限流"(unthrottle),重新允许运行。

在 Kubernetes 场景中,Pod 的 CPU Limit 就是通过 CFS Bandwidth Control 实现的,而 CPU Request 则通过 cpu.shares(权重)实现。这是理解容器资源限制的核心机制。

七、NUMA 感知调度

在多核 NUMA 系统中,内存访问延迟差异显著。CFS 通过以下机制优化 NUMA 局部性:

1. NUMA 负载均衡(NUMA Balancing):内核会周期性地扫描进程的内存页访问模式,将频繁访问远程内存的进程迁移到靠近该内存的 CPU 上。Linux 3.8 引入的 Auto NUMA Balancing 自动完成这一过程。

2. 唤醒时的 NUMA 亲和:当进程被唤醒时(如 I/O 完成),优先选择与上次运行在同一 NUMA 节点的空闲 CPU,以提高缓存命中率。

3. sched_numa_balancing_scan_period:控制 NUMA 扫描频率,默认 1000ms,可通过 sysctl 调整。过频繁的扫描本身会消耗 CPU 资源。

NUMA 调度的核心指标是 numa_faults,记录每个页面被不同节点 CPU 访问的次数,内核根据这些数据决定是否需要迁移内存或进程。

八、性能调优参数与最佳实践

CFS 提供了丰富的调优接口:

sched_migration_cost:进程迁移成本估计值,影响负载均衡的积极性。增大此值可减少不必要的进程迁移,适合 NUMA 敏感型负载。

sched_min_granularity_ns:最小调度粒度。对于延迟敏感的数据库应用,可以适当增大以减少上下文切换开销。

sched_wakeup_granularity_ns:唤醒抢占粒度。降低此值可提高交互响应性,但增加切换频率。

sched_autogroup_enabled:会话自动分组。开启后同一会话内的进程自动分为一组,对桌面交互体验有显著改善。

sysctl_sched_tunable_scaling:控制调度参数是否随 CPU 数量线性缩放,可选 none、log(对数缩放)、linear(线性缩放)。

典型场景调优建议:

  • 高性能计算:关闭 autogroup,设置较大的 min_granularity,亲和性绑定 CPU
  • 实时音视频:使用 PREEMPT_RT 补丁或实时调度策略 + CPU 隔离
  • Web 服务器:默认配置即可,可适当降低 wakeup_granularity 提升响应性
  • 大数据/批处理:利用带宽控制限制最大 CPU 使用率,保障在线服务

九、CFS 与现代调度类协作

Linux 调度器是多层的,不同调度类按优先级排列:

  1. stop_sched_class:最高优先级,用于 CPU 热插拔等关键操作
  2. dl_sched_class:Deadline 调度类,用于实时任务,基于 EDF 算法
  3. rt_sched_class:实时调度类(FIFO/RR),优先级高于普通进程
  4. fair_sched_class:CFS,处理普通进程的公平调度
  5. idle_sched_class:空闲调度类,仅在无任务时运行

每个 CPU 的运行队列包含所有调度类的队列。pick_next_task() 按优先级遍历所有调度类,选择一个可运行的高优先级任务。

从 Linux 3.14 开始引入的 Energy Aware Scheduling(EAS)进一步扩展了 CFS,在 ARM big.LITTLE 架构上实现了能耗感知调度,在公平性的同时考虑功耗。EAS 使用能耗模型(Energy Model,EM)计算每个 CPU 频率的功耗,选择能耗最优的调度决策。

十、内核源码剖析与调试

CFS 的核心代码位于 kernel/sched/fair.c,关键函数包括:

  • enqueue_task_fair() / dequeue_task_fair():实体入队/出队,维护红黑树和运行队列统计
  • pick_next_task_fair():从红黑树选择 vruntime 最小的实体
  • task_tick_fair():时钟中断时更新 vruntime 并检查是否需要重新调度
  • entity_tick():处理单个实体的 tick 更新
  • place_entity():放置新唤醒实体,设置初始 vruntime
  • update_curr():更新当前运行实体的统计信息

调试 CFS 可以通过以下途径:

# 查看进程调度统计
cat /proc/<pid>/sched

# 查看调度延迟直方ogram
cat /sys/kernel/debug/sched/latency_ns

# 跟踪调度事件
perf sched record -a -- sleep 10
perf sched latency
perf sched map

# BPF 工具
bpftrace -e 'ksched:sched_switch { printf("prev: %s -> next: %s\n", args->prev_comm, args->next_comm); }'

通过 /proc/<pid>/sched 可以查看 vruntime、nr_switches、wait_start 等详细信息,是排查进程调度问题的首选工具。

十一、CFS 的未来演进

CFS 仍在持续演进中。当前内核社区关注的改进方向包括:

1. 可扩展性优化:针对 1000+ 核心的超大规模系统,CFS 的锁竞争和缓存效率问题。"Core Scheduling"技术允许在 SMT(超线程)场景下按核心粒度进行安全调度。

2. eBPF 扩展调度:Linux 6.x 开始探索通过 eBPF 程序扩展调度策略,允许用户在不修改内核的情况下实现自定义调度逻辑。이는对 CFS 灵活性的重大突破。

3. 异构计算调度:随着 GPU、NPU、FPGA 等加速器成为主流,调度器需要感知不同计算单元的特性,做出更智能的任务分配决策。

4. Rust 部分重写:Linux 内核引入 Rust 支持后,部分调度辅助功能可能用 Rust 重写,提高安全性和可维护性。

CFS 作为 Linux 调度子系统的基石,其优雅的设计思想——基于虚拟运行时间的公平队列证明了在操作系统核心组件中数学建模的力量。二十余年来,它始终在公平性、响应性、吞吐量之间寻找最佳平衡点。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部