3. Linux 2.4 调度器回顾:O(n)调度器的设计、运行队列遍历、时间复杂度分析

3.1 设计背景与核心思想

Linux 2.4 内核(2001年发布)采用的 O(n)调度器,是 Linux 调度史上第一个全局统一调度器。在此之前,调度逻辑分散在各架构代码中。O(n)调度器的设计目标是在多任务环境下提供公平的 CPU 时间分配,但其核心数据结构——全局运行队列——决定了它的性能瓶颈。

核心设计理念: 所有可运行进程(TASK_RUNNING 状态)被放入一个全局的 runqueue 链表中。每次调度发生时,调度器必须遍历整个链表,计算每个进程的“好度”(goodness),选出最佳进程执行。

3.2 全局运行队列结构

O(n)调度器的运行队列是一个双向循环链表,所有 CPU 共享同一个队列(SMP 系统通过大内核锁保护)。每个进程的 task_struct 通过 run_list 字段链接到队列中。

/* 内核源码:include/linux/sched.h (2.4.x) */
struct task_struct {
    volatile long state;        /* -1 unrunnable, 0 runnable, >0 stopped */
    struct list_head run_list;  /* 运行队列链表节点 */
    unsigned long policy;       /* 调度策略:SCHED_OTHER, SCHED_FIFO, SCHED_RR */
    int priority;               /* 静态优先级 (0-99 实时, 100-139 普通) */
    int counter;                /* 动态时间片计数器 */
    unsigned long rt_priority;  /* 实时优先级 */
    /* ... 其他字段 ... */
};

/* 全局运行队列定义 */
struct list_head runqueue_head;  /* 所有可运行进程的链表头 */
关键数据结构特点: 全局链表 + 大内核锁保护。在单 CPU 系统上,锁竞争不明显;但在 SMP 系统上,所有 CPU 的调度操作都要竞争同一把锁,导致严重的可扩展性问题。

3.3 调度算法核心:goodness 函数

调度器通过 goodness() 函数为每个进程打分,分数最高的进程获得 CPU。这个函数是 O(n)调度器的灵魂,它综合了进程类型、优先级和时间片剩余量。

/* 内核源码:kernel/sched.c (2.4.x) - 简化版 */
static inline int goodness(struct task_struct *p, int this_cpu, struct mm_struct *this_mm)
{
    int weight;

    /* 1. 实时进程直接返回高权重 */
    if (p->policy != SCHED_OTHER) {
        weight = 1000 + p->rt_priority;
        goto out;
    }

    /* 2. 普通进程:基础权重 = counter + priority */
    weight = p->counter;
    if (!weight)
        goto out;

    /* 3. 奖励:如果进程上次运行在同一 CPU 且共享内存描述符 */
    if (p->mm == this_mm || !p->mm)
        weight += 1;
    weight += 20 - p->priority;  /* priority 越小优先级越高 */

out:
    return weight;
}
进程类型 goodness 计算方式 典型分数范围
SCHED_FIFO / SCHED_RR(实时) 1000 + rt_priority 1000 ~ 1099
SCHED_OTHER(普通)且 counter > 0 counter + (20 - priority) + 缓存奖励 1 ~ 139
SCHED_OTHER 且 counter = 0 0(跳过,不参与调度) 0

3.4 调度主流程:schedule() 函数

每次调用 schedule() 时,调度器执行以下步骤。注意第 2 步的 全链表遍历 是 O(n) 复杂度的根源。

  1. 关闭内核抢占,获取大内核锁。
  2. 遍历 runqueue_head 链表,对每个进程调用 goodness(),记录最高分进程。
  3. 如果所有进程的 counter 都为 0(时间片耗尽),则执行 重新计算时间片:遍历所有进程(包括睡眠进程),按 counter = counter/2 + priority 重新分配。
  4. 选择 goodness 最高的进程,执行上下文切换。
