引言

Linux 内核的进程调度器是操作系统最核心的组件之一,它负责决定哪个进程获得 CPU 时间。从早期的 O(n) 调度器到 O(1) 调度器,Linux 2.6.23 内核引入的完全公平调度器(CFS, Completely Fair Scheduler)彻底改变了进程调度的设计理念。本文将深入剖析 CFS 的红黑树核心数据结构、虚拟运行时间(vruntime)计算机制、调度延迟与粒度平衡策略,并提供生产环境中的实战调优指南。

一、CFS 的设计哲学

CFS 的核心理念可以用一句话概括:让每个进程获得"公平"的 CPU 份额。与传统的基于时间片轮询的调度器不同,CFS 不直接使用时间片,而是通过以下方式实现公平性:

  • 维护每个进程的虚拟运行时间(vruntime),记录进程已获得的加权 CPU 时间
  • 使用红黑树按 vruntime 对所有可运行进程排序
  • 始终选择 vruntime 最小的进程进行调度(最"亏欠" CPU 的进程)
  • 通过权重(weight)差异化分配给不同 nice 值的进程

这种设计避免了传统调度器中时间片到期后的批量切换开销,系统随着进程增多仅以 O(log N) 增长。

二、核心数据结构

2.1 struct sched_entity(调度实体)

CFS 以调度实体而非进程为基本单位,这意味着它可以调度单个进程、进程组甚至整个用户会话。每个调度实体包含以下关键字段:

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; // 上次调度时的总运行时间
    ...
};

2.2 CFS 红黑树(struct cfs_rq)

每个 CPU 运行队列上维护一个 CFS 运行队列:

struct cfs_rq {
    struct load_weight  load;       // 队列总权重
    unsigned int        nr_running; // 可运行进程数
    u64                 min_vruntime; // 树中最小 vruntime(基准值)
    struct rb_root      tasks_timeline;  // 红黑树根
    struct rb_node      *rb_leftmost;    // 最左节点(缓存,加速 pick_next)
    struct sched_entity *curr, *next, *last;
};

红黑树的关键性质充分发挥:最左侧节点始终是 vruntime 最小的进程,通过缓存 rb_leftmost 可在 O(1) 时间内找到下一个调度目标。

三、虚拟运行时间(vruntime)的计算

vruntime 是 CFS 实现加权公平的核心。其计算公式为:

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

实际上内核使用以下高效实现(kernel/sched/fair.c):

static void update_curr(struct cfs_rq *cfs_rq)
{
    struct sched_entity *curr = cfs_rq->curr;
    u64 now = rq_clock_task(rq_of(cfs_rq));
    u64 delta_exec;

    delta_exec = now - curr->exec_start;
    curr->exec_start = now;
    curr->sum_exec_runtime += delta_exec;
    
    // 核心:vruntime 增量 = delta_exec * (NICE_0_LOAD / weight)
    curr->vruntime += calc_delta_fair(delta_exec, curr);

    // 更新 min_vruntime 以保持基准单调递增
    update_min_vruntime(cfs_rq);
}

关键点解读:

  • nice 值越小(优先级越高),权重越大,vruntime 增长越慢,获得了更多实际 CPU 时间
  • calc_delta_fair() 使用位移运算避免浮点操作,NICE_0_LOAD=1024 对应 nice=0
  • 权重每级差约 1.25 倍,即 nice 值相差 1 级时 CPU 份额差异约 10%

四、调度时机与核心操作

4.1 进程入队(enqueue_entity)

当进程被唤醒或新创建时,会被插入 CFS 红黑树。如果该进程之前正在运行或刚从睡眠中醒来,其 vruntime 可能被调整以避免长时间等待导致的"饥饿反转":

