2、Linux内核调度器演进:从O(n)到CFS,RT调度类的诞生

调度器,说白了就是内核的“交通指挥官”。它决定哪个进程上CPU,哪个继续等。我入行那会儿,Linux调度器还处在蛮荒时代。这些年一路走过来,看着它从O(n)进化到O(1),再到今天的CFS,感触挺深的。

今天咱们就聊聊这段演进史。你想想看,一个操作系统,如果调度做不好,实时性就是空谈。

2.1 远古时代:O(n)调度器

Linux 2.4内核用的就是O(n)调度器。为什么叫O(n)?因为每次调度,它都要遍历整个进程链表。进程越多,耗时越长。

核心设计:一个全局的runqueue,所有可运行进程都挂在这个链表上。每次选下一个进程,就从头到尾扫一遍,算算谁的“好度”最高。

好度计算

goodness = 进程优先级 + 剩余时间片 / 2

嗯,这里要注意。这个算法有个致命问题——当进程数达到几百个时,调度延迟会飙升到几十毫秒。我在项目中遇到过,一个嵌入式设备上跑了200多个线程,系统响应直接崩了。查了半天,发现是调度器在“拖后腿”。

O(n)调度器的痛点

  • 时间复杂度O(n),进程越多越慢
  • 多核CPU下,全局锁竞争激烈
  • 交互式进程响应差,用户体验糟糕
  • 没有实时调度支持

2.2 里程碑:O(1)调度器

Linux 2.6内核引入了O(1)调度器。这是调度器的一次革命。为什么叫O(1)?因为调度决策的时间是常数,跟进程数量无关。

核心思想:用两个优先级数组——active数组和expired数组。每个优先级对应一个链表。调度时直接从active数组中找最高优先级的非空链表,取出第一个进程就行。

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

我个人习惯把O(1)调度器比作“银行柜台”。每个优先级是一个窗口,VIP客户(高优先级)优先办理。普通客户(低优先级)等前面办完再说。

时间片分配

优先级范围 时间片(毫秒) 适用场景
0-99(实时) 固定时间片 实时任务
100-139(普通) 100ms - 800ms 交互/批处理

O(1)调度器解决了O(n)的性能问题,但它有个硬伤——对交互式进程的识别不够智能。我曾经调试过一个桌面系统,鼠标总是卡顿。后来发现是调度器把X server当成了批处理任务,给了太少的时间片。

避坑指南:O(1)调度器虽然快,但公平性不足。高优先级进程可能长时间霸占CPU,导致低优先级进程“饿死”。我曾经在服务器上遇到过,一个高优先级的日志进程吃掉了90%的CPU,业务进程全在等。

2.3 现代基石:CFS调度器

Linux 2.6.23引入了CFS(Completely Fair Scheduler)。这是调度器设计的又一次飞跃。CFS的理念很简单——让每个进程都能公平地获得CPU时间。

核心设计:用红黑树代替优先级数组。每个进程有一个“虚拟运行时间”(vruntime)。调度器总是选择vruntime最小的进程运行。

// CFS的核心数据结构
struct cfs_rq {
    struct rb_root tasks_timeline;  // 红黑树根节点
    struct rb_node *rb_leftmost;    // 最左节点(vruntime最小)
    u64 min_vruntime;               // 最小虚拟运行时间
    unsigned int nr_running;        // 运行队列中的进程数
};

为什么用红黑树?因为插入和删除都是O(log n),比O(n)快得多。而且红黑树天然有序,找最小vruntime的节点就是最左边的节点,时间复杂度O(1)。

vruntime计算

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

说白了,优先级高的进程,vruntime增长得慢,所以更容易被选中。优先级低的进程,vruntime增长得快,运行一会儿就得让位。

我在项目中遇到过一个问题:一个计算密集型的进程,总是被交互式进程抢占。后来发现是CFS的“睡眠公平”机制在起作用。交互式进程经常睡眠,醒来后vruntime会被补偿,所以更容易获得CPU。

个人经验:调优CFS时,重点关注这几个参数:

  • sched_latency_ns:调度延迟,默认6ms
  • sched_min_granularity_ns:最小时间片,默认0.75ms
  • sched_nr_migrate:负载均衡时迁移的进程数

我一般把sched_latency_ns调小到3ms,交互式应用的响应会明显提升。

2.4 实时调度类(RT)的引入

实时调度,说白了就是“说到做到”。普通调度器追求公平,实时调度器追求确定性。Linux的实时调度类,就是为这个目标设计的。

实时调度策略

  • SCHED_FIFO:先进先出,直到主动让出CPU或被更高优先级抢占
  • SCHED_RR:时间片轮转,同优先级进程轮流运行
  • SCHED_DEADLINE:基于截止时间的调度,最严格的实时保证

优先级范围:实时进程的优先级是0-99,数值越大优先级越高。普通进程的优先级是100-139。所以实时进程永远优先于普通进程。

// 设置实时调度策略和优先级
struct sched_param param;
param.sched_priority = 80;  // 实时优先级80
sched_setscheduler(pid, SCHED_FIFO, ¶m);

嗯,这里要注意。实时调度虽然好,但用不好会出大问题。我曾经在项目中遇到过一个坑:一个SCHED_FIFO进程死循环了,结果整个系统卡死,连ssh都连不上。因为实时进程优先级太高,普通进程根本抢不到CPU。

避坑指南:使用实时调度时,一定要设置CPU亲和性,把实时进程绑定到特定CPU上。同时,给实时进程设置一个“看门狗”,防止它跑飞。我曾经因为没做这些,导致产线停机了2小时。

2.5 知识体系总览

下面这张图,是我自己画的调度器演进路线。你可以看到,从O(n)到CFS,每一步都是对前一代的扬弃。

Linux内核调度器演进路线 O(n) Linux 2.4 全局链表遍历 时间复杂度O(n) O(1) Linux 2.6.0 优先级数组 时间复杂度O(1) CFS Linux 2.6.23 红黑树 完全公平 RT Linux 2.6.x SCHED_FIFO/RR 实时确定性 调度器特性对比 特性 O(n) O(1) CFS RT调度类 时间复杂度 O(n) O(1) O(log n) O(1) 公平性 中等 优先级驱动 实时支持 有限 完整 多核扩展 中等 适用场景 嵌入式 桌面 通用 实时系统

从这张图可以看得很清楚:O(n)调度器是“蛮力型”,O(1)调度器是“效率型”,CFS是“公平型”,RT调度类是“确定型”。每个调度器都有自己的定位和适用场景。

我的建议:做实时系统调优时,不要一上来就用RT调度类。先试试CFS,把sched_latency_ns调小,把进程的nice值调高。如果还不行,再考虑SCHED_FIFO或SCHED_RR。记住,实时调度是把双刃剑,用好了是利器,用不好是灾难。

好了,调度器的演进就聊到这儿。下一节咱们深入看看CFS的源码实现,到时候我会带着你一行一行地读代码。


专注资料整理