引言

在现代计算系统中,进程调度器是操作系统的核心组件之一,它决定了哪个进程在何时获得 CPU 时间片。Linux 内核自 2.6.23 版本起引入的 CFS(Completely Fair Scheduler,完全公平调度器)彻底改变了 Linux 的调度设计理念。CFS 不再基于传统的时间片轮转算法,而是基于一个简洁而优雅的概念:虚拟运行时间(vruntime),通过红黑树数据结构实现 O(log n) 复杂度的高效调度决策。

1. CFS 的核心设计理念

CFS 的核心思想可以用一句话概括:让所有可运行进程的虚拟运行时间尽可能相等。这个"完全公平"的理念基于以下几个关键原则:

1.1 虚拟运行时间(vruntime)

每个进程维护一个 vruntime(virtual runtime)字段,表示该进程经过权重归一化后的实际运行时间。计算公式如下:

delta_vruntime = (delta_exec * NICE_0_LOAD) / weight

其中:

  • delta_exec:进程实际执行的物理时间
  • NICE_0_LOAD:nice 值为 0 时的权重基准(1024)
  • weight:进程的调度权重,由 nice 值决定

优先级高的进程(nice 值更低)拥有更大的权重,因此 vruntime 增长更慢,从而获得更多的 CPU 时间比例。

1.2 红黑树作为调度队列

CFS 使用红黑树(Red-Black Tree)来组织所有可运行进程,以 vruntime 作为排序键。最左侧节点即为 vruntime 最小的进程——也就是最"亏待"的进程,它将被选为下一个运行进程。这种设计带来了以下优势:

  • O(log n) 时间复杂度的进程插入和删除
  • O(1) 时间复杂度获取下一个待调度进程(最左节点缓存在 rb_leftmost)
  • 天然支持范围查询和高效更新

2. 权重与优先级体系

CFS 通过 nice 值到权重的映射来实现优先级控制。Linux 内核使用 sched_prio_to_weight 数组定义了 40 个优先级等级(nice 值 -20 到 19)对应的权重:

static const int 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,
};

相邻等级间的权重比例约为 1.25 倍,这意味着每降低一个 nice 值,进程多获得约 25% 的 CPU 时间。

2.1 调度权重计算示例

假设系统中有两个进程:

  • 进程 A:nice = 0,weight = 1024
  • 进程 B:nice = 5,weight = 335

CPU 时间分配比例约为:1024 : 335 ≈ 3.06 : 1,即进程 A 获得约 75% 的 CPU 时间,进程 B 获得约 25%。这与传统固定时间片轮转相比更加灵活和精确。

3. 调度延迟与粒度控制

CFS 通过多个 sysctl 参数精细控制调度行为:

3.1 关键调度参数

  • sched_latency(默认 6ms):目标调度延迟,即所有可运行进程轮流运行一遍的目标时间
  • min_granularity(默认 0.75ms):最小粒度,防止进程切换过于频繁
  • sched_min_granularity_ns:进程获得的最小连续执行时间
  • sched_wakeup_granularity_ns:唤醒抢占粒度,控制新唤醒进程是否立即抢占当前进程

3.2 时间片计算

当可运行进程数为 n 时,每个进程的理论时间片为:

time_slice = max(sched_latency / n, min_granularity)

当进程数量很多时(n > sched_latency/min_granularity = 8),每个进程获得 min_granularity 而非按比例分配,避免切换开销过大。

4. 组调度(CGroup 支持)

CFS 天然支持组调度(Group Scheduling),通过 CPU 控制组(cpu cgroup)实现层次化的带宽分配:

4.1 两层调度架构

CFS 实现了两层调度结构:

  1. 用户组间:按照 cgroup 分配的 CPU 带宽比例进行调度
  2. 用户组内:按照进程的 nice 值进行比例公平调度

4.2 CFS 带宽控制

通过 cpu.cfs_quota_us 和 cpu.cfs_period_us 参数实现硬性的 CPU 带宽限制:

# 限制容器最多使用 1 个 CPU 核心的 50%
echo 50000 > /sys/fs/cgroup/cpu/mycontainer/cpu.cfs_quota_us
echo 100000 > /sys/fs/cgroup/cpu/mycontainer/cpu.cfs_period_us

当 cgroup 内的进程在 period 内累计使用 CPU 达到 quota 后,throttle 机制会被触发,该组所有进程将被暂停直到下一个 period 开始。

5. NUMA 感知调度

在现代多核 NUMA 架构中,CFS 与 NUMA 平衡机制协同工作:

5.1 NUMA 节点亲和性

进程的内存访问模式会影响调度决策:

  • 进程倾向于在其内存所在 NUMA 节点上运行
  • 内核的 AutoNUMA 平衡机制会扫描进程内存页访问频率
  • 当远端访问开销过大时,内核会将进程迁移到其内存所在节点

5.2 调度域(Sched Domain)

Linux 调度域按硬件拓扑分层组织:

DIE → MC → SMT

SMT 层级处理超线程 sibling,MC 层级处理同一物理核心,DIE 层级处理不同 NUMA 节点。更高层级的负载均衡迁移成本更高,因此优先在低层级平衡。

6. 负载跟踪与带宽估计

CFS 使用 PELT(Per-Entity Load Tracking)算法来跟踪每个调度实体的负载:

6.1 PELT 衰减模型

每个调度实体维护一个负载值,按指数衰减规律更新:

y^n = y^(n-1) * (1 - 1/2^(n/decay)) + active * (1 - y^(n-1)/y^n)

