引言
在操作系统内核中,进程调度器是最核心的组件之一——它决定哪个进程获得 CPU 时间片,直接影响系统的吞吐量、响应速度和公平性。Linux 内核 2.6.23 引入的 CFS(Completely Fair Scheduler,完全公平调度器) 彻底取代了之前的 O(1) 调度器,标志着 Linux 调度设计理念的重大转变:从基于时间片的轮转调度,转向基于虚拟运行时间(vruntime)的红黑树模型。
本文将从 CFS 的设计哲学出发,深入剖析其核心数据结构、调度算法流程、组调度机制,并结合生产环境调优实践和真实案例,帮助读者全面理解这一关键子系统。
一、CFS 的设计哲学:"理想多任务处理器"模型
CFS的核心思想极其简洁:如果系统有 N 个可运行进程,每个进程应获得 1/N 的 CPU 时间。CFS 通过引入"虚拟处理器"的概念来逼近这一理想模型。
在理想多任务处理器上,每个进程在一小段时间内都独占 CPU,所有进程交替执行,宏观上看起来并行。CFS 的目标就是尽可能模拟这一行为。关键公式:
虚拟运行时间 = 实际运行时间 × NICE_0_LOAD / 进程权重
这意味着:
- 权重越高的进程,虚拟运行时间增长越慢,获得更多实际 CPU 时间
- nice 值为 0 的进程,虚拟运行时间等于实际运行时间
- nice 值每降低 1 级(优先级提高),获得约 1.25 倍 CPU 时间
这一设计的精妙之处在于:不需要时间片。传统调度器需要计算和分配时间片,而 CFS 只关心"谁落后了(vruntime 最小)",就选谁运行。
二、核心数据结构
CFS 的数据结构设计非常优雅,主要围绕三个关键结构:
2.1 调度实体:struct sched_entity
// 简化结构
struct sched_entity {
struct load_weight load; // 权重
struct rb_node run_node; // 红黑树节点
u64 vruntime; // 虚拟运行时间(核心字段)
u64 exec_start; // 本次开始执行的时间
u64 sum_exec_runtime; // 累计实际运行时间
u64 prev_sum_exec_runtime; // 切换前累计时间
// ... 组调度相关字段
};
关键洞察:每个调度实体(而非每个进程)都有自己的 vruntime。这为组调度提供了基础——一个 cgroup 作为一个整体参与调度,其内部再进行二级调度。
2.2 CFS 运行队列:struct cfs_rq
struct cfs_rq {
struct load_weight load; // 总权重
unsigned long nr_running; // 可运行任务数
u64 min_vruntime; // 最小虚拟运行时间(基准)
struct rb_root tasks_timeline; // 红黑树根节点
struct rb_node *rb_leftmost; // 最左节点(O(1) 选取)
// ...
};
红黑树按 vruntime 排序,最左侧节点是 vruntime 最小的进程(最需要调度的)。rb_leftmost 缓存使 pick_next_task 达到 O(1)。
2.3 进程描述符中的调度信息
// struct task_struct 中相关字段
struct task_struct {
// ...
struct sched_entity se; // 调度实体
struct sched_rt_entity rt; // 实时调度实体
const struct sched_class *sched_class; // 调度类链
unsigned int policy; // SCHED_NORMAL/SCHED_FIFO/SCHED_RR 等
int static_prio; // 静态优先级(nice + 120)
int prio; // 动态优先级(可能被提升)
// ...
};
三、CFS 调度算法全流程
3.1 入队:enqueue_entity
当进程变为可运行状态(wakeup 或 fork),将其加入红黑树:
入队操作:
1. 找到红黑树中的位置,按 vruntime 插入
2. 更新 cfs_rq->nr_running 和 load
3. 如果新进程 vruntime < 当前最左节点,更新 rb_leftmost
4. 调用 resched_curr() 检查是否需要抢占当前进程
关键细节:新进程或唤醒进程的 vruntime 会被设置为 cfs_rq->min_vruntime(可能稍作补偿),避免新进程饿死老进程或反过来的不公平问题。
3.2 出队:dequeue_entity
当进程阻塞或被抢占下树时从红黑树移除,更新统计信息。
3.3 选择下一个进程:pick_next_task_fair
选择流程:
1. 检查 rb_leftmost 是否为空(无任务则返回 NULL)
2. 取出最左节点对应的 sched_entity
3. 如果有组调度,可能需要递归选择
4. 返回对应的 task_struct
3.4 抢占逻辑:check_preempt_curr
CFS 使用"软实时"抢占策略:
- 新唤醒的进程如果 vruntime 比当前进程小一定阈值,则抢占
- 唤醒抢占粒度由
sysctl_sched_wakeup_granularity控制 - 时间片到期(实际运行超过预期)也会触发抢占
3.5 时间片计算:sched_slice
进程的理论时间片 = 调度延迟 × (进程权重 / cfs_rq总权重)
其中:
- 调度延迟:默认 6ms(sysctl_sched_latency)
- 当 nr_running > 8 时,时间片不小于最小粒度 0.75ms(sysctl_sched_min_granularity)
四、组调度(Group Scheduling)
CFS 的组调度机制是实现容器 CPU 资源限制的基础:
- shares:cgroup 的 CPU 权重。/sys/fs/cgroup/cpu/<cgroup>/cpu.shares,默认 1024
- 同一层级的 cgroup 之间按 shares 比例分配 CPU
- 每个 cgroup 有自己的 cfs_rq,内部再运行独立的 CFS 调度
- 总配额限制:
cpu.cfs_quota_us / cpu.cfs_period_us
这意味着 Kubernetes/Docker 的 CPU requests/limits 最终映射到 cgroup 的 shares 和 quota/period。
五、CFS 带宽控制(Bandwidth Control)
除了 shares 比例分配,CFS 还通过 bandwidth controller 提供硬上限:
// /proc/sys/kernel/sched_cfs_bandwidth_slice_us (默认 5000us)
带宽控制流程:
1. 每个 CFS bandwidth period(默认 100ms)内,cgroup 最多使用 quota 时间
2. 用尽后该 cgroup 被节流(throttled),直到下一个 period
3. 由 throttled_timer 在 period 恢复时 unthrottle
这使得容器场景中 CPU 硬限制成为可能,但也带来了"突发后空等"的利用率问题。
六、生产环境调优实践
6.1 关键 sysctl 参数
| 参数 | 默认值 | 说明 |
|---|---|---|
| sched_latency | 6ms | 目标调度延迟,所有进程轮转一圈的时间 |
| sched_min_granularity | 0.75ms | 最小时间片,防止过多上下文切换 |
| sched_wakeup_granularity | 1ms | 唤醒抢占粒度 |
| sched_migration_cost | 0.5ms | 进程迁移成本,影响负载均衡 |
| sched_nr_migrate | 32 | 负载均衡时一次迁移的进程数 |
| sched_autogroup_enabled | 1(Ubuntu) | 按 tty 自动分组桌面进程 |
6.2 高性能计算/低延迟场景调优
# 降低调度延迟(增加上下文切换频率但减少延迟)
kernel.sched_latency = 2000000 # 2ms
kernel.sched_min_granularity = 200000 # 200us
kernel.sched_wakeup_granularity = 200000 # 200us
# 对关键进程使用 chrt 设置实时优先级
chrt -f 99 ./critical_process
# 使用 taskset 绑核避免缓存冷启动
taskset -c 0,1 ./latency_sensitive_app
6.3 高吞吐量批处理场景
# 增大批次减少切换开销
kernel.sched_latency = 24000000 # 24ms
kernel.sched_min_granularity = 3000000 # 3ms
kernel.sched_wakeup_granularity = 4000000 # 4ms
七、生产事故案例
案例一:vruntime 雪崩导致系统卡顿
某容器平台大量使用低 shares 值的 cgroup 运行批处理任务。当高 shares 服务进程因 I/O 阻塞时 vruntime 停滞,唤醒后 vruntime 被设为 min_vruntime(远小于活跃进程),导致该进程长时间霸占 CPU,其他进程饿死。根因:CFS 的"补偿"机制将阻塞进程的 vruntime 重置为 min_vruntime,引发"vruntime 借债"问题。
方案:调整 sched_wakeup_granularity 到更大值,或引入 SCHED_IDLE 策略隔离批处理任务。
案例二:CFS 带宽节流导致业务超时
K8s 服务设置了 CPU limit=2 核,cpu.cfs_quota_us=200000。当流量突发时,进程在 period 前半段快速消耗完 quota 被 throttle,即使宿主机 CPU 空闲。根因:硬配额限制不考虑整体负载,造成资源浪费和延迟尖刺。
方案:适当提高 quota,或利用 CFS burst(cpu.cfs_burst_us)允许短期超出,或采用基于调度的 CPU 管理策略(static + cpuset)。
案例三:NUMA 跨节点迁移的性能衰减
某数据库宿主机在进程数激增后 QPS 下降 40%。分析发现 CFS 的负载均衡将大量进程在 NUMA 节点间频繁迁移,导致远端内存访问和缓存失效。根因:sched_migration_cost 设置过低,迁移过于激进。
方案:调高 sched_migration_cost,或结合 NUMA balancing 参数 numa_balancing 和 cpuset 绑核。
八、与实时调度器的协同
Linux 的调度类优先级链为:stop_sched_class → dl_sched_class → rt_sched_class → fair_sched_class → idle_sched_class
CFS 属于 fair_sched_class,排在实时类之后:
SCHED_FIFO/SCHED_RR:硬实时,抢占一切 CFS 进程SCHED_DEADLINE:基于 EDF 的截止期限调度SCHED_NORMAL/SCHED_BATCH:CFS 管理的普通进程SCHED_IDLE:仅当无其他进程时运行的极低优先级
关键启示:在实时系统上运行关键实时任务时,必须隔离 CPU(isolcpus 或 cpuset),否则 CFS 的长周期计算可能干扰中断线程。
九、前沿演进:EEVDF 即将取代 CFS
Linux 6.6 内核引入 EEVDF(Earliest Eligible Virtual Deadline First) 作为 CFS 的替代方案,预计在 6.12+ 成为默认调度器。核心改进:
- 引入 eligible time(合格时间),只有 vruntime 超过该时间才参与竞争,解决 vruntime 借债问题
- 每个进程有明确的 deadline,调度决策更确定
- 更精确地满足目标延迟(target latency),不再因进程数增加而线性拉长调度周期
- 简化了 CFS 中复杂的补偿逻辑
EEVDF 同样基于红黑树,且 API 兼容,这使得平滑迁移成为可能。
十、工程建议总结
- 理解 vruntime 即"不公平指标":vruntime 最小者最应被调度,这是理解一切 CFS 行为的基础
- 生产环境谨慎调整调度参数:默认值适合大多数场景,降级或 HPC 场景再针对性优化
- 容器环境重视 shares 与 quota 的搭配:比例分配提供弹性,硬配额保证隔离,两者结合才能兼顾效率和稳定性
- NUMA 感知部署:进程绑定、内存局部性优于纯粹的 CFS 负载均衡
- 关注 EEVDF 进展:新特性(eligible time、deadline)将简化很多现有调优技巧
- 高精度场景考虑实时调度 + CPU 隔离:CFS 的公平性在设计上就不是为低延迟保证服务的
参考资料
- Linux 内核源码:kernel/sched/fair.c
- Ingo Molnar 原始 CFS 论文 "CFS: Complete Fairness in Process Scheduling" (2007)
- Kernel Documentation: CFS Scheduler Design
- Peter Zijlstra, "EEVDF Scheduler Proposal", LKML, 2023

发表评论 取消回复