Linux内核进程调度器runqueue就绪队列深度解析 - 第1节

1.1 Linux调度器发展历史

Linux内核调度器经历了从简单到复杂、从低效到高效的演进过程。理解这段历史有助于把握当前CFS调度器的设计取舍。下表总结了三个主要阶段的对比:

版本 内核版本 时间复杂度 核心数据结构 主要问题
O(n)调度器 Linux 2.4及之前 O(n) 全局链表 多核扩展性差、交互体验不佳
O(1)调度器 Linux 2.6.0~2.6.22 O(1) 140个优先级位图+数组 交互进程响应不够平滑、调参复杂
CFS调度器 Linux 2.6.23至今 O(log n) 红黑树 ——(当前标准)

1.1.1 O(n)调度器:简单但低效

早期Linux使用基于全局循环链表的调度器。每次调度时,内核需要遍历所有就绪进程,计算每个进程的优先级动态修正值,选择“最好”的进程执行。

性能瓶颈:当系统中存在1000个进程时,每次调度都需要扫描整个链表,时间复杂度为O(n)。多核处理器下,全局链表成为锁竞争热点,扩展性极差。
/* O(n)调度器核心选择逻辑(简化示意) */
for_each_process(p) {
    goodness = p->counter + p->priority;
    if (goodness > best_goodness) {
        best_goodness = goodness;
        next = p;
    }
}

1.1.2 O(1)调度器:位图加速的里程碑

O(1)调度器由Ingo Molnár设计,引入优先级位图和双数组结构(active/expired),将调度决策时间降为常数级。每个CPU维护独立的运行队列,大幅提升多核性能。

设计亮点:使用find_first_bit()位图操作在140个优先级中快速定位最高优先级队列,时间复杂度恒定为O(1),不受进程数量影响。
/* O(1)调度器优先级位图查找(简化) */
struct prio_array {
    unsigned long bitmap[BITMAP_SIZE];  /* 140位位图 */
    struct list_head queue[MAX_PRIO];   /* 140个链表 */
};

/* 查找最高优先级非空队列 */
idx = sched_find_first_bit(array->bitmap);
next = list_entry(array->queue[idx].next, task_t, run_list);

然而O(1)调度器存在交互进程识别不准的问题,导致桌面响应出现“卡顿”或“抖动”,最终被CFS取代。

1.1.3 CFS调度器:公平与效率的统一

CFS(Completely Fair Scheduler)由Con Kolivas的楼梯调度器和RSDL启发,由Ingo Molnár在Linux 2.6.23中合入。它摒弃了固定时间片和优先级数组,改用红黑树组织就绪队列,以虚拟运行时间(vruntime)作为调度依据。

核心思想:CFS模拟“理想多任务处理器”——假设一个CPU能同时运行所有进程,每个进程获得1/N的CPU时间。实际调度时,选择vruntime最小的进程执行,使所有进程的vruntime尽可能接近。
/* CFS调度器核心选择逻辑(简化) */
static struct sched_entity *pick_next_entity(struct cfs_rq *cfs_rq)
{
    struct rb_node *left = cfs_rq->rb_leftmost;
    struct sched_entity *se = rb_entry(left, struct sched_entity, run_node);
    return se;  /* 红黑树最左节点 = vruntime最小 */
}
Linux调度器演进时间线 O(n) 调度器 Linux 2.4 全局链表遍历 O(1) 调度器 Linux 2.6.0 位图+双数组 CFS 调度器 Linux 2.6.23 红黑树+vruntime 各阶段核心特性对比 ● O(n):单链表、全局锁、动态优先级计算 ● O(1):位图加速、per-CPU队列、时间片轮转 ● CFS:红黑树、虚拟运行时间、完全公平 关键转折:O(1)→CFS 解决了交互进程响应不平滑的问题 当前状态:CFS 仍是主线默认调度器,EAS/RT 作为补充

1.2 调度器核心目标与设计哲学

无论调度器如何演进,其核心目标始终围绕以下三个维度展开。CFS的设计哲学正是对这些目标的重新诠释和平衡。

1.2.1 三大核心目标

