Linux内核调度器是操作系统的核心组件,它决定了哪个进程获得CPU时间。从早期的O(n)调度器,到至今仍广泛使用的CFS(完全公平调度器),再到Linux 6.6引入的EEVDF调度器,Linux调度器经历了三次重大架构变革。本文将深入剖析这三种调度器的设计思想、算法实现与性能差异。

一、O(n)调度器:早期的简单尝试

在Linux 2.4时代,O(n)调度器是默认的进程调度器。它的核心思想非常直接:每个时间片轮转遍历所有可运行进程,为它们分配时间片。

该调度器的主要数据结构是一个可运行进程的链表。每次调度时,内核遍历整个链表,为每个进程计算counter(时间片)。虽然实现简单,但其时间复杂度为O(n),当进程数量增加时,调度开销线性增长,严重限制了服务器场景下的扩展能力。

// Linux 2.4 调度器核心逻辑(简化版)
for (p = task_queue; p; p = p->next) {
    p->counter = (p->counter / 2)   priority;
}

此外,O(n)调度器还存在饥饿问题——当进程数量多时,遍历延迟会导致交互式进程响应变慢。这些问题催生了O(1)调度器的诞生。

二、O(1)调度器:数组的胜利

Linux 2.6.0到2.6.22使用的O(1)调度器采用了优先级数组(priority array)结构。它将140个优先级分成两个数组:active数组和expired数组。每个优先级对应一个进程链表。

调度时,调度器只需从active数组中选择最高优先级的非空链表队首进程——这是一个常数时间操作。进程用完时间片后被移到expired数组,当active数组为空时,交换两个数组指针即可——同样O(1)。

struct prio_array {
    unsigned long bitmap[BITMAP_SIZE];  // 优先级位图
    struct list_head queue[MAX_PRIO];   // 各优先级进程链表
    int nr_active;                      // 活跃进程数
};

O(1)调度器虽然解决了扩展性问题,但其\"交互性评分\"算法过于复杂且难以预测——内核通过启发式规则(如睡眠时间)判断进程是否为交互式,经常出错。这促使社区寻找更优雅的方案。

三、CFS(完全公平调度器):红黑树的引入

Linux 2.6.23起,CFS取代了O(1)调度器,由Ingo Molnar设计。CFS的核心理念非常大胆:不直接分配时间片,而是记录每个进程的虚拟运行时间(vruntime),始终选择vruntime最小的进程运行。

3.1 核心数据结构

CFS使用红黑树(rbtree)作为调度队列。每个调度实体(sched_entity)作为红黑树的一个节点,以vruntime为键值排序。最左侧节点始终是vruntime最小(即最\"亏欠\"CPU时间)的进程。

// 内核中CFS调度实体
struct sched_entity {
    struct load_weight load;          // 权重
    struct run_node      run_node;    // 红黑树节点
    struct cfs_rq       *cfs_rq;      // 所属CFS队列
    u64                  vruntime;    // 虚拟运行时间
    u64                  exec_start;  // 本次执行开始时间
    u64                  sum_exec_runtime; // 总执行时间
};

3.2 vruntime计算

vruntime是CFS的\"时间货币\"。对于权重越高的进程(nice值越低),vruntime增长越慢——这意味着它们能更频繁地被调度。

// vruntime增量 = 实际运行时间 / 权重 * 调度粒度
vruntime  = delta_exec * (NICE_0_LOAD / se->load.weight);

当nice值为0时,vruntime等于实际运行时间。nice值每降低1(优先级提高),vruntime增长速度减慢约25%;nice值每升高1,vruntime增长速度加快约25%。这种指数级权重分配确保了高优先级进程获得更多CPU时间。

3.3 调度流程

CFS的调度流程如下:

1. 当进程被唤醒或被创建时,将其插入红黑树

2. 每次tick中断,检查当前进程的vruntime是否仍然是树中最小值

3. 如果不是,设置调度标志(need_resched),在适当时机切换进程

4. 当进程阻塞(等待I/O等)时,从红黑树中移除,重新唤醒时基于最小vruntime重新插入(防止饥饿)

四、EEVDF调度器:下一代调度算法

Linux 6.6引入了EEVDF(Earliest Eligible Virtual Deadline First)调度器,作为CFS的一个替代选项。EEVDF源于实时调度理论,旨在提供更精确的延迟控制和更公平的带宽分配。

4.1 核心概念

EEVDF引入了三个关键属性:

lag(延迟偏差):进程应该获得但尚未获得的CPU时间。lag=virtual_time - vruntime,正值表示进程\"欠账\",负值表示进程\"超前\"。

eligible time(合格时间):进程最早可以开始被调度的时间(防止lag为负的进程立即抢占,避免频繁上下文切换)。

virtual deadline(虚拟截止时间):基于请求周期和权重计算,决定进程下一次调度的优先级。

// EEVDF调度实体
struct sched_entity {
    u64 vruntime;         // 虚拟运行时间
    u64 vlag;             // 延迟偏差
    u64 eligible_time;    // 合格时间
    u64 vdeadline;        // 虚拟截止时间
    // ...其他字段
};

4.2 调度决策

EEVDF选择eligible_time ≤ 当前时间 且 vdeadline最小的进程运行。这比CFS单纯比较vruntime更具前瞻性:

- 当进程lag为负时使用eligible_time延后调度,减少不必要的抢占

- 通过vdeadline保证长期公平性:在请求周期窗口内,调度比例严格等于权重比

4.3 EEVDF vs CFS 性能对比

根据内核邮件列表的测试数据,在多核高负载场景下:

- 延迟一致性:EEVDF的P99延迟波动比CFS小约15-30%

- 吞吐量:相近场景下差异在±2%以内

- 上下文切换:EEVDF通过eligible_time机制减少了约10%的不必要切换

五、如何选择调度器

桌面/交互式环境:默认的CFS已经足够,无需调整

服务器/数据库:可尝试EEVDF(Linux 6.6 ),观察延迟改善

低延迟交易系统:考虑配合实时调度策略(SCHED_FIFO/SCHED_RR) CPU隔离

容器编排(K8s):使用cgroups v2的cpu.weight控制相对份额,底层调度器选择影响较小

六、调优实践

在/sys/fs/cgroup下可以调整CFS参数:

# 查看CFS调度周期(默认4ms)
cat /sys/fs/cgroup/cpu.max

# 设置CPU限制:50ms预算/100ms周期 = 0.5个核
echo "50000 100000" > /sys/fs/cgroup/mycgroup/cpu.max

# 调整调度粒度(内核参数)
sysctl kernel.sched_min_granularity_ns=1000000
sysctl kernel.sched_wakeup_granularity_ns=2000000

对于EEVDF,可通过sysctl kernel.sched_enable_eevdf切换内核版本。

七、总结

从O(n)到CFS再到EEVDF,Linux调度器的演进始终围绕一个核心目标:在保证公平性的前提下最大化系统吞吐和响应速度。红黑树 vruntime的CFS设计巧妙而优雅,在绝大多数场景下表现优异。EEVDF则通过引入eligible_time和vdeadline,在理论上提供了更严格的延迟保证。

对于系统运维和性能工程师而言,理解这些调度器的内部机制,有助于在性能瓶颈分析时做出更精准的判断。下次遇到\"CPU打满但响应慢\"的问题时,不妨检查一下CPU负载、上下文切换率和vruntime分布——答案往往就藏在这些指标中。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部