一、调度器演进:为什么需要 CFS?

Linux 调度器的演进是一部不断追求"公平"与"高效"的历史。早期 Linux 2.4 使用 O(n) 遍历所有进程选择最高优先级任务,每次调度决策代价随进程数线性增长。2.6 初期引入 O(1) 调度器,使用 active/expired 双数组实现常数时间选取,但其交互式进程识别算法(sleep_avg 奖惩机制)复杂且容易误判——"I/O 密集型"与"CPU 密集型"的启发式推断在实际负载中经常失效。

2007 年 Ingo Molnár 提交 CFS(Completely Fair Scheduler),彻底抛弃"时间片"概念,改为"谁欠的 CPU 最少就先给谁运行"。CFS 的核心洞察是:调度公平 = 每个进程获得的 CPU 时间与其权重成正比。这一设计使得调度算法从启发式分类回归到简洁的数学模型。

二、CFS 核心数据结构

2.1 sched_entity 与 vruntime

每个进程的调度实体(sched_entity)中最关键的字段是 vruntime(virtual runtime),记录进程在虚拟时钟下的累计运行时间:

// kernel/sched/sched.h
struct sched_entity {
    struct load_weight  load;       // 权重(nice 值转换)
    struct rb_node      run_node;   // 红黑树节点
    u64                 exec_start; // 本次开始执行时间
    u64                 sum_exec_runtime; // 总执行时间
    u64                 vruntime;   // 虚拟运行时间
    u64                 prev_sum_exec_runtime; // 上次总执行时间
    ...
};

vruntime 的增长公式为:

vruntime += (实际运行时间) * (NICE_0_LOAD / 进程权重)

权重越高的进程(nice 值越小/优先级越高),vruntime 增长越慢,因此在红黑树中位置越靠前,被调度次数越多。nice 值每差 1 级,权重相差约 1.25 倍(10% CPU 差距),这是由 prio_to_weight[] 数组决定的经典比例。

2.2 红黑树:选择 vruntime 最小的进程

CFS 使用红黑树(cfs_rq->tasks_timeline)以 vruntime 为 key 管理所有可运行进程。调度时只需取树的最左节点(__pick_next_entity()),即 vruntime 最小的进程——也就是最"亏欠"CPU的那个:

static struct sched_entity *__pick_next_entity(struct cfs_rq *cfs_rq)
{
    struct rb_node *left = cfs_rq->tasks_timeline.rb_node;
    // 实际通过 cached 的 rb_leftmost 直接获取
    return rb_entry(cfs_rq->leftmost, struct sched_entity, run_node);
}

插入/删除/查找均为 O(log n),即使面对数万进程的高效调度也能保持稳定延迟。

2.3 min_vruntime 与防止饥饿

每个运行队列维护 min_vruntime,表示队列中曾经出现过的最小 vruntime。新建进程或唤醒的进程的 vruntime 会被设置为 max(vruntime, min_vruntime),防止新进程因 vruntime 从零开始而长期霸占 CPU。但过度补偿也会导致等待进程被加入时反而处于劣势,因此内核还会通过 min_vruntime 适度追赶。

三、调度时机与抢占

3.1 触发调度的时机

CFS 在以下场景触发调度决策:

  • 主动让出:进程睡眠(schedule())、等待 I/O、获取锁失败
  • 中断返回:时钟 tick(scheduler_tick())检测当前进程的 ideal_runtime 是否耗尽
  • 唤醒抢占:唤醒的进程 vruntime 显著小于当前进程时(check_preempt_wakeup()),立即抢占
  • 新建进程:wake_up_new_task() 将新进程插入红黑树并检查抢占

3.2 时钟 tick 与理想调度周期

CFS 不分配固定时间片,而是使用"理想调度周期"(sysctl_sched_latency,默认 6ms)。对于 N 个可运行进程,每个进程的 ideal_runtime = latency / N。当一个进程的实际运行时间超过 ideal_runtime 时,task_tick_fair() 会调用 resched_curr() 设置 TIF_NEED_RESCHED 标志,在下次调度点切换。

为防止进程过多时 ideal_runtime 过短导致频繁切换,内核设置 sysctl_sched_min_granularity(默认 0.75ms)作为最低保证粒度。

四、组调度(cgroups CPU 子系统)

4.1 CFS 组调度

CFS 支持组调度(CONFIG_FAIR_GROUP_SCHED),允许将进程分组(对应 cgroup),每组有独立的 sched_entity,在父级红黑树中参与竞争。两个层级:

  • 组间公平:各 cgroup 的 vruntime 反映该组整体CPU欠额
  • 组内公平:同一 cgroup 内各进程按 CFS 规则竞争

用户通过 /sys/fs/cgroup/cpu/<group>/cpu.shares 设置组权重(默认 1024)。

4.2 CPU 带宽控制(cpu.cfs_quota_us)

除了按比例分配,CFS 还支持硬上限:通过 cpu.cfs_period_us 和 cpu.cfs_quota_us 组合,限制一个 cgroup 在固定周期内最多使用的 CPU 时间:

/sys/fs/cgroup/cpu/myapp/cpu.cfs_period_us = 100000  # 100ms
/sys/fs/cgroup/cpu/myapp/cpu.cfs_quota_us  =  50000  # 50ms = 0.5核

超出配额的进程会在当前周期剩余时间中被节流(throttled),直到下一个周期 replenish。

五、实时调度类

5.1 SCHED_FIFO 与 SCHED_RR

Linux 内核提供两种 POSIX 实时调度策略:

  • SCHED_FIFO:先进先出,不使用时间片。高优先级进程会一直运行直到主动放弃(睡眠/让出),同优先级进程不轮转。
  • SCHED_RR:轮转(Round-Robin),同优先级进程共享时间片(sched_rr_timeslice,默认 100ms),用完后放到队列末尾。