目标 定义 CFS实现方式 衡量指标
公平性 每个进程获得与其权重成比例的CPU时间 vruntime单调递增,红黑树保证最小vruntime优先 最大vruntime差距 < sysctl_sched_latency
吞吐量 单位时间内完成的工作量最大化 调度粒度(min_granularity)防止频繁切换 上下文切换次数/秒
响应延迟 交互进程从就绪到获得CPU的时间最小化 唤醒抢占、nice值映射到权重 调度延迟 < 4ms(桌面场景)
设计哲学:CFS不追求“最优”而追求“足够好”。它通过权重分配(而非固定优先级)实现公平,通过调度粒度控制平衡吞吐与延迟,通过唤醒抢占保证交互响应。

1.2.2 CFS设计哲学:完全公平的数学建模

CFS将调度问题抽象为:在时间区间[0, T]内,N个进程应各自获得T/N的CPU时间。实际运行时,进程可能因I/O阻塞、睡眠等原因未用完时间片,CFS通过虚拟运行时间记录每个进程的“已消耗CPU时间”,并选择消耗最少的进程运行。

/* vruntime更新核心公式 */
vruntime += delta_exec * (NICE_0_LOAD / se->load.weight);

/* 其中:
 * delta_exec  —— 实际执行时间(ns)
 * NICE_0_LOAD —— nice=0时的权重(1024)
 * load.weight —— 当前进程的权重(由nice值映射)
 */

权重映射表(部分):

nice值 权重(weight) CPU占比(相对nice=0)
-20 88761 约86.7倍
-10 9548 约9.3倍
0 1024 1倍(基准)
10 110 约0.11倍
19 15 约0.015倍
CFS调度器核心目标与设计哲学 公平性 vruntime最小优先 权重比例分配CPU 吞吐量 调度粒度控制 减少上下文切换 响应延迟 唤醒抢占机制 交互进程优先 CFS设计哲学:三个平衡 公平 vs 效率 权重分配+调度粒度 交互 vs 批处理 唤醒抢占+睡眠补偿 通用 vs 实时 CFS+RT调度类共存

1.2.3 调度类与调度策略

Linux内核通过调度类(sched_class)实现多态调度,CFS是其中之一。每个进程关联一个调度策略,决定其所属的调度类:

调度类 调度策略 优先级范围 典型用途
stop_sched_class —— 最高(内核内部) CPU停止、热插拔
dl_sched_class SCHED_DEADLINE 实时(0~99) 硬实时任务
rt_sched_class SCHED_FIFO / SCHED_RR 实时(0~99) 软实时任务
fair_sched_class SCHED_NORMAL / SCHED_BATCH 普通(100~139) 普通进程、批处理
idle_sched_class SCHED_IDLE 最低 低优先级后台任务
重点理解:CFS只负责fair_sched_class(即SCHED_NORMAL/SCHED_BATCH策略)的调度。实时进程由rt_sched_class处理,优先级高于CFS。当系统中存在实时进程时,CFS进程只有在实时进程阻塞后才有机会运行。

1.2.4 调度器核心数据结构关系

每个CPU维护一个运行队列(struct rq),其中包含多个调度类的子队列。CFS对应的子队列是struct cfs_rq,其核心是红黑树根节点和调度实体链表。

/* 每个CPU的运行队列结构(简化) */
struct rq {
    struct cfs_rq    cfs;        /* CFS就绪队列 */
    struct rt_rq     rt;         /* 实时就绪队列 */
    struct dl_rq     dl;         /* Deadline就绪队列 */
    struct task_struct *curr;    /* 当前正在运行的进程 */
    u64              nr_running; /* 就绪进程总数 */
    u64              nr_switches;/* 上下文切换计数 */
};

/* CFS就绪队列核心字段(简化) */
struct cfs_rq {
    struct rb_root   tasks_timeline;  /* 红黑树根节点 */
    struct rb_node   *rb_leftmost;    /* 最左节点(vruntime最小) */
    struct sched_entity *curr;        /* 当前正在运行的调度实体 */
    u64              min_vruntime;    /* 队列最小虚拟运行时间 */
    u64              nr_running;      /* 就绪实体数量 */
    u64              exec_clock;      /* 执行时钟 */
};