static void enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags)
{
    bool renorm = !(flags & ENQUEUE_WAKEUP) || (flags & ENQUEUE_MIGRATED);
    bool curr = cfs_rq->curr == se;

    // 处理 vruntime 归一化
    if (renorm && curr)
        se->vruntime += cfs_rq->min_vruntime;

    update_curr(cfs_rq);
    
    if (renorm && !curr)
        se->vruntime += cfs_rq->min_vruntime;

    // 实际入队:位置 = max(vruntime, min_vruntime - sysctl_sched_latency)
    if (flags & ENQUEUE_WAKEUP)
        place_entity(cfs_rq, se, 0);

    account_entity_enqueue(cfs_rq, se);
    if (flags & ENQUEUE_WAKEUP)
        check_schedstat_required();

    if (!curr)
        __enqueue_entity(cfs_rq, se);
    se->on_rq = 1;
}

4.2 进程出队(dequeue_entity)

当进程睡眠或停止时,从红黑树中移除,同时更新 min_vruntime:

static void dequeue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags)
{
    update_curr(cfs_rq);
    
    // 保存当前 vruntime 到 prev,减去 min_vruntime 保持区间相对值
    if (se != cfs_rq->curr)
        __dequeue_entity(cfs_rq, se);
    se->on_rq = 0;
    
    account_entity_dequeue(cfs_rq, se);
    // 出队时利用 se 的 vruntime 更新 min_vruntime
}

4.3 选择下一个进程(pick_next_entity)

static struct sched_entity *pick_next_entity(struct cfs_rq *cfs_rq)
{
    // 直接从缓存的最左子节点获取
    struct sched_entity *se = __pick_first_entity(cfs_rq);
    
    struct sched_entity *left = se;
    struct sched_entity *second;
    
    if (cfs_rq->next && entity_before(cfs_rq->next, left))
        second = exchange(&cfs_rq->next, left);
    ...
    return left;
}

这就是为什么 rb_leftmost 缓存重要——在 tick 触发的抢占检查时,可以快速判断是否需要抢占当前进程。

五、调度延迟与粒度——精密的平衡

CFS 需要在两个目标间取得平衡:低延迟(快速响应) 和 高吞吐(减少切换开销)。内核通过以下 sysctl 参数控制:

# 默认值(内核 5.x+)
kernel.sched_latency_ns       = 24000000   // 24ms,一个调度周期
kernel.sched_min_granularity_ns = 3000000   // 3ms,最小运行时间
kernel.sched_wakeup_granularity_ns = 4000000 // 4ms,唤醒抢占粒度

核心规则:

调度周期  = max(sched_latency_ns, nr_running * sched_min_granularity_ns)
进程时间片 = 调度周期 * (本进程权重 / 总权重)

这意味着当系统进程数少时(<8 个),所有进程能在 sched_latency 内至少运行一次;当进程数多时,通过 min_granularity 保证切换开销可控。

六、组调度与层级调度(cgroups 集成)

CFS 天然支持控制组(cgroup)调度,形成层次化的带宽分配:

  • shares:定义组间 CPU 份额权重(默认 1024)
  • cfs_quota/cfs_period:限制组的最大 CPU 使用率
  • 层级限制:组的 quota 不能超过其父组的 quota

例如,将 Docker 容器的 CPU 限制为 50%:

docker run --cpus=0.5 ...
# 等价于 cpu.cfs_quota_us=50000 cpu.cfs_period_us=100000
// 内核校验逻辑:检查组的带宽是否超限
static int tg_set_cfs_bandwidth(struct task_group *tg, u64 period, u64 quota)
{
    ...
    // 父组的可用 quota 检查
    if (!cfs_bandwidth_can_quota(tg, quota))  
        return -EINVAL;
}

七、NUMA 感知调度

现代多核/多插槽系统的 NUMA 架构给 CPU 调度带来了新挑战。CFS 通过以下机制处理:

  • NUMA balancing:自动将进程迁移到靠近其内存的 NUMA 节点
  • sched_numa_balancing:通过缺页中断扫描进程的内存访问模式
  • sched_scan_period_min/max:控制扫描频率(默认 100ms ~ 6s)
  • 负载均衡:pull/push 跨节点迁移任务时考虑 NUMA 亲和性