32ms 的半衰期窗口意味着:32ms 前的负载贡献为当前的 50%,64ms 前为 25%。这种指数衰减模型能快速响应负载变化,同时提供稳定的历史负载估计。

6.2 利用率传播

调度实体的负载不仅影响自身,还会向上传播到其所属的 cgroup 和 CPU 运行队列:

  • 进程负载 → CPU 运行队列负载 → 调度域负载
  • 层级的负载用于负载均衡决策:从高负载域向低负载域迁移进程

7. 内核实现:关键数据结构

7.1 运行队列(cfs_rq)

struct cfs_rq {
    struct load_weight load;      // 运行队列总权重
    unsigned long runnable_weight;
    unsigned int nr_running;     // 可运行进程数
    unsigned int h_nr_running;   // 包含组调度的总运行数
    
    u64 exec_clock;              // 执行时钟
    u64 min_vruntime;            // 最小 vruntime 基准
    struct rb_root_cached runned_tasks_timeline; // 红黑树根
    
    struct sched_entity *curr;   // 当前运行实体
    struct sched_entity *next;   // 下一个运行实体(用于抢占后恢复)
    struct sched_entity *last;   // 上一个运行实体(用于上下文切换)
};

7.2 调度实体(sched_entity)

struct sched_entity {
    struct load_weight load;     // 实体权重
    struct rb_node run_node;     // 红黑树节点
    unsigned int on_rq;          // 是否在运行队列上
    
    u64 exec_start;              // 开始执行时间
    u64 sum_exec_runtime;        // 累计实际运行时间
    u64 vruntime;                // 虚拟运行时间
    u64 prev_sum_exec_runtime;   // 上次累计执行时间
    
    u64 nr_migrations;           // 迁移次数
    // ...
};

8. 实践调优与性能优化

8.1 参数调优建议

# 低延迟桌面环境
sysctl kernel.sched_min_granularity_ns=1000000
sysctl kernel.sched_wakeup_granularity_ns=1500000

# 高吞吐量服务器
sysctl kernel.sched_min_granularity_ns=10000000
sysctl kernel.sched_wakeup_granularity_ns=15000000

# HPC / 实时计算
sysctl kernel.sched_min_granularity_ns=100000000
sysctl kernel.sched_latency_ns=500000000

8.2 观测与调试

理解 CFS 行为的最佳途径是通过 procfs 和 debugfs 获取运行时数据:

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

# 查看 CFS 运行队列状态
cat /sys/kernel/debug/sched/debug

# 监控调度延迟
perf sched record -- sleep 1
perf sched latency

# 使用 schedtool 查看和设置调度策略
schedtool -v <PID>

8.3 sched_features 控制

可以通过 /proc/sys/kernel/sched_features 文件系统开关调整特定行为:

  • NO_GENTLE_FAIR_SLEEPERS:禁用对睡眠进程的 vruntime 补偿
  • NO_NEXT_BUDDING:禁用新唤醒进程优先选择逻辑
  • NO_WAKEUP_PREEMPTION:禁用唤醒立即抢占,改为时间片耗尽才进行抢占

9. 实时调度类与 CFS 的协作

Linux 调度器是一个模块化框架,按优先级从高到低依次为:

SCHED_DEADLINE (SCHED_DL)  →  SCHED_FIFO/SCHED_RR  →  SCHED_NORMAL (CFS)  →  SCHED_IDLE

CFS 实际管理 SCHED_NORMAL、SCHED_BATCH 和 SCHED_IDLE 三类进程。SCHED_IDLE 进程的 nice 值被固定映射到极低的权重(3),确保只在 CPU 完全空闲时才运行。

9.1 SCHED_DEADLINE 与 CFS 的交互

SCHED_DEADLINE 基于 EDF(Earliest Deadline First)算法,采用 Constant Bandwidth Server(CBS)机制。当 DEADLINE 任务处于可运行时,CFS 会主动避让。CBS 规则确保 DEADLINE 任务不会超过其预留带宽,同时不影响 CFS 进程的正常执行。

10. 前沿发展与未来方向

10.1 内核 5.x/6.x 的演进

近年来 Linux 调度器的持续改进包括:

  • Core Scheduling:缓解 MDS/L1TF 等侧信道攻击,限制同一核心上 SMT sibling 运行不可信进程
  • NUMA 平衡改进:AutoNUMA v2 引入周期性内存扫描和智能进程迁移
  • 热插拔优化:CPU 热插拔时的负载均衡策略优化,减少服务中断
  • EAS(Energy Aware Scheduling):在 ARM big.LITTLE 架构上结合调度与能耗管理

10.2 云原生时代的调度需求

容器密度和微服务架构对调度器提出了新挑战:

  • 延迟敏感型 workload:需要在容器级别保证 P99 尾延迟
  • CPU 配额与 CFS 带宽的交互:高并发场景下 quota 耗尽导致的不公平问题
  • 大规模节点上的调度域优化:减少不必要的跨 NUMA 负载均衡开销

总结

Linux CFS 通过虚拟运行时间和红黑树的优雅组合,在简洁的设计中实现了高效、公平的进程调度。其基于权重的比例公平分配、对 NUMA 拓扑的感知能力、以及与 cgroup 的无缝整合,使其从 2007 年诞生至今仍然是 Linux 内核调度的核心。理解 CFS 的内部机制,能够帮助系统管理员做出更精准的调优决策,也能让开发者编写出更加符合调度器友好的应用程序。

随着异构计算(GPU、DPU、NPU)的普及和调度需求的日益复杂,CFS 的架构仍将持续演进,在"完全公平"的核心理念之上,支撑起下一代计算平台的需求。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部