引言

Linux 内核的进程调度器是操作系统的核心组件之一,它决定了哪个进程在何时获得 CPU 时间。自 2.6.23 版本以来,完全公平调度器(Completely Fair Scheduler, CFS) 取代了传统的 O(1) 调度器,成为 Linux 默认的进程调度器。CFS 的设计理念——"尽可能公平地分配 CPU 时间给所有可运行进程"——看似简单,但其实现却精妙而深刻。

本文将从 CFS 的核心概念出发,深入剖析其底层数据结构、虚拟运行时间计算机制、组调度支持、实时调度策略,以及生产环境中的调优实践。

1. CFS 核心设计理念

CFS 的核心思想是维护一个虚拟运行时间(Virtual Runtime, vruntime)。每个进程都有一个 vruntime 值,表示该进程在虚拟时钟下已经运行的时间。调度器总是选择 vruntime 最小的进程执行,从而在宏观上保证所有进程获得"公平"的 CPU 份额。

关键原则:

  • 理想多处理器模型:假设有 N 个相同的进程,每个应获得 1/N 的 CPU 时间
  • 红黑树组织:所有可运行进程按 vruntime 排列在红黑树中,最左侧节点即为下一个调度的目标
  • 细粒度补偿:被唤醒的进程会根据当前最小 vruntime 进行调整,避免长时间睡眠后"霸占"CPU

2. 核心数据结构

2.1 sched_entity(调度实体)

每个进程的调度信息存储在 sched_entity 结构中:

struct sched_entity {
    struct load_weight  load;       // 负载权重(与 nice 值相关)
    struct rb_node      run_node;   // 红黑树节点
    u64                 vruntime;   // 虚拟运行时间 ⭐ 核心字段
    u64                 exec_start; // 本次开始执行的时间
    u64                 sum_exec_runtime; // 总实际运行时间
    u64                 prev_sum_exec_runtime; // 上一次切换时的总运行时间
    // ...
};

load 字段的权重由 nice 值映射表决定。nice 值每降低 1 级(优先级提高),权重增加约 25%;每升高 1 级,权重减少约 10%。这意味着 nice -20 的进程获得的 CPU 时间约为 nice 19 的进程的 ~3300 倍。

2.2 cfs_rq(CFS 运行队列)

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

struct cfs_rq {
    struct load_weight load;        // 队列总权重
    unsigned long runnable_weight;  // 可运行总权重
    unsigned int nr_running;        // 可运行进程数
    
    u64 min_vruntime;              // 队列最小 vruntime(单调递增)
    struct rb_root_cached tasks_timeline; // 红黑树根节点 ⭐
    
    // 负载跟踪(PELT 机制)
    struct sched_avg avg;
    // ...
};

min_vruntime 是整个队列的基准线。新创建进程的 vruntime 会被设置为当前 min_vruntime,而不是 0,从而防止新进程"霸占"CPU。

2.3 进程描述符中的调度相关字段

struct task_struct {
    const struct sched_class *sched_class;  // 调度类
    struct sched_entity se;                  // 普通调度实体
    struct sched_rt_entity rt;               // 实时调度实体
    int policy;                              // 调度策略
    int static_prio;                         // 静态优先级 (100-139)
    int normal_prio;                         // 动态优先级
    int rt_prio;                             // 实时优先级 (0-99)
    // ...
};

3. 虚拟运行时间(vruntime)的精确计算

CFS 的公平性依赖于 vruntime 的精确计算:

delta_vruntime = delta_exec × (NICE_0_LOAD / curr->load)

其中:

  • delta_exec:进程实际运行的时钟时间
  • NICE_0_LOAD:nice 0 进程的权重基准(1024)
  • curr->load:当前进程的权重

关键洞察:高权重进程(低 nice 值)的 vruntime 增长更慢,因此获得更多 CPU 时间;低权重进程(高 nice 值)的 vruntime 增长更快,被调度频率更低。

例如,两个进程 A(nice 0, weight=1024)和 B(nice 5, weight 335):

  • A 运行 10ms → vruntime 增加 10ms × (1024/1024) = 10ms
  • B 运行 10ms → vruntime 增加 10ms × (1024/335) ≈ 30.6ms
  • 结果:A 与 B 的 CPU 时间比 ≈ 3:1

4. 组调度(CGroup 支持)

CFS 支持层级调度:用户组(task group)内部再运行 CFS,组间再进行公平分配。这在容器和 cgroup 场景中至关重要。

// 层级调度实体
struct task_group {
    struct sched_entity **se;    // 每个 CPU 一个调度实体
    struct cfs_rq **cfs_rq;      // 每个 CPU 一个运行队列
    unsigned long shares;         // 组权重(cpu.shares)
    // Linux CGroup v2: cpu.max (quota/period)
};