八、实时调度策略

CFS 管理普通(SCHED_NORMAL)任务,而实时任务由独立的调度类处理。调度优先级链为:

stop_sched_class  (最高,不可抢占)
dl_sched_class    (SCHED_DEADLINE,基于 EDF 算法)
rt_sched_class    (SCHED_FIFO / SCHED_RR,固定优先级)
fair_sched_class  (CFS,SCHED_NORMAL / SCHED_BATCH / SCHED_IDLE)
idle_sched_class (最低,仅空闲)

注意:当存在可运行的实时任务时,CFS 会被完全抢占,除非实时任务主动让出 CPU。

九、性能分析工具

分析 CFS 相关性能问题可参考以下工具:

  • perf sched record/report:记录和分析调度延迟
  • perf sched latency:统计每个任务的调度延迟分布
  • perf sched map:可视化 CPU 迁移和空闲时间
  • /proc/<pid;/schedstat:进程级调度统计(运行时间/等待时间/切换次数)
  • /sys/kernel/debug/sched/debug:调度器调试信息(需开启 CONFIG_SCHED_DEBUG)
$ perf sched record -- sleep 10
$ perf sched latency --sort max
 -------------------------------------------------------------------------------
  Task                  |   Runtime ms  | Switches | Average delay ms | Max delay ms |
 -------------------------------------------------------------------------------
  kworker/0:1H-(398)    |      4.948 ms |       21 |          0.278   |     1.689    |
  MainThread-(1586319)  |     11.308 ms |       20 |          1.559   |     4.841    |
 -------------------------------------------------------------------------------

十、生产环境调优指南

10.1 交互式桌面系统

# 降低调度延迟,提升响应速度
sysctl -w kernel.sched_latency_ns=12000000
sysctl -w kernel.sched_min_granularity_ns=1500000
sysctl -w kernel.sched_wakeup_granularity_ns=2000000

10.2 高吞吐服务器

# 增大调度周期,减少上下文切换
sysctl -w kernel.sched_latency_ns=48000000
sysctl -w kernel.sched_min_granularity_ns=6000000
sysctl -w kernel.sched_wakeup_granularity_ns=8000000

10.3 低延迟服务(数据库/交易系统)

# CPU 绑核 + 实时优先级 + 调度参数调优
chrt -f 99 ./trading_engine              # SCHED_FIFO 优先级99
isocpus=4-7 nosoftlockup nohz_full=4-7    # 内核参数隔离核心
sysctl -w kernel.sched_rt_runtime_us=1000000  # 允许实时任务占满
# 非关键进程移到掩码外
cset shield --cpu=0-3 --kthread=on

10.4 排查调度抖动

当发现 CPU 使用率正常但执行时间抖动大时,按以下顺序排查:

  1. 检查 perf sched latency 的 Max delay 是否异常
  2. 查看 kernel.watchdog 是否开启(NMI watchdog 引发中断延迟)
  3. 检查是否有过多的 timer tick 中断(CONFIG_HZ_PERIODIC vs CONFIG_NO_HZ_FULL)
  4. 排查 D 状态进程阻塞——ps aux | awk '~/D/ {print}'
  5. 检查 cgroup cpu.stat 的 throttled_time 是否增长

总结

CFS 通过红黑树和 vruntime 的优雅设计,在简洁的数据结构中实现了复杂的加权公平调度。作为 Linux 内核中最精妙的子系统之一,理解 CFS 不仅有助于系统性能调优,更能帮助开发者建立"公平调度"的系统设计思维。记住三个关键点:vruntime 决定公平、红黑树保证效率、粒度参数控制平衡——掌握这三者,方能驾驭 Linux 进程调度的精髓。

(本文基于 Linux 6.x 内核源码分析,适用于 5.x 及 6.x 系列版本)

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部