Skip to content

第五十三章:调度器演进

结丹圆满

涉及内核源码:

林小源在调度竞技场的深处,发现了一座博物馆。

博物馆的墙壁上挂满了画像——每一张画像都是一位调度器的肖像。最左边是一幅粗糙的素描,画着一个简单的循环;中间是一幅精美的油画,画着位图和优先级数组;最右边是一幅工笔画,画着一棵红黑树。

"这是调度器的历史。"CFS 从红黑树画像旁走来,"Linux 调度器经历了三代演进。"

林小源走到最左边的素描前。画中是一个简单的调度器,正在遍历一个长长的进程列表。

"第一代,O(n) 调度器,"CFS 说,"Linux 2.4 时代的调度器。每次调度都要遍历所有进程,找到最佳进程。时间复杂度 O(n),n 是进程数。简单,但当进程数成千上万时,效率低下。"

林小源走到中间的油画前。画中是一个精巧的数据结构——位图标记哪些优先级有进程,优先级数组存储各优先级的进程链表。

"第二代,O(1) 调度器,"CFS 说,"Linux 2.6 时代的调度器。使用位图和优先级数组,时间复杂度 O(1)。高效,但对交互式进程不友好——它用启发式判断哪些进程是'交互式'的,但这个判断经常出错。"

"谁写的?"林小源问。

CFS 微微一笑:"Ingo Molnar。"

林小源走到最右边的工笔画前。画中是一棵红黑树,每个节点都是一个进程,按 vruntime 排序。

"第三代,CFS 调度器,"CFS 说,"Linux 2.6.23 以来的调度器。使用红黑树,按 vruntime 排序。不需要启发式,不需要区分'交互式'和'批处理'——所有进程一视同仁。时间复杂度 O(log n)。"

"谁写的?"林小源又问。

CFS 笑得更深了:"也是 Ingo Molnar。他亲手推翻了自己的 O(1) 调度器——因为 CFS 的设计哲学更好。"

林小源站在红黑树画像前,仔细观察每一个节点。

"为什么 CFS 要取代 O(1)?"他问。

CFS 走到他身边:"O(1) 调度器的问题在于启发式。它用'睡眠时间'来判断一个进程是不是交互式的——睡眠时间长的进程被认为是交互式的,给予更高的优先级。但这个判断经常出错:一个批处理进程可能因为等待 I/O 而长时间睡眠,被错误地当作交互式进程。"

"而 CFS 不需要这种判断?"

"不需要,"CFS 说,"CFS 的核心思想是'公平'——每个进程的 vruntime 应该趋于相等。vruntime 是进程的虚拟运行时间,按权重分配。权重高的进程 vruntime 增长慢,获得更多的 CPU 时间;权重低的进程 vruntime 增长快,获得更少的 CPU 时间。调度器总是选择 vruntime 最小的进程运行——这是精确的数学计算,不是启发式猜测。"

林小源看着红黑树的结构,问道:"per-CPU 运行队列呢?"

"O(1) 调度器使用全局运行队列,多核竞争一把锁,扩展性差。CFS 使用 per-CPU 运行队列,每个 CPU 有自己的红黑树,互不干扰,扩展性好。"CFS 说,"简单、公平、高效、可扩展——这就是 CFS 取代 O(1) 的原因。"

林小源坐在博物馆的长椅上,看着三代调度器的画像。

"好的设计不是一蹴而就的,"他说,"它需要经历多次迭代。"

CFS 在他身边坐下:"没错。O(n) 调度器是'能用'——它能工作,但效率低。O(1) 调度器是'好用'——它高效,但启发式不可靠。CFS 调度器是'优雅'——它用最简单的思想解决了最复杂的问题。"

"但 CFS 也有权衡,"林小源说,"O(log n) 比 O(1) 慢。"

"是的,"CFS 说,"但 O(log n) 的'慢'几乎可以忽略——红黑树的深度通常只有十几层。而 CFS 的'公平'带来的好处远远超过这点微小的性能损失。"

