1. CFS 设计哲学:为何抛弃传统时间片轮转
O(1)调度器在 2.6.23 之前主导 Linux,但面对多核、交互式负载和服务器负载混合场景时存在严重缺陷:固定时间片在多核下权重不公平,active/expired 数组导致交互进程等待时间不确定。CFS(Completely Fair Scheduler)由 Ingo Molnár 提出,核心思想颠覆性地放弃了时间片概念——取而代之的是虚拟运行时(virtual runtime, vruntime)。
CFS 的核心保证:当系统中有 N 个权重相同的可运行进程时,每个进程在 wall-clock 时间 T 内应获得 T/N 的 CPU 时间。若进程 i 权重为 w_i,则其 vruntime 增长速率与 1/w_i 成正比:
dv/dt vruntime_i = delta_t_clock * 1024 / w_i
其中 1024 是 NICE_0_LOAD(对应 nice 0 的基准权重)。权重越高的进程 vruntime 增长越慢,因此在红黑树中停留更久,获得更多 CPU 时间。
2. vruntime 计算与 update_curr 核心路径
每次时钟 tick 触发时,scheduler_tick() 调用 curr->task_tick(rq, curr, 0),最终执行 CFS 类的 entity_tick():
// kernel/sched/fair.c - entity_tick()
static void entity_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr, int queued)
{
update_curr(cfs_rq);
update_load_avg(cfs_rq, curr, UPDATE_TG);
update_cfs_group(cfs_rq, curr); // cgroup 带宽控制
if (cfs_rq->nr_running > 1)
check_preempt_tick(cfs_rq, curr);
}
update_curr() 做了三件事:
- 统一时钟:now = sched_clock() 获取单调递增的纳秒级硬件时钟
- 计算差值:delta_exec = now - curr->exec_start 得到实际运行时间
- 累积 vruntime:curr->vruntime += calc_delta_fair(delta_exec, curr),其中 fair 计算把 delta_exec 乘以 NICE_0_LOAD/weight
关键细节:vruntime 不是全局的——CFS 通过 cfs_rq->min_vruntime 追踪最左红黑树节点的 vruntime,用于跨 CPU 公平性校准。新进程的初始 vruntime 设为 cfs_rq->min_vruntime(而非 0),避免新 fork 的进程因饥饿被饿死。
3. 红黑树选取:pick_next_entity 与 __schedule()
CFS 用红黑树(按 vruntime 排序)取代 O(1) 的优先级数组。选取下一个运行进程:
// kernel/sched/fair.c 简化逻辑
static struct sched_entity *pick_next_entity(struct cfs_rq *cfs_rq)
{
struct sched_entity *left = __pick_first_entity(cfs_rq); // 最左节点 O(1)
struct sched_entity *se = left;
if (cfs_rq->skip == left) // 跳过被 set_next_buddy 标记的进程
se = __pick_next_entity(left);
// 抢占检查:当前运行进程 vruntime 比最左节点大太多
if (left && wakeup_preempt_entity(left, curr) == 1)
se = left;
return se;
}
复杂度:插入 O(log n)、删除 O(log n)、最左选取 O(1)(通过红黑树增强,缓存最左指针)。大规模场景测试显示:红黑树在百万级可运行进程下仍能维持调度延迟在微秒级。
4. 三大核心调优参数:吞吐 vs 延迟的拉锯战
CFS 调度行为受三个 sysctl 参数统治:
| 参数 | 默认值 | 含义 | 增大效果 | 减小效果 |
|---|---|---|---|---|
| sched_latency | 24ms | 调度周期内所有任务至少运行一次的时间窗 | 提升吞吐,更宽调度粒度 | 降低调度延迟,增加切换开销 |
| min_granularity | 3ms | 任务在让出 CPU 前最小连续运行时间 | 减少切换开销、提高吞吐 | 交互延迟上升、CPU 碎片化 |
| wakeup_granularity | 4ms | 唤醒抢占的 vruntime 容差阈值 | 减少抢占、稳定吞吐 | 提高交互响应但增加抖动 |
实际周期计算:period = max(sched_latency, min_granularity * nr_running)。8 核服务器运行 32 个进程时:period = max(24ms, 3ms*32) = 96ms,每个进程分得 3ms。这意味着在负载较重时,调度周期拉长,交互延迟会线性增长。
调优实战:数据库 OLTP 场景(高 qps + 低延迟需求)建议:
sysctl -w kernel.sched_latency_ns=6000000 # 6ms 缩短周期
sysctl -w kernel.sched_min_granularity_ns=750000 # 750μs 最小时间片
sysctl -w kernel.sched_wakeup_granularity_ns=1000000 # 1ms 抢占容差
注意:将 sched_latency 降到低于 min_granularity * nr_running 是无效的,因为 min_granularity 会强制覆盖。
5. load_balance:跨核迁移与调度域层级
现代服务器多为多 NUMA 节点多核架构,CFS 通过分层的调度域(sched_domain)实现负载均衡:
- SMT 域(Hyperthread 级别):最先检查,将进程从空闲物理核的超线程迁移到另一个
- MC 域(多核共享 L2/L3):同 package 的 CPU 之间均衡
- DIE 域(芯片级别):同 NUMA 节点内跨 package 均衡
- NUMA 域(节点级别):跨 NUMA 均衡,开销最大,受 numa_balancing 控制
均衡触发时机:
- IDLE CPU 时立即调用 load_balance()
- 每次 tick 在 run_rebalance_domains() 中按 domain 层级调用 balance()
- 周期性均衡(通过 softirq SCHED_SOFTIRQ 或工作队列)
迁移成本的关键权衡:NUMA 跨节点迁移虽然能均衡负载,但远程内存访问延迟比本地高 2-4 倍(100ns vs 300ns+)。因此 Linux 的 Auto NUMA Balancing(numabalancing=on)采用采样策略:通过在 fault 时扫描页访问模式,仅在收益大于成本时才迁移。
6. 实时调度策略 vs CFS
Linux 对实时任务的支持本质上优先级高于 CFS(实时优先级 1-99 映射到 rt_sched_class)。CFS 的调度类优先级链:stop_sched_class > dl_sched_class > rt_sched_class > fair_sched_class > idle_sched_class。
| 策略 | 类型 | 关键特性 | 典型场景 |
|---|---|---|---|
| SCHED_NORMAL | CFS | 按 vruntime 公平分配 | 通用应用、NICE 调整权重 |
| SCHED_BATCH | CFS | 降低唤醒抢占(wakeup_granularity x2) | 批处理、编译任务、MapReduce |
| SCHED_IDLE | CFS | 极低权重(仅当系统空闲时运行) | 后台索引、低优先级脚本 |
| SCHED_FIFO | RT | 先进先出,无时间片,可独占 CPU | 硬实时、工业控制 |
| SCHED_RR | RT | 时间片轮转,时间到降级到队列尾 | 软实时、音视频编码 |
| SCHED_DEADLINE | EDF | 基于截止时间的最早截止时间最早优先 | 机器人控制、DO-178C 航电 |
SCHED_DEADLINE 使用 GEDF(Global Earliest Deadline First)算法,更适合硬实时场景:
struct sched_attr {
.sched_policy = SCHED_DEADLINE,
.sched_runtime = 10 * 1000 * 1000, // 10ms 运行预算
.sched_deadline = 20 * 1000 * 1000, // 20ms 截止期
.sched_period = 20 * 1000 * 1000, // 20ms 周期
};
sched_setattr(pid, &attr, 0);
7. cgroup v2 cpu controller:容器时代的调度隔离
cgroup v2 在 cpu 调度层面相较于 v1 有根本性改变——它引入了权重模型 + 带宽限制双重控制:
- cpu.weight(1-10000,默认 100):类似 nice 值,控制相对比例。权重 200 的 cgroup 在同等条件下获得 2x CPU
- cpu.max(格式 ):硬限额,如 50000 100000 表示每 100ms 最多运行 50ms(0.5 核上限)
- cpu.uclamp.min / uclamp.max:约束进程的 nice 值上下限,确保关键 QoS
写入示例:限制某容器最多使用 2 个核、允许突发到 3 核:
# /sys/fs/cgroup/mycontainer/cpu.max
200000 100000 # 每 100ms 可消耗 200ms CPU runtime = 2 核
8. 生产环境调优实战:80 核 NUMA 服务器在线推理
某 80 核 / 2 NUMA 节点的在线推理服务(混合 CPU+GPU),遭遇调度器导致的尾延迟 P99 不稳定问题。通过以下组合优化使 P99 延迟下降 42%:
# 1. 缩短 CFS 调度周期降低交互延迟
sysctl -w kernel.sched_latency_ns=4000000
sysctl -w kernel.sched_min_granularity_ns=500000
sysctl -w kernel.sched_wakeup_granularity_ns=800000
# 2. 禁用跨 NUMA 自平衡(对绑定负载无用且有害)
sysctl -w kernel.numa_balancing=0
# 3. 推理进程绑核到 NUMA node 0 的物理核(避开超线程)
taskset -c 0-39 ./inference_server --num_threads=40
# 4. 调度策略对关键实时线程使用 SCHED_FIFO + 高优先级
chrt -f 50 ./inference_worker
# 5. 开启 NO_HZ_FULL 在全核绑定时跳过 tick
nohz_full=0-39 rcu_nocbs=0-39
基准测试对比(wrk2 400 并发 QPS,P99 关键指标):
| 指标 | 优化前 | 优化后 | 变化 |
|---|---|---|---|
| QPS | 3200 | 3850 | +20% |
| P50 延迟 | 125ms | 118ms | -6% |
| P99 延迟 | 890ms | 520ms | -42% |
| P999 延迟 | 3200ms | 1450ms | -55% |
| CPU 调度开销(perf) | 4.2% | 2.1% | -50% |
9. CFS 内部数据结构与性能监控
调试调度问题时的关键探针:
# 当前进程的 vruntime 和调度统计
cat /proc/self/sched | grep -E "vruntime|nr_switches|se.avg"
# CFS 运行队列状态(per-cpu)
cat /proc/sched_debug | grep -A5 "cfs_rq"
# 调度延迟直方图(同时开启 perf 探针)
perf record -e sched:sched_switch,sched:sched_migrate_task -a
# ftrace 跟踪调度决策路径
echo "sched_switch sched_wakeup sched_migrate_task" > /sys/kernel/debug/tracing/set_event
cat /sys/kernel/debug/tracing/trace_pipe
监控关键点:sched_lag_ns(/sys/kernel/debug/sched/lag)、sched_wait_runtime,以及 perf bench sched 微基准。
当 vruntime 差异超过 sched_lag_ns 时,内核会判定系统过度调度过重并主动强制进程睡眠(TASK_UNINTERRUPTIBLE),这是内核自我保护的边界机制——意味着调度器的公平保证正在崩溃。
10. 前沿演进:sched_ext 与 eBPF 调度器
Linux 6.12 引入 sched_ext(Scheduler Extending),允许通过 eBPF 在运行时加载自定义调度器类并替换 CFS/RT。这为 AI 推理、大数据批处理等异构负载提供了终极灵活性:
- 可在用户态实现特定拓扑感知的调度策略
- 通过 BPF 映射实时调整权重和时间片
- 无需重新编译内核,调度策略热加载
- 对 Kubernetes kubelet 的 CPU Manager 策略是天然补充
// sched_ext BPF 调度器骨架
SEC("struct_ops/sched_ext_ops")
void BPF_STRUCT_OPS(enqueue, struct task_struct *p)
{
// 自定义入队逻辑:权重按 container QoS 调整
}
但 sched_ext 仍在早期阶段:CFS 作为通用调度器的整体公平性在可预见的未来仍不可替代,sched_ext 更适合特定负载的加层而非替代。

发表评论 取消回复