引言

CPU 调度器是操作系统内核中最核心的组件之一,它决定了哪个进程在何时获得 CPU 时间。Linux 内核自 2.6.23 版本引入了 完全公平调度器(Completely Fair Scheduler, CFS),彻底取代了之前 O(1) 调度器,成为 Linux 默认的进程调度类。十余年来,CFS 经历了无数优化与扩展,但其核心设计理念 —— "完美多任务下的公平性" —— 始终未变。

本文将从 CFS 的设计哲学出发,深入剖析其基于虚拟运行时间(vruntime)的红黑树调度模型、组调度(Group Scheduling)与 cgroup 集成、负载均衡机制、NUMA 感知调度,并结合实际案例分析延迟敏感型工作负载的调优策略。

一、设计哲学:完美多任务下的公平性

CFS 的核心思想非常直观:如果有 N 个可运行进程,每个进程应该获得 1/N 的 CPU 时间。在理想硬件(无限精度、无上下文切换开销)上,这意味着每个进程都恰好获得等量的 CPU 时间。这种状态称为"完美多任务"(Ideal Preemptive Multitasking)。

现实世界中,上下文切换有开销,精度有限,但 CFS 尽可能逼近理想状态。与 O(1) 调度器使用时间片(timeslice)不同,CFS 不使用时间片概念,而是通过虚拟运行时间(vruntime) 来追踪每个进程已获得的"有效 CPU 时间"。

关键设计决策:

  • 无时间片:进程不在固定时间片结束时被抢占,而是当有更小的 vruntime 进程存在时才被抢占
  • 红黑树作为运行队列:按 vruntime 排序的进程集合,最左侧节点(最小 vruntime)即为下一个调度目标
  • 粒度控制:通过 sysctl_sched_min_granularity 和 sysctl_sched_wakeup_granularity 控制调度延迟与吞吐的平衡

二、虚拟运行时间(vruntime)

vruntime 是 CFS 调度的核心数据结构,定义于 struct sched_entity 中。其更新公式为:

delta_exec = now - se.exec_start;
delta_exec_weighted = delta_exec * (NICE_0_LOAD / se.load.weight);
se.vruntime  = delta_exec_weighted;

其中:

  • delta_exec:实际执行时间(纳秒)
  • NICE_0_LOAD:nice 值 0 对应的权重(1024)
  • se.load.weight:该进程的调度权重,由 nice 值转换而来

权重越高 → vruntime 增长越慢 → 实际获得更多 CPU 时间。例如,nice 值 -20 的进程权重约为 88761,nice 值 19 的进程权重仅为 15。这意味着 nice -20 进程的 vruntime 增长速度仅为 nice 19 进程的约 1/5900。

load_weight 的结构(kernel/sched/)包含以下关键字段:

struct load_weight {
    unsigned long weight;
    u32 invweight;   // premultiplied inverse: 2^32 / weight
};

使用 invweight 的乘法代替除法,是因为在热路径中乘法远快于除法。

三、调度器数据结构:cfs_rq 与 sched_entity

3.1 核心结构关系

struct cfs_rq {
    struct load_weight load;         // 总权重
    unsigned int nr_running;         // 可运行进程数
    u64 min_vruntime;                // 最小 vruntime(单调递增基准)
    struct rb_root_cached tasks_timeline; // 红黑树根
    struct sched_entity *curr;       // 当前运行实体
    // ...
};

struct sched_entity {
    struct load_weight load;         // 权重
    struct rb_node run_node;         // 红黑树节点
    u64 vruntime;                    // 虚拟运行时间
    u64 exec_start;                  // 本次执行开始时间
    u64 sum_exec_runtime;            // 总实际执行时间
    u64 prev_sum_exec_runtime;       // 上次切换前执行时间
    // ...
};

内核将进程(task_struct)、调度组实体、进程组都视为 sched_entity。一个 cfs_rq 可能对应一个 CPU 的运行队列、一个调度组,甚至一个 cgroup 的公平份额。

3.2 min_vruntime 的作用

min_vruntime 是红黑树中最左侧节点的 vruntime 值,作为所有进程的"vruntime 基准线"。新创建进程的 vruntime 会被设置为当前 min_vruntime(而非 0),避免长期休眠的进程在醒来后因 vruntime 过小而垄断 CPU(即睡眠者饥饿问题)。

当一个进程从一个 CPU 迁移到另一个 CPU 时,其 vruntime 会根据目标 cfs_rq 的 min_vruntime 进行调整,保证公平性不被破坏。

四、红黑树与调度选择

CFS 使用红黑树作为任务优先级队列,树键为 vruntime。最左侧节点即下一个被调度目标,时间复杂度为 O(1)(通过 rb_leftmost 缓存)。插入操作为 O(log n),在典型桌面负载(数十到数百进程)下开销极小。

调度主路径(pick_next_task_fair → pick_next_entity)流程:

  1. 从 cfs_rq->tasks_timeline 获取最左侧节点
  2. 如果存在 cfs_rq->next(用户通过 sched_setattr 设置的 next_buddy),且其 vruntime 小于最左侧节点 sysctl_sched_wakeup_granularity,则优先选择 next buddy
  3. 同理处理 last(上一次运行的进程,利用缓存局部性)
  4. 返回最左侧节点

这种 "last/next buddy" 机制允许用户空间显式提示调度器优先选择某个实体,对紧密通信的进程对(如生产者-消费者)特别有效。

五、唤醒抢占与放置策略

当一个进程从睡眠中被唤醒时,CFS 需要判断是否抢占当前运行进程。关键检查流程:

// check_preempt_wakeup:
granularity = sysctl_sched_wakeup_granularity;
if (vdiff= se->vruntime - curr->vruntime)                        
                    
点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部