Skip to content

第四十八章:调度类

结丹后期

涉及内核源码:

林小源在调度竞技场中,发现了一座巨大的阶梯。

阶梯有五层,每一层都站着一位守关人。最顶层是一个模糊的身影,几乎看不清面目;第二层是 Deadline,金色长袍在风中飘动;第三层是 RT,银色铠甲闪闪发光;第四层是 CFS,绿色长衫温文尔雅;最底层是一个懒洋洋的少年,正靠在柱子上打瞌睡——那是 idle。

"这是调度类的层次。"CFS 从第四层走下来,对林小源说,"每个调度类都实现了相同的接口——enqueue_task、dequeue_task、pick_next_task、task_tick——但有不同的调度策略。"

林小源看着阶梯,问道:"调度器怎么知道该用哪个调度类?"

CFS 指了指最顶层:"pick_next_task() 按优先级从高到低检查每个调度类。先检查 stop,再检查 dl,再检查 rt,再检查我,最后检查 idle。第一个有可运行进程的调度类,它的选择就是最终结果。"

"那 stop 调度类是什么?"林小源望着最顶层那个模糊的身影。

"stop 是最高优先级的调度类,"CFS 的表情变得严肃,"它只用于 CPU hotplug 等特殊情况。stop 进程可以抢占任何其他进程——包括 Deadline 和 RT。它几乎不会出现,但一旦出现,就是绝对的权威。"

林小源跟着 CFS 走到阶梯旁边的一面石壁前。石壁上刻着调度类的接口函数,每一个函数都有详细的注释。

"这种设计叫多态,"CFS 说,"每种调度类都实现了相同的接口,但行为不同。调度器的核心代码不需要知道具体的调度策略——它只需要调用接口函数,调度类自己知道该怎么做。"

林小源伸手触摸石壁上的 函数。指尖传来一阵温热,他仿佛看到了函数内部的执行过程:一个循环,从 stop 开始,依次调用每个调度类的 pick_next_task 方法,直到找到一个有可运行进程的调度类。

"这种设计的好处是什么?"林小源问。

"扩展性,"CFS 说,"如果有人想添加新的调度策略——比如专门为机器学习任务设计的调度器——只需要实现一个新的调度类,注册到调度器中,不需要修改核心代码。调度器的核心代码从 Linux 2.6 以来几乎没变过,但调度类不断演进。"

林小源想起了 CFS 取代 O(1) 调度器的历史。O(1) 调度器的问题在于它的启发式判断不准确,而 CFS 用 vruntime 精确计算公平性。但调度器的核心框架——调度类的多态设计——一直沿用至今。

林小源坐在阶梯的最低层,和 idle 少年并肩而坐。

"你看起来很闲,"林小源说。

idle 少年睁开一只眼睛:"我就是闲。只有当所有其他调度类都没有可运行进程时,才会选到我。但你别小看我——没有我,CPU 会空转浪费电。"

林小源笑了笑,又转向 CFS:"pick_next_task() 的效率怎么样?它需要遍历所有进程吗?"

CFS 摇头:"不需要。pick_next_task() 只检查每个调度类的第一个可运行进程。由于调度类只有五个,这个操作是 O(1) 的。层次设计提高了效率——不需要遍历成千上万的进程,只需要检查五个调度类。"

林小源看着五层阶梯,心中感慨。调度类的设计就像一个金字塔——越往上越特殊,越往下越普通。stop 几乎不出现,Deadline 和 RT 服务于特殊场景,CFS 管理大多数进程,idle 守护着最后的底线。

"每种调度类都有自己的位置,"CFS 说,"没有高低之分,只有职责不同。"


c
/*
 * Linux 调度类的层次(从高到低):
 *
 * stop_sched_class     — 最高优先级
 * dl_sched_class       — Deadline 调度器
 * rt_sched_class       — 实时调度器
 * fair_sched_class     — CFS 调度器
 * idle_sched_class     — 最低优先级
 *
 * 每个调度类实现相同的接口:
 *   enqueue_task()     — 把进程加入运行队列
 *   dequeue_task()     — 把进程从运行队列移除
 *   pick_next_task()   — 选择下一个运行的进程
 *   task_tick()        — 时钟中断处理
 *   task_fork()        — fork 时的处理
 *
 * 调度器的 pick_next_task():
 *   按优先级检查每个调度类
 *   返回第一个有可运行进程的调度类选择的进程
 */

struct sched_class {
    const char *name;
    int priority;
    int (*pick_next)(void);
};

int pick_next_stop(void) { return -1; }   /* 通常没有进程 */
int pick_next_dl(void) { return 300; }    /* Deadline 进程 */
int pick_next_rt(void) { return 200; }    /* RT 进程 */
int pick_next_fair(void) { return 100; }  /* CFS 进程 */
int pick_next_idle(void) { return 0; }    /* idle 进程 */

printf("=== 调度类 — 多态设计 ===\n\n");

struct sched_class classes[] = {
    { "stop",  0, pick_next_stop },
    { "dl",    1, pick_next_dl },
    { "rt",    2, pick_next_rt },
    { "fair",  3, pick_next_fair },
    { "idle",  4, pick_next_idle },
};
int nr = sizeof(classes) / sizeof(classes[0]);

printf("调度类层次:\n");
for (int i = 0; i < nr; i++) {
    printf("  %s (优先级 %d)\n", classes[i].name, classes[i].priority);
}

printf("\n--- pick_next_task() 的逻辑 ---\n");
printf("按优先级检查每个调度类:\n\n");

for (int i = 0; i < nr; i++) {
    int pid = classes[i].pick_next();
    printf("检查 %s: ", classes[i].name);
    if (pid > 0) {
        printf("选择 PID %d\n", pid);
        printf("→ 调度完成,返回 PID %d\n", pid);
        break;
    } else {
        printf("没有可运行进程,继续检查\n");
    }
}

printf("\n--- 调度类的设计哲学 ---\n");
printf("stop:   不可抢占,用于 CPU hotplug\n");
printf("dl:     时间保证,硬实时任务\n");
printf("rt:     确定性,软实时任务\n");
printf("fair:   公平,普通进程\n");
printf("idle:   空闲,没有其他进程时运行\n");

道藏笔记

内核启示

调度类是 Linux 调度器的多态设计。

调度类的层次(从高到低):

  1. — 最高优先级,用于 CPU hotplug
  2. — Deadline 调度器
  3. — 实时调度器
  4. — CFS 调度器
  5. — 最低优先级

每个调度类实现相同的接口:

  • — 把进程加入运行队列
  • — 把进程从运行队列移除
  • — 选择下一个运行的进程
  • — 时钟中断处理

的逻辑:

  1. 按优先级检查每个调度类
  2. 返回第一个有可运行进程的调度类选择的进程
  3. 时间复杂度 O(1)

调度类让调度器可以"扩展"——添加新策略不需要修改核心代码。


破关试炼

调度类之试

多个调度类按优先顺序接力挑选任务时,最后落到哪个通用挑选入口?

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

以修仙之名,悟内核之道