引言
完全公平调度器(Completely Fair Scheduler, CFS)是 Linux 内核 2.6.23 版本引入的进程调度器,它彻底改变了 Linux 调度器的设计理念。CFS 的核心思想不是基于传统的时间片分配,而是基于「虚拟运行时间」(virtual runtime, vruntime)的纳红黑树(RB-Tree)模型,实现真正意义上的「完全公平」。
本文将深入剖析 CFS 调度器的底层机制,包括虚拟运行时间计算、红黑树调度实体、组调度(Group Scheduling)、带宽控制(Bandwidth Control),并结合 eBPF 工具进行实战观测与调优。
1. CFS 核心设计理念
CFS 摒弃了传统 O(1) 调度器的 runqueue + active/expired 数组架构,转而采用一种更为优雅的方案:
- 目标:让每个任务获得「完全公平」的 CPU 时间份额
- 方法:维护每个任务的虚拟运行时间,总是选择 vruntime 最小的任务运行
- 数据结构:红黑树(rbtree),以 vruntime 为键,O(log N) 查找和插入
这意味着 CFS 不需要时间片的概念——任务的运行时间被转化为 vruntime,调度器只需要选择树最左边的节点(最小 vruntime)即可获得下一个要运行的任务。
2. 虚拟运行时间(vruntime)详解
vruntime 是 CFS 最核心的概念,它决定了任务在红黑树中的位置。
2.1 vruntime 计算公式
vruntime += delta_exec * (NICE_0_LOAD / weight);
// delta_exec: 实际运行时间(纳秒)
// NICE_0_LOAD: nice 0 的权重(1024)
// weight: 当前任务的权重(由 nice 值决定)
关键洞察:优先级越高的任务(nice 值越小),weight 越大,vruntime 增长越慢,因此能在红黑树中停留更久,获得更多 CPU 时间。
2.2 Nice 值与权重对照
| Nice值 | 权重 | CPU 份额比(相对 nice 0) |
|---|---|---|
| -20 | 88761 | 约 13.3x |
| -10 | 1501 | 约 3.0x |
| 0 | 1024 | 1.0x(基准) |
| 10 | 127 | 约 0.08x |
| 19 | 15 | 约 0.01x |
2.3 最小粒度与调度周期
// kernel/sched/fair.c
#define SCHED_NR_MAX_RUNNABLE_SCHED 32
#define MIN_GRANULARITY (0.75 * HZ) // 最小运行时间粒度
#define TARGET_LATENCY (6 * HZ) // 目标延迟 6ms
// 调度周期计算
if (nr_running > 1)
sysctl_sched_min_latency = sysctl_sched_min_latency * nr_running;
CFS 会确保每个可运行任务至少运行 SCHED_MIN_GRANULARITY 才被抢占,避免过多任务导致的调度开销。
3. 红黑树调度队列(cfs_rq)
3.1 核心数据结构
struct cfs_rq {
struct load_weight load; // 队列总权重
unsigned int nr_running; // 可运行任务数
unsigned int h_nr_running; // 包含组调度的层级计数
u64 min_vruntime; // 队列最小 vruntime(单调递增)
struct rb_root_cached tasks_timeline; // 红黑树根节点
struct sched_entity *curr; // 当前运行实体
struct sched_entity *next; // 下一个要运行(用于skip机制)
struct sched_entity *last; // 上一个运行(用于缓存预热)
};
3.2 调度实体(sched_entity)
struct sched_entity {
struct load_weight load; // 实体权重
struct rb_node run_node; // 红黑树节点
unsigned int on_rq; // 是否在运行队列
u64 vruntime; // 虚拟运行时间
u64 exec_start; // 本次开始运行时间
u64 sum_exec_runtime; // 总实际运行时间
u64 prev_sum_exec_runtime; // 上一次总运行时间
// 组调度相关
struct cfs_rq *cfs_rq; // 所属 CFS 队列
struct cfs_rq *my_q; // 组调度时的子队列
};
4. 组调度(Group Scheduling)
Linux 支持通过 cgroup 将 CPU 时间在不同用户组之间按比例分配。每个 cgroup 拥有独立的 CFS 队列,实现层次化资源控制。
4.1 两层调度架构
┌──────────────────────────┐
│ task_group (根组) │
│ cfs_rq (系统级) │
└───────────┬──────────────┘
│
┌─────────────────┼─────────────────┐
▼ ▼ ▼
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ cgroup A (50%) │ │ cgroup B (30%) │ │ cgroup C (20%) │
│ cfs_rq_A │ │ cfs_rq_B │ │ cfs_rq_C │
└────────┬─────────┘ └────────┬─────────┘ └────────┬─────────┘
│ │ │
┌────┴────┐ ┌────┴────┐ ┌─────┴─────┐
▼ ▼ ▼ ▼ ▼ ▼
task_1 task_2 task_3 task_4 task_5 task_6
每个 cgroup 的 shroups 值决定了它的 CPU 份额。当多个 cgroup 竞争 CPU 时,内核使用 DPE(Distributed Propagation Engine)来公平地传播 CPU 带宽。
5. 带宽控制(Bandwidth Control / CFS Throttling)
除了基于份额的调度,CFS 还引入了带宽控制机制,用于限制 cgroup 在固定周期内的最大 CPU 使用量。
5.1 带宽控制参数
// 每周期配额(微秒)
echo 500000 > /sys/fs/cgroup/cpu/mygroup/cpu.cfs_quota_us
// 周期长度(微秒)
echo 1000000 > /sys/fs/cgroup/cpu/mygroup/cpu.cfs_period_us
// 表示:每 1000ms 内最多使用 500ms CPU(即 0.5 个 CPU 核心)
5.2 带宽控制内核实现
// 带宽检查核心逻辑
static int tg_unthrottle_up(struct task_group *tg, void *data)
{
struct cfs_rq *cfs_rq = tg->cfs_rq[cpu];
if (!cfs_rq->runtime_enabled)
return 0;
// 恢复被限流的任务
while (cfs_rq_throttled(cfs_rq)) {
/* unthrottle */
raw_spin_lock(&cfs_rq->__runtime_lock);
cfs_rq->runtime_remaining += cfs_rq->runtime_assigned;
raw_spin_unlock(&cfs_rq->__runtime_lock);
}
return 0;
}
6. 多核调度:负载均衡与 NUMA 亲和性
6.1 调度域(Sched Domain)层级
// 调度域层级(从高到低)
// DIE > MC > SMT (Intel)
// MC > SMT > DIE (某些 ARM)
struct sched_domain {
struct sched_domain *parent; // 上层域
struct sched_domain *child; // 下层域
struct sched_group *groups; // 负载均衡组
unsigned long min_interval; // 最小均衡间隔
unsigned long max_interval; // 最大均衡间隔
unsigned int busy_factor; // 繁忙系数
unsigned int imbalance_pct; // imbalance 百分比阈值
};
6.2 NUMA 感知调度
// NUMA 调度策略
// 1. 任务首次分配时选择内存所在 NUMA 节点
// 2. 访问统计通过 task_numa_faults 跟踪 page fault
// 3. numa_migrate 触发时调用 migrate_misplaced_page
if (node_load(src_nid) > node_load(dst_nid) * imbalance_ratio / 100) {
// 触发任务迁移到负载更低的 NUMA 节点
}
7. eBPF 实战:观测 CFS 行为
7.1 使用 bpftrace 追踪调度事件
# 追踪每个任务的 vruntime 变化
bpftrace -e 'kprobe:update_curr {
= (struct cfs_rq *)arg0;
= (struct sched_entity *) ->curr;
printf("comm=%s vruntime=%llu delta_exec=%llu\n",
comm, ->vruntime, ->sum_exec_runtime - ->prev_sum_exec_runtime);
}'
# 追踪任务入队/出队
bpftrace -e '/tracepoint:sched:sched_switch/ {
printf("prev_pid=%d next_pid=%d\n", args->prev_pid, args->next_pid);
}'
7.2 使用 BCC 工具观测调度延迟
# runqlat - 运行队列延迟分布
$ runqlat 1 10
# runqlen - 运行队列长度
$ runqlen 1 10
# cpudist - CPU 时间分布
$ cpudist -p 1 5
7.3 自定义 eBPF 探针监控 CFS 限流
// throttled_monitor.bpf.c
SEC("tracepoint/sched/sched_process_exec")
int trace_sched_throttled(struct trace_event_raw_sched_process_exec *ctx)
{
u32 pid = bpf_get_current_pid_tgid() >> 32;
// 检查该进程是否在 cgroup 中被限流
struct task_struct *task = (struct task_struct *)bpf_get_current_task();
// ... 读取 cfs_bandwidth 状态
return 0;
}
8. CFS 调优实践
8.1 内核参数调优
# /etc/sysctl.conf
# 最小调度粒度(默认 0.75ms,适当增加减少上下文切换)
kernel.sched_min_granularity_ns = 1000000
# 调度周期(默认 6ms,增大有利于吞吐,减小有利于延迟)
kernel.sched_latency_ns = 12000000
# 唤醒预取(减少唤醒延迟)
kernel.sched_wakeup_granularity_ns = 15000000
# 迁移成本(影响负载均衡积极度)
kernel.sched_migration_cost_ns = 5000000
# NUMA 均衡周期
kernel.numa_balancing = 1
8.2 实时任务与 CFS 共存
Linux 实时调度策略(SCHED_FIFO/SCHED_RR)优先级始终高于 CFS。但过多的实时任务会饿死普通 CFS 任务。最佳实践:
# 限制实时任务总运行时间(每秒 950ms)
echo 950000 > /proc/sys/kernel/sched_rt_runtime_us
echo -1 > /proc/sys/kernel/sched_rt_period_us # 取消限制(危险)
8.3 容器场景 CFS 配置
# Docker CPU 份额限制(默认 1024)
docker run --cpus=2.0 --cpu-shares=512 my_app
# Kubernetes CPU requests/limits
resources:
requests:
cpu: "500m" # 50% CPU 份额
limits:
cpu: "1500m" # 最多 1.5 核心(启用带宽控制)
9. CFS 常见陷阱与排查
9.1 CPU 高负载但无响应
# 诊断步骤
# 1. 查看调度器统计
cat /proc/sched_debug | head -50
# 2. 查看每个 CPU 的运行队列
cat /proc/schedstat
# 3. 检查是否因 CFS 带宽限流
cat /sys/fs/cgroup/cpu/cpu.stat
# 输出:nr_periods 1234 nr_throttled 100 throttled_time 5000000000
9.2 上下文切换频繁(CS 过高)
可能原因:
- 任务粒度太细小,系统调用过于频繁
- 调度周期
sched_latency_ns过小,被 nr_running 补偿后仍导致频繁切换 - 中断风暴(网络/IO)导致频繁抢占
10. 总结
CFS 代表了操作系统调度器设计的一次范式转变。其核心创新在于:将「公平」定义为虚拟运行时间的均等化,而非传统的时间片轮转。这种设计使 CFS 在桌面交互、服务器吞吐、容器编排等场景下均表现优异。
理解 CFS 的关键在于掌握以下几个概念之间的关系:
- vruntime ↔ 权重:权重越大 vruntime 增长越慢,CPU 份额越高
- 红黑树 ↔ 调度决策:O(log N) 总是选择最左节点
- min_vruntime ↔ 公平保证:防止新任务饿死老任务
- cfs_bandwidth ↔ 硬限制:确保 QoS 的硬性约束
随着异构计算(big.LITTLE/Intel Thread Director)和新硬件的出现,CFS 也在持续演进,但其核心设计理念——完全公平——仍然是现代调度器设计的基准。

发表评论 取消回复