引言

完全公平调度器(Completely Fair Scheduler, CFS)是 Linux 内核 2.6.23 版本引入的进程调度器,它彻底改变了 Linux 调度器的设计理念。CFS 的核心思想不是基于传统的时间片分配,而是基于「虚拟运行时间」(virtual runtime, vruntime)的纳红黑树(RB-Tree)模型,实现真正意义上的「完全公平」。

本文将深入剖析 CFS 调度器的底层机制,包括虚拟运行时间计算、红黑树调度实体、组调度(Group Scheduling)、带宽控制(Bandwidth Control),并结合 eBPF 工具进行实战观测与调优。

1. CFS 核心设计理念

CFS 摒弃了传统 O(1) 调度器的 runqueue + active/expired 数组架构,转而采用一种更为优雅的方案:

  • 目标:让每个任务获得「完全公平」的 CPU 时间份额
  • 方法:维护每个任务的虚拟运行时间,总是选择 vruntime 最小的任务运行
  • 数据结构:红黑树(rbtree),以 vruntime 为键,O(log N) 查找和插入

这意味着 CFS 不需要时间片的概念——任务的运行时间被转化为 vruntime,调度器只需要选择树最左边的节点(最小 vruntime)即可获得下一个要运行的任务。

2. 虚拟运行时间(vruntime)详解

vruntime 是 CFS 最核心的概念,它决定了任务在红黑树中的位置。

2.1 vruntime 计算公式

vruntime += delta_exec * (NICE_0_LOAD / weight);

// delta_exec: 实际运行时间(纳秒)
// NICE_0_LOAD: nice 0 的权重(1024)
// weight: 当前任务的权重(由 nice 值决定)

关键洞察:优先级越高的任务(nice 值越小),weight 越大,vruntime 增长越慢,因此能在红黑树中停留更久,获得更多 CPU 时间。

2.2 Nice 值与权重对照

Nice值权重CPU 份额比(相对 nice 0)
-2088761约 13.3x
-101501约 3.0x
010241.0x(基准)
10127约 0.08x
1915约 0.01x

2.3 最小粒度与调度周期

// kernel/sched/fair.c
#define SCHED_NR_MAX_RUNNABLE_SCHED 32
#define MIN_GRANULARITY (0.75 * HZ)  // 最小运行时间粒度
#define TARGET_LATENCY  (6 * HZ)     // 目标延迟 6ms

// 调度周期计算
if (nr_running > 1)
    sysctl_sched_min_latency = sysctl_sched_min_latency * nr_running;

CFS 会确保每个可运行任务至少运行 SCHED_MIN_GRANULARITY 才被抢占,避免过多任务导致的调度开销。

3. 红黑树调度队列(cfs_rq)

3.1 核心数据结构

struct cfs_rq {
    struct load_weight load;       // 队列总权重
    unsigned int nr_running;       // 可运行任务数
    unsigned int h_nr_running;     // 包含组调度的层级计数

    u64 min_vruntime;              // 队列最小 vruntime(单调递增)
    struct rb_root_cached tasks_timeline; // 红黑树根节点

    struct sched_entity *curr;     // 当前运行实体
    struct sched_entity *next;     // 下一个要运行(用于skip机制)
    struct sched_entity *last;     // 上一个运行(用于缓存预热)
};

3.2 调度实体(sched_entity)

struct sched_entity {
    struct load_weight load;       // 实体权重
    struct rb_node run_node;       // 红黑树节点
    unsigned int on_rq;            // 是否在运行队列
    
    u64 vruntime;                  // 虚拟运行时间
    u64 exec_start;                // 本次开始运行时间
    u64 sum_exec_runtime;          // 总实际运行时间
    u64 prev_sum_exec_runtime;     // 上一次总运行时间
    
    // 组调度相关
    struct cfs_rq *cfs_rq;         // 所属 CFS 队列
    struct cfs_rq *my_q;           // 组调度时的子队列
};

4. 组调度(Group Scheduling)