实时进程的优先级(1-99,1 最低 99 最高)始终高于普通 CFS 进程。任何可运行的实时进程都会抢占 CFS 进程。

5.2 SCHED_DEADLINE:最早截止时间优先(EDF)

Linux 3.14 引入 SCHED_DEADLINE,基于 EDF(Earliest Deadline First)算法,配合 CBS(Constant Bandwidth Server)保证实时任务的截止时间:

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);

三个参数约束条件:runtime ≤ deadline ≤ period。内核通过可调度性测试保证在最坏情况下也不会超限,适合音视频处理、控制回路等硬实时场景。

5.3 实时调度的优先级继承

实时场景中经典问题是优先级反转:高优先级任务因等待低优先级任务持有的锁而被阻塞。Linux 内核通过 rt_mutex 的优先级继承(Priority Inheritance)协议解决:当高优任务阻塞在 rt_mutex 上时,临时提升锁持有者的优先级到等待者级别,确保中间优先级任务无法抢占锁持有者。

六、生产环境调优实践

6.1 chrt 设置实时优先级

# 以 SCHED_FIFO 优先级 50 运行关键音频处理程序
chrt -f 50 ./audio_processor

# 以 SCHED_RR 优先级 80 运行实时控制任务
chrt -r 80 ./control_loop

# 以 SCHED_DEADLINE 运行硬实时任务
sched_setattr --dl_runtime=10000000 --dl_deadline=20000000 --dl_period=20000000 -p 0 ./rt_task

6.2 taskset / cpuset 绑核

# 将进程绑定到 CPU 2-3
taskset -c 2-3 ./db_server

# 使用 cpuset cgroup 隔离核
mkdir /sys/fs/cgroup/cpuset/rt_tasks
echo "2-3" > /sys/fs/cgroup/cpuset/rt_tasks/cpuset.cpus
echo "0"   > /sys/fs/cgroup/cpuset/rt_tasks/cpuset.mems
echo $PID  > /sys/fs/cgroup/cpuset/rt_tasks/cgroup.procs

6.3 关键 sysctl 参数

# 调度粒度(影响延迟与吞吐的权衡)
kernel.sched_min_granularity_ns = 1000000     # 1ms
kernel.sched_latency_ns       = 6000000      # 6ms
kernel.sched_wakeup_granularity_ns = 1500000  # 唤醒抢占容忍度

# RCU 宽限期kip实时性
kernel.sched_rt_period_us  = 1000000  # 实时进程 1s 内最多跑 950ms
kernel.sched_rt_runtime_us =  950000  # 保留 5% 给普通进程

6.4 调度延迟诊断

# 跟踪调度延迟(sched_wakeup → sched_switch 的时差)
perf sched record -- sleep 5
perf sched latency

# 实时监控特定进程的 vruntime 与上下文切换
perf trace -e sched:sched_switch -p $PID

# 查看进程调度统计
cat /proc/$PID/sched
se.vruntime                  :        1234567.89
nr_switches                  :              1024
nr_voluntary_switches        :               800
nr_involuntary_switches      :               224

七、调度器层次:stop → dl → rt → fair → idle

内核调度类按优先级排列:

stop_sched_class      # 停机调度(CPU 热插拔)
dl_sched_class        # SCHED_DEADLINE (EDF + CBS)
rt_sched_class        # SCHED_FIFO / SCHED_RR
fair_sched_class      # CFS (SCHED_NORMAL / BATCH)
idle_sched_class      # IDLE(无任务时的占位)

调度时按此顺序遍历,for_each_class 依次检查是否有可运行任务。实时任务始终被优先考虑,空闲任务仅在无其他任务时运行。

八、NUMA 感知调度

在多路服务器上,NUMA(Non-Uniform Memory Access)拓扑对调度性能至关重要。内核的 NUMA 平衡机制包括:

  • NUMA balancing(AutoNUMA):通过页错误统计热页位置,自动将进程迁移到靠近其内存的 CPU
  • Sched NUMA:调度器倾向于在 NUMA 内选择目标 CPU,减少跨节点流量
  • numactl:手动绑定进程到指定 NUMA 节点

对于高频交易、HPC 等场景,建议关闭 AutoNUMA(/proc/sys/kernel/numa_balancing)并使用显式 cpuset + numactl 绑定的确定性策略。

九、未来方向:extensible scheduler 与 BPF 调度器

Linux 6.12+ 引入 sched_ext(Scheduler Extension),允许通过 BPF 程序实现自定义调度策略而无需修改内核源码:

// 自定义 BPF 调度器核心逻辑
SEC("struct_ops/bpf_ext_ops")
int BPF_PROG(enqueue, struct task_struct *p, u64 enq_flags)
{
    // 自定义入队策略,如基于 LLM 推理延迟的优先级选择
    scx_bpf_dispatch(p, SCX_DSQ_GLOBAL, SCX_SLICE_DFL, enq_flags);
    return 0;
}

这意味着用户可以像写 eBPF 一样写调度器——不再需要漫长的内核补丁 review 周期。社区已有 scx_rustland、scx_rlfifo 等参考实现,为实时音视频、游戏服务器等场景提供了低门槛的自定义调度能力。

十、总结:选型速查

场景推荐策略关键参数
Web 服务器 / 数据库CFS(默认)nice 调整 + cpu.shares
低延迟音视频SCHED_RR / SCHED_FIFOchrt + 绑核 + isolcpus
批处理 / CI/CD 构建CFS (SCHED_BATCH)nice +19,吞吐量优先
控制系统 / 嵌入式SCHED_DEADLINEruntime/deadline/period 精细调谐
需要自定义策略sched_ext (BPF)scx_rustland / scx_rlfifo
点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部