林小源看着博物馆的墙壁,看到了未来的空间——也许会有第四代调度器,也许 CFS 也会被推翻。但设计哲学不会变:简单、公平、高效、可扩展。

"进化是永无止境的,"林小源说。

CFS 点头:"每一次迭代都解决前一代的问题,但也引入新的权衡。这就是内核的演化——不是一步到位,而是不断试错、不断改进。"


c
/*
 * 第一代:O(n) 调度器 (Linux 2.4)
 *   遍历所有进程,找到最佳进程
 *   时间复杂度 O(n),n 是进程数
 *   简单但效率低
 *
 * 第二代:O(1) 调度器 (Linux 2.6)
 *   使用位图和优先级数组
 *   位图标记哪些优先级有进程
 *   优先级数组存储各优先级的进程链表
 *   时间复杂度 O(1)
 *   但对交互式进程不友好
 *
 * 第三代:CFS 调度器 (Linux 2.6.23+)
 *   使用红黑树,按 vruntime 排序
 *   vruntime 最小的进程获得 CPU
 *   公平、高效、对交互式友好
 *   时间复杂度 O(log n)
 */

struct o_n_scheduler {
    int nr_running;
    int best_idx;
};

struct o_1_scheduler {
    unsigned long bitmap;   /* 位图 */
    int prio_array[32];     /* 优先级数组 */
    int nr_running;
};

struct cfs_scheduler {
    int nr_running;
    unsigned long min_vruntime;
};

printf("=== 调度器的演进 ===\n\n");

printf("--- 第一代:O(n) 调度器 ---\n");
printf("时间复杂度:O(n)\n");
printf("优点:简单\n");
printf("缺点:进程数多时效率低\n");
printf("时代:Linux 2.4\n\n");

printf("--- 第二代:O(1) 调度器 ---\n");
printf("时间复杂度:O(1)\n");
printf("优点:高效\n");
printf("缺点:对交互式进程不友好\n");
printf("时代:Linux 2.6\n\n");

printf("--- 第三代:CFS 调度器 ---\n");
printf("时间复杂度:O(log n)\n");
printf("优点:公平、对交互式友好\n");
printf("缺点:比 O(1) 稍慢\n");
printf("时代:Linux 2.6.23+\n\n");

printf("--- CFS 为什么取代 O(1) ---\n");
printf("O(1) 调度器的问题:\n");
printf("  - 交互式进程的启发式判断不准确\n");
printf("  - 某些进程被不公平地对待\n");
printf("  - 全局变量导致扩展性差\n\n");
printf("CFS 的解决方案:\n");
printf("  - 不使用启发式,使用 vruntime 精确计算\n");
printf("  - 所有进程按 vruntime 排序,天然公平\n");
printf("  - per-CPU 运行队列,扩展性好\n");

printf("\n--- 调度器的设计哲学 ---\n");
printf("O(n):   简单优先\n");
printf("O(1):   效率优先\n");
printf("CFS:    公平优先\n");

道藏笔记

内核启示

Linux 调度器经历了三代演进。

第一代:O(n) 调度器(Linux 2.4)

  • 遍历所有进程,找到最佳进程
  • 时间复杂度 O(n)
  • 简单但效率低

第二代:O(1) 调度器(Linux 2.6)

  • 使用位图和优先级数组
  • 时间复杂度 O(1)
  • 但对交互式进程不友好

第三代:CFS 调度器(Linux 2.6.23+)

  • 使用红黑树,按 vruntime 排序
  • 时间复杂度 O(log n)
  • 公平、高效、对交互式友好

CFS 取代 O(1) 的原因:

  • O(1) 的启发式判断不准确
  • CFS 不需要启发式,使用 vruntime 精确计算
  • CFS 的 per-CPU 设计扩展性更好

好的设计需要迭代——每一次迭代都解决前一代的问题。


破关试炼

调度器演进之试

本章回顾调度器演进时,取代 O(1) 思路并以公平为核心的调度器缩写是什么?

答对后才能继续滑动和进入下一章。

以修仙之名,悟内核之道