Linux 支持通过 cgroup 将 CPU 时间在不同用户组之间按比例分配。每个 cgroup 拥有独立的 CFS 队列,实现层次化资源控制。

4.1 两层调度架构

                    ┌──────────────────────────┐
                    │    task_group (根组)     │
                    │    cfs_rq (系统级)       │
                    └───────────┬──────────────┘
                                │
              ┌─────────────────┼─────────────────┐
              ▼                 ▼                 ▼
    ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
    │ cgroup A (50%)   │ │ cgroup B (30%)   │ │ cgroup C (20%)   │
    │ cfs_rq_A         │ │ cfs_rq_B         │ │ cfs_rq_C         │
    └────────┬─────────┘ └────────┬─────────┘ └────────┬─────────┘
             │                    │                    │
        ┌────┴────┐          ┌────┴────┐         ┌─────┴─────┐
        ▼         ▼          ▼         ▼         ▼           ▼
     task_1   task_2      task_3   task_4   task_5      task_6

每个 cgroup 的 shroups 值决定了它的 CPU 份额。当多个 cgroup 竞争 CPU 时,内核使用 DPE(Distributed Propagation Engine)来公平地传播 CPU 带宽。

5. 带宽控制(Bandwidth Control / CFS Throttling)

除了基于份额的调度,CFS 还引入了带宽控制机制,用于限制 cgroup 在固定周期内的最大 CPU 使用量。

5.1 带宽控制参数

// 每周期配额(微秒)
echo 500000 > /sys/fs/cgroup/cpu/mygroup/cpu.cfs_quota_us

// 周期长度(微秒)
echo 1000000 > /sys/fs/cgroup/cpu/mygroup/cpu.cfs_period_us

// 表示:每 1000ms 内最多使用 500ms CPU(即 0.5 个 CPU 核心)

5.2 带宽控制内核实现

// 带宽检查核心逻辑
static int tg_unthrottle_up(struct task_group *tg, void *data)
{
    struct cfs_rq *cfs_rq = tg->cfs_rq[cpu];
    
    if (!cfs_rq->runtime_enabled)
        return 0;
    
    // 恢复被限流的任务
    while (cfs_rq_throttled(cfs_rq)) {
        /* unthrottle */
        raw_spin_lock(&cfs_rq->__runtime_lock);
        cfs_rq->runtime_remaining += cfs_rq->runtime_assigned;
        raw_spin_unlock(&cfs_rq->__runtime_lock);
    }
    return 0;
}

6. 多核调度:负载均衡与 NUMA 亲和性

6.1 调度域(Sched Domain)层级

// 调度域层级(从高到低)
// DIE > MC > SMT (Intel)
// MC > SMT > DIE (某些 ARM)

struct sched_domain {
    struct sched_domain *parent;  // 上层域
    struct sched_domain *child;   // 下层域
    struct sched_group *groups;   // 负载均衡组
    unsigned long min_interval;   // 最小均衡间隔
    unsigned long max_interval;   // 最大均衡间隔
    unsigned int busy_factor;     // 繁忙系数
    unsigned int imbalance_pct;   // imbalance 百分比阈值
};

6.2 NUMA 感知调度

// NUMA 调度策略
// 1. 任务首次分配时选择内存所在 NUMA 节点
// 2. 访问统计通过 task_numa_faults 跟踪 page fault
// 3. numa_migrate 触发时调用 migrate_misplaced_page

if (node_load(src_nid) > node_load(dst_nid) * imbalance_ratio / 100) {
    // 触发任务迁移到负载更低的 NUMA 节点
}

7. eBPF 实战:观测 CFS 行为

7.1 使用 bpftrace 追踪调度事件

# 追踪每个任务的 vruntime 变化
bpftrace -e 'kprobe:update_curr {
     = (struct cfs_rq *)arg0;
     = (struct sched_entity *) ->curr;
    printf("comm=%s vruntime=%llu delta_exec=%llu\n",
            comm, ->vruntime, ->sum_exec_runtime - ->prev_sum_exec_runtime);
}'