容器场景示例:两个容器分别设置 cpu.shares 为 1024 和 512,当它们同时争抢 CPU 时,获得的 CPU 时间比为 2:1。若使用 CGroup v2 的 cpu.max 设置 100000 100000,则该容器最多使用 1 个 CPU 核心。

5. 实时调度策略

除了 CFS 处理的 SCHED_NORMAL(普通进程),Linux 还支持两种实时策略:

策略优先级说明
SCHED_FIFO1-99先进先出,高优先级可抢占低优先级,不进行时间片轮转
SCHED_RR1-99轮转策略,同优先级进程按时间片轮转执行

实时进程 vs 普通进程:实时进程(优先级 0-99)永远优先于普通进程(优先级 100-139)。即使普通进程的 vruntime 很小,只要有可运行的实时进程,就会立即抢占。

SCHED_DEADLINE(自 3.14 内核新增)则基于 EDF(最早截止时间优先)算法,适用于严格的实时任务。进程声明 (runtime, period, deadline),内核保证每个周期内至少获得 runtime 的 CPU 时间。

6. PELT:负载跟踪机制

CFS 使用 Per-Entity Load Tracking (PELT) 来跟踪进程和运行队列的负载。该机制通过衰减累加器追踪 CPU 利用率:

load_avg = load_avg × decay + active × (1 - decay)

衰减因子确保历史数据的影响随时间指数衰减,窗口约为 32ms。PELT 的输出用于:

  • 负载均衡:将任务从繁忙 CPU 迁移到空闲 CPU
  • CPU 频率调节:cpufreq governor 感知负载变化
  • 容量感知调度(CAS):大小核架构中选择合适的核心

2023 年 Linux 6.6 引入了 CQW(Concurrent Queue Workload) 补丁,改进了多线程工作负载的 Pelt 响应延迟问题。

7. 生产环境调优实践

7.1 关键 sysctl 参数

# 调度粒度(默认 1ms,越低响应越快但上下文切换开销越大)
kernel.sched_min_granularity_ns = 1000000

# 唤醒抢占粒度(默认 2ms,降低可被唤醒进程抢占的阈值)
kernel.sched_wakeup_granularity_ns = 2000000

# 迁移成本(影响负载均衡决策,默认 500000ns)
kernel.sched_migration_cost_ns = 500000

# NUMA 负载均衡间隔
kernel.numa_balancing = 1  # 启用 NUMA 自动均衡

7.2 进程优先级控制

# 设置进程 nice 值(-20 最高,19 最低)
nice -n -10 ./cpu_intensive_task
renice -n -5 -p 12345

# 设置实时优先级
chrt -f -p 50 12345  # SCHED_FIFO 优先级50
chrt -r -p 30 12345  # SCHED_RR 优先级30

# 使用 taskset 绑定 CPU
taskset -c 0-3 ./pinned_task

# 使用 cgroup v2 限制容器资源
mkdir /sys/fs/cgroup/mycontainer
echo "50000 100000" > /sys/fs/cgroup/mycontainer/cpu.max
echo 12345 > /sys/fs/cgroup/mycontainer/cgroup.procs

7.3 延迟敏感型应用优化

数据库、消息队列等低延迟应用常见配置:

  • 设置 SCHED_RR 或 SCHED_FIFO 实时优先级
  • 通过 isolcpus 隔离 CPU 核心供独占使用
  • 将中断绑定到非隔离核心(/proc/irq/*/smp_affinity)
  • 使用 NO_FULL_NOHZ 减少 tick 中断对计算核心的影响
  • 启用 SCHED_BATCH 降低批处理任务的调度频率

7.4 容器密度优化

高密部署场景下:

  • CFS 带宽控制(cpu.cfs_budelta/kuota)限制 CPU 突发
  • 设置 sched_latency_ns 与 sched_min_granularity_ns 的比例为 8:1,在公平性和切换开销间平衡
  • 使用 cpu.weight(CGroup v2, 1-10000)替代 shares,控制更直观

8. 调度器的未来方向

Linux 内核社区持续改进调度器:

  • Core Scheduling:解决 L1TF/Meltdown 漏洞的副作用,限制同一组进程共享物理核心,同时减少跨核侧信道风险
  • 调度器扩展(sched_ext):Linux 6.12 引入的可扩展调度框架,允许用户空间实现自定义调度策略,无需修改内核源码
  • 异构架构优化:Intel Thread Director 和 ARM big.LITTLE 的协同调度改进
  • NUMA 感知增强:自动页面迁移与进程放置的联合优化

总结

CFS 通过虚拟运行时间和红黑树数据结构,在简洁的设计中实现了高效的公平调度。理解 vruntime 计算机制、PELT 负载跟踪、组调度层级和实时策略优先级,是进行系统级性能调优的基础。随着 sched_ext 等用户可编程调度框架的成熟,Linux 调度器的灵活性和可扩展性将进一步提升,为云原生、实时计算和异构硬件场景提供更精细的控制能力。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部