/* 内核源码:kernel/sched.c (2.4.x) - schedule() 核心逻辑 */
asmlinkage void schedule(void)
{
    struct task_struct *prev, *next, *p;
    int this_cpu, c;

    prev = current;
    this_cpu = smp_processor_id();

    /* 步骤 2:遍历运行队列,寻找最佳进程 */
    next = &init_task;  /* idle 进程作为默认 */
    c = -1000;
    list_for_each(tmp, &runqueue_head) {
        p = list_entry(tmp, struct task_struct, run_list);
        if (p->state == TASK_RUNNING) {
            int weight = goodness(p, this_cpu, prev->active_mm);
            if (weight > c) {
                c = weight;
                next = p;
            }
        }
    }

    /* 步骤 3:如果所有 counter 为 0,重新计算 */
    if (!c) {
        recalc_task_timeslice(prev, this_cpu);
        /* 重新遍历一次 ... */
    }

    /* 步骤 4:上下文切换 */
    if (prev != next)
        switch_to(prev, next);
}
性能陷阱: 当系统中有 N 个可运行进程时,每次调度都需要遍历整个链表。更糟糕的是,当所有进程时间片耗尽时,recalc_task_timeslice() 会遍历系统中 所有进程(包括睡眠进程),复杂度为 O(所有进程数),而不仅仅是 O(可运行进程数)。

3.5 时间复杂度分析

O(n)调度器的时间复杂度可以从两个维度分析:

操作场景 时间复杂度 说明
普通调度(时间片未耗尽) O(Nrunnable) 遍历可运行进程链表,Nrunnable 为可运行进程数
时间片重新计算 O(Ntotal) 遍历系统中所有进程(包括睡眠、僵尸),Ntotal 为总进程数
进程创建/退出(插入/删除链表) O(1) 链表操作,常数时间
SMP 调度(多 CPU) O(Nrunnable) + 锁竞争 全局链表 + 大内核锁,CPU 数越多竞争越严重
实际性能数据: 在 2.4 内核时代,当系统运行 100 个以上进程时,调度开销开始显著影响系统响应。当进程数达到 1000 时,调度器本身可能消耗 5%~10% 的 CPU 时间。这也是为什么 2.6 内核必须引入 O(1) 调度器的根本原因。

3.6 O(n)调度器架构总览

下面的 SVG 图展示了 O(n)调度器的完整架构,包括全局运行队列、调度决策流程和时间片重新计算路径。

Linux 2.4 O(n)调度器架构图 Linux 2.4 O(n) 调度器架构 全局运行队列 (runqueue_head) 双向循环链表 | 所有 CPU 共享 | 大内核锁保护 进程 A counter=7 进程 B counter=3 进程 C counter=0 进程 D counter=5 进程 E counter=2 schedule() 调度决策 遍历链表 → 计算 goodness → 选择最大值 goodness() 评分函数 实时: 1000+rt_priority 普通: counter + (20-prio) + 缓存奖励 recalc_task_timeslice() 遍历所有进程(含睡眠) counter = counter/2 + priority 上下文切换 (switch_to) 保存/恢复寄存器、栈、内存映射 性能瓶颈分析 ① 每次调度 O(N) 遍历 → 进程数越多越慢 ② 时间片重新计算 O(所有进程) → 大规模系统不可接受

3.7 O(n)调度器的历史局限

从今天的视角看,O(n)调度器存在以下明显缺陷,这些缺陷直接推动了 O(1) 调度器的诞生:

  • 可扩展性差: 调度开销随进程数线性增长,在桌面系统(几十个进程)尚可接受,但在服务器(数百上千进程)场景下成为瓶颈。
  • SMP 性能低下: 全局运行队列和大内核锁导致多 CPU 系统无法并行调度,CPU 越多锁竞争越严重。
  • 交互性响应不稳定: 时间片重新计算时所有进程的 counter 被重置,导致交互式进程无法获得及时响应。
  • 缺乏 CPU 亲和性支持: 虽然 goodness 函数有简单的缓存奖励,但没有显式的 CPU 绑定机制,进程在不同 CPU 间频繁迁移,缓存命中率低。
历史评价: O(n)调度器在 Linux 2.4 时代是合理的工程选择——当时硬件 CPU 数量少、进程数有限。但随着 SMP 服务器普及和桌面多任务需求增长,它的设计缺陷逐渐暴露。2003 年,Ingo Molnár 开发的 O(1) 调度器在 2.6 内核中取代了它,开启了 Linux 调度器的新纪元。