引言
完全公平调度器(Completely Fair Scheduler, CFS)是 Linux 内核自 2.6.23 版本引入的默认进程调度器,它的设计理念颠覆了传统基于时间片轮转的调度方式 —— 不再关注"每个进程运行多长时间",而是关注"每个进程的 CPU 时间分配是否公平"。
本文将从 CFS 的核心概念 vruntime(虚拟运行时间)出发,深入剖析红黑树调度数据结构、组调度(cgroup)集成、NUMA 多核负载均衡,并结合 eBPF 工具进行实战观测与调优。
1. CFS 核心理念 —— vruntime
CFS 的目标是让所有可运行进程的 vruntime 尽可能相等。vruntime 的计算公式为:
vruntime += delta_exec * (NICE_0_LOAD / weight)
其中:
delta_exec:进程实际运行的物理时间NICE_0_LOAD:nice 值 0 对应的权重基准常量 (1024)weight:根据 nice 值查表得到的优先级权重
这意味着低优先级进程(nice > 0)的 vruntime 增长更慢,获得更少的 CPU 时间;高优先级进程(nice < 0)的 vruntime 增长更快,但因为权重更高,实际获得的物理时间仍然更多。
2. 红黑树数据结构
CFS 使用红黑树(Red-Black Tree)来组织所有可运行进程,以 vruntime 作为排序键值。该数据结构提供了 O(log n) 的插入和删除效率,以及 O(1) 获取最左侧节点(最小 vruntime)的能力。
// 内核源码: kernel/sched/fair.c
struct cfs_rq {
struct rb_root_cached tasks_timeline; // 红黑树根节点
struct sched_entity *curr, *next, *last;
unsigned int h_nr_running; // 可运行任务数
u64 min_vruntime; // 最小 vruntime 基准
...
};
关键设计要点:
- 左节点优先:红黑树最左侧节点永远是需要调度的下一个进程
- min_vruntime 基准:新进程的 vruntime 被初始化为当前 cfs_rq->min_vruntime,防止新进程饥饿老进程
- key 偏移:
sched_entity->vruntime - cfs_rq->min_vruntime作为红黑树的实际键值
3. 调度时机与抢占
CFS 在以下场景触发调度决策:
- 时间片耗尽:
scheduler_tick()通过 HZ 周期性中断检查当前进程是否超过sched_latency / nr_running的配额 - 阻塞唤醒:进程从睡眠态唤醒时,
check_preempt_curr()对比唤醒进程与当前进程的 vruntime - 新进程 fork:通过
task_fork_fair()设置合理的初始 vruntime
Linux 引入了 GENTLE_FAIR_SLEEPERS 特性:对于长时间阻塞后被唤醒的进程,内核会适当减少其 vruntime,但不会低于 min_vruntime - sysctl_sched_latency,以避免频繁唤醒的进程获得不公平优势。
4. 组调度与 CGroup 集成
CFS 通过组调度(CONFIG_FAIR_CGROUP)支持层级资源分配。每个 cgroup 维护独立的 cfs_rq,父进程选择组内哪个 cfs_rq 获得时间片。
层级结构示例:
/system.slice/
├── nginx.service (shares=1024)
└── postgresql.service (shares=2048)
组调度的权重分配遵循比例原则:在相同层级下,shares 值越大,获得的 CPU 比例越高。cpu.shares 默认值为 1024。
两个重要的 cgroup 参数:
cpu.cfs_period_us:调度周期长度(默认 100ms)cpu.cfs_quota_us:组在周期内的最大 CPU 时间(默认 -1 无限制)
5. NUMA 感知调度
在 NUMA(非统一内存访问)架构中,CFS 与内核调度域(sched_domain)协同实现负载均衡和 NUMA 感知。
关键机制:
- idle_balance():CPU 空闲时从其他 CPU 拉取任务
- load_balance():周期性在调度域内迁移任务,保持 CPU 利用率均衡
- NUMA balancing(AutoNUMA):自内核 3.8 引入,将进程迁移到靠近其内存页面的 NUMA 节点
NUMA 节点拓扑示例:
┌──────────────────┐ ┌──────────────────┐
│ Node 0 │ │ Node 1 │
│ CPU 0-7 │ │ CPU 8-15 │
│ Local Memory │ │ Local Memory │
└────────┬─────────┘ └────────┬─────────┘
│ QPI/UPI Link │
└───────────────────────┘
NUMA balancing 的开销包括页迁移延迟和跨节点访存开销,可通过 /proc/sys/kernel/numa_balancing 开关控制。超算场景下通常手动绑核避免 NUMA 迁移。
6. eBPF 实战:观测 CFS 行为
利用现代 eBPF 工具可以零侵入地观察 CFS 调度行为。以下示例展示如何追踪进程上下文切换。
// trace_sch_switch.bpf.c
#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
struct event {
u32 prev_pid, next_pid;
u64 vruntime_delta;
char prev_comm[16], next_comm[16];
};
struct {
__uint(type, BPF_MAP_TYPE_PERF_OUTPUT);
__uint(key_size, sizeof(u32));
__uint(value_size, sizeof(struct event));
} events SEC(".maps");
SEC("tp_btf/sched_switch")
int BPF_PROG(trace_sched_switch, bool preempt, struct task_struct *prev,
struct task_struct *next) {
struct event e = {};
e.prev_pid = prev->pid;
e.next_pid = next->pid;
e.vruntime_delta = next->se.vruntime - prev->se.vruntime;
bpf_probe_read_kernel_str(e.prev_comm, sizeof(e.prev_comm), prev->comm);
bpf_probe_read_kernel_str(e.next_comm, sizeof(e.next_comm), next->comm);
bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &e, sizeof(e));
return 0;
}
char _license[] SEC("license") = "GPL";
调度延迟分析工具:
- runqlat(BCC):统计进程就绪队列等待时间分布
- runqlen(BCC):采样各 CPU 就绪队列长度
- schedstat:读取
/proc/schedstat获取调度域级统计数据
# 使用 runqlat 观察调度延迟
$ sudo runqlat-bpfcc 1 10
usecs : count distribution
0 -> 1 : 245 |********** |
2 -> 3 : 312 |************* |
4 -> 7 : 189 |******* |
8 -> 15 : 67 |** |
16 -> 31 : 23 |* |
32 -> 63 : 8 | |
64 -> 127: 3 | |
7. 性能调优实践
7.1 调度参数调优
# 调度周期长度,影响粒度与上下文切换开销
sysctl -w kernel.sched_latency_ns=6000000
# 最小调度粒度,小于此值不进行抢占
sysctl -w kernel.sched_min_granularity_ns=750000
# 唤醒抢占粒度,控制唤醒进程抢占当前进程的激进程度
sysctl -w kernel.sched_wakeup_granularity_ns=1000000
7.2 CPU 绑核与隔离
# 通过 taskset 将进程绑定到特定 CPU
taskset -c 2,3 ./high_perf_app
# 通过 cgroup CPU 配额限制容器资源
echo 50000 > /sys/fs/cgroup/cpu/myapp/cpu.cfs_quota_us
echo 100000 > /sys/fs/cgroup/cpu/myapp/cpu.cfs_period_us
7.3 实时性场景优化
对于延迟敏感型应用(金融交易、工业控制),需要结合 SCHED_FIFO 或 SCHED_DEADLINE:
chrt --fifo 99 ./trading_engine
chrt --deadline --sched-runtime=10000000 --sched-deadline=20000000 --sched-period=20000000 ./control_loop
8. CFS 未来发展趋势
CFS 仍在持续演进中,值得关注的几个方向:
- Extensible Scheduler(sched_ext):Linux 6.12 引入的用户态调度器框架,允许通过 eBPF 程序自定义调度策略
- Core Scheduling:在侧信道攻击威胁下,实现 SMT(超线程)级别的安全调度隔离
- EAS 与 CFS 协同:Energy Aware Scheduling 与 CFS 在 Arm big.LITTLE 架构的深度融合
- AI-driven 调度:利用机器学习预测进程行为,优化调度决策
总结
CFS 以其精妙的设计和卓越的自适应性,成为 Linux 内核中最为复杂的子系统之一。理解 vruntime、红黑树数据结构、组调度与 NUMA 感知机制,是每个系统程序员深入 Linux 内核的必经之路。结合 eBPF 等现代可观测工具,开发者能够在生产环境中精准定位调度瓶颈,实现极致性能优化。
深入理解 CFS,就是理解 Linux 如何在内核层面实现"公平"这一朴素而深刻的理念。
参考资料
- Linux 内核源码:kernel/sched/fair.c
- Documentation/scheduler/sched-design-CFS.rst
- BCC 工具集:runqlat, runqlen, offcputime
- Brendan Gregg — "Systems Performance" 2nd Edition

发表评论 取消回复