# 追踪任务入队/出队
bpftrace -e '/tracepoint:sched:sched_switch/ {
    printf("prev_pid=%d next_pid=%d\n", args->prev_pid, args->next_pid);
}'

7.2 使用 BCC 工具观测调度延迟

# runqlat - 运行队列延迟分布
$ runqlat 1 10

# runqlen - 运行队列长度
$ runqlen 1 10

# cpudist - CPU 时间分布
$ cpudist -p  1 5

7.3 自定义 eBPF 探针监控 CFS 限流

// throttled_monitor.bpf.c
SEC("tracepoint/sched/sched_process_exec")
int trace_sched_throttled(struct trace_event_raw_sched_process_exec *ctx)
{
    u32 pid = bpf_get_current_pid_tgid() >> 32;
    
    // 检查该进程是否在 cgroup 中被限流
    struct task_struct *task = (struct task_struct *)bpf_get_current_task();
    // ... 读取 cfs_bandwidth 状态
    
    return 0;
}

8. CFS 调优实践

8.1 内核参数调优

# /etc/sysctl.conf

# 最小调度粒度(默认 0.75ms,适当增加减少上下文切换)
kernel.sched_min_granularity_ns = 1000000

# 调度周期(默认 6ms,增大有利于吞吐,减小有利于延迟)
kernel.sched_latency_ns = 12000000

# 唤醒预取(减少唤醒延迟)
kernel.sched_wakeup_granularity_ns = 15000000

# 迁移成本(影响负载均衡积极度)
kernel.sched_migration_cost_ns = 5000000

# NUMA 均衡周期
kernel.numa_balancing = 1

8.2 实时任务与 CFS 共存

Linux 实时调度策略(SCHED_FIFO/SCHED_RR)优先级始终高于 CFS。但过多的实时任务会饿死普通 CFS 任务。最佳实践:

# 限制实时任务总运行时间(每秒 950ms)
echo 950000 > /proc/sys/kernel/sched_rt_runtime_us
echo -1 > /proc/sys/kernel/sched_rt_period_us  # 取消限制(危险)

8.3 容器场景 CFS 配置

# Docker CPU 份额限制(默认 1024)
docker run --cpus=2.0 --cpu-shares=512 my_app

# Kubernetes CPU requests/limits
resources:
  requests:
    cpu: "500m"    # 50% CPU 份额
  limits:
    cpu: "1500m"   # 最多 1.5 核心(启用带宽控制)

9. CFS 常见陷阱与排查

9.1 CPU 高负载但无响应

# 诊断步骤
# 1. 查看调度器统计
cat /proc/sched_debug | head -50

# 2. 查看每个 CPU 的运行队列
cat /proc/schedstat

# 3. 检查是否因 CFS 带宽限流
cat /sys/fs/cgroup/cpu/cpu.stat
# 输出:nr_periods 1234 nr_throttled 100 throttled_time 5000000000

9.2 上下文切换频繁(CS 过高)

可能原因:

  • 任务粒度太细小,系统调用过于频繁
  • 调度周期 sched_latency_ns 过小,被 nr_running 补偿后仍导致频繁切换
  • 中断风暴(网络/IO)导致频繁抢占

10. 总结

CFS 代表了操作系统调度器设计的一次范式转变。其核心创新在于:将「公平」定义为虚拟运行时间的均等化,而非传统的时间片轮转。这种设计使 CFS 在桌面交互、服务器吞吐、容器编排等场景下均表现优异。

理解 CFS 的关键在于掌握以下几个概念之间的关系:

  • vruntime ↔ 权重:权重越大 vruntime 增长越慢,CPU 份额越高
  • 红黑树 ↔ 调度决策:O(log N) 总是选择最左节点
  • min_vruntime ↔ 公平保证:防止新任务饿死老任务
  • cfs_bandwidth ↔ 硬限制:确保 QoS 的硬性约束

随着异构计算(big.LITTLE/Intel Thread Director)和新硬件的出现,CFS 也在持续演进,但其核心设计理念——完全公平——仍然是现代调度器设计的基准。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部