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_latency24ms调度周期内所有任务至少运行一次的时间窗提升吞吐,更宽调度粒度降低调度延迟,增加切换开销
min_granularity3ms任务在让出 CPU 前最小连续运行时间减少切换开销、提高吞吐交互延迟上升、CPU 碎片化
wakeup_granularity4ms唤醒抢占的 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)实现负载均衡:

  1. SMT 域(Hyperthread 级别):最先检查,将进程从空闲物理核的超线程迁移到另一个
  2. MC 域(多核共享 L2/L3):同 package 的 CPU 之间均衡
  3. DIE 域(芯片级别):同 NUMA 节点内跨 package 均衡
  4. 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_NORMALCFS按 vruntime 公平分配通用应用、NICE 调整权重
SCHED_BATCHCFS降低唤醒抢占(wakeup_granularity x2)批处理、编译任务、MapReduce
SCHED_IDLECFS极低权重(仅当系统空闲时运行)后台索引、低优先级脚本
SCHED_FIFORT先进先出,无时间片,可独占 CPU硬实时、工业控制
SCHED_RRRT时间片轮转,时间到降级到队列尾软实时、音视频编码
SCHED_DEADLINEEDF基于截止时间的最早截止时间最早优先机器人控制、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 关键指标):

指标优化前优化后变化
QPS32003850+20%
P50 延迟125ms118ms-6%
P99 延迟890ms520ms-42%
P999 延迟3200ms1450ms-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 更适合特定负载的加层而非替代。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部