

目录
1️⃣竞争性(Competitiveness):调度存在的根本原因
[2️⃣ 独立性(Independence):调度的核心保障目标](#2️⃣ 独立性(Independence):调度的核心保障目标)
[3️⃣并行(Parallelism):多 CPU 的 "真正同时"](#3️⃣并行(Parallelism):多 CPU 的 “真正同时”)
[4️⃣并发(Concurrency):单 CPU 的 "伪同时",调度切换的直接结果](#4️⃣并发(Concurrency):单 CPU 的 “伪同时”,调度切换的直接结果)
[3.1活动队列(Active Queue)🧐](#3.1活动队列(Active Queue)🧐)
[2️⃣核心结构拆解:3 个关键组成部分](#2️⃣核心结构拆解:3 个关键组成部分)
[3️⃣调度核心流程:从 "选进程" 到 "执行" 的 4 步逻辑](#3️⃣调度核心流程:从 “选进程” 到 “执行” 的 4 步逻辑)
[1. 未优化版流程:遍历查找(效率较低)](#1. 未优化版流程:遍历查找(效率较低))
[2. 位图优化版流程:位运算加速(效率跃升)](#2. 位图优化版流程:位运算加速(效率跃升))
[4️⃣关键细节:2 个容易混淆的核心逻辑](#4️⃣关键细节:2 个容易混淆的核心逻辑)
[1. 优先级与队列的映射:下标即优先级](#1. 优先级与队列的映射:下标即优先级)
[2. 位图的冗余设计:160 位覆盖 140 个优先级](#2. 位图的冗余设计:160 位覆盖 140 个优先级)
[5️⃣核心价值:为何活动队列是 O (1) 调度器的关键?](#5️⃣核心价值:为何活动队列是 O (1) 调度器的关键?)
[3.2过期队列(Expired Queue)🧐](#3.2过期队列(Expired Queue)🧐)
[3.3活动队列(Active Queue) and 过期队列(Expired Queue)🗝️](#3.3活动队列(Active Queue) and 过期队列(Expired Queue)🗝️)
[3️⃣核心流转逻辑:进程在双队列间的 3 种关键移动](#3️⃣核心流转逻辑:进程在双队列间的 3 种关键移动)
[1. 场景 1:活动队列进程时间片耗尽 → 转移至过期队列](#1. 场景 1:活动队列进程时间片耗尽 → 转移至过期队列)
[2. 场景 2:新就绪进程加入 → 直接入过期队列](#2. 场景 2:新就绪进程加入 → 直接入过期队列)
[3. 场景 3:活动队列为空 → 双队列角色互换 + 过期队列时间片重算](#3. 场景 3:活动队列为空 → 双队列角色互换 + 过期队列时间片重算)
[4️⃣调度周期:双队列驱动的 "循环调度模型"](#4️⃣调度周期:双队列驱动的 “循环调度模型”)
[5️⃣核心价值:双队列设计为何支撑 O (1) 调度效率?](#5️⃣核心价值:双队列设计为何支撑 O (1) 调度效率?)
[3.4active 和 expired 结构体指针🧐](#3.4active 和 expired 结构体指针🧐)
[1️⃣核心定义:指针的 "身份绑定" 与 "动态指向"](#1️⃣核心定义:指针的 “身份绑定” 与 “动态指向”)
[2️⃣核心作用:避免 "数据搬运",实现 "瞬时切换"](#2️⃣核心作用:避免 “数据搬运”,实现 “瞬时切换”)
[3️⃣完整切换逻辑:从 "活动队列为空" 到 "指针交换" 的 4 步流程](#3️⃣完整切换逻辑:从 “活动队列为空” 到 “指针交换” 的 4 步流程)
[1. 触发条件:active 指针指向的队列空了](#1. 触发条件:active 指针指向的队列空了)
[2. 前置操作:为 expired 指针指向的队列重算时间片](#2. 前置操作:为 expired 指针指向的队列重算时间片)
[3. 核心操作:交换 active 与 expired 指针的指向](#3. 核心操作:交换 active 与 expired 指针的指向)
[4. 恢复调度:从新 active 指针指向的队列挑选进程](#4. 恢复调度:从新 active 指针指向的队列挑选进程)
[4️⃣核心价值:指针设计如何支撑 O (1) 调度效率?](#4️⃣核心价值:指针设计如何支撑 O (1) 调度效率?)
[1. 队列切换时间 O (1):避免进程数据拷贝的 "性能杀手"](#1. 队列切换时间 O (1):避免进程数据拷贝的 “性能杀手”)
[2. 逻辑简洁性:固定 "角色",降低代码复杂度](#2. 逻辑简洁性:固定 “角色”,降低代码复杂度)
[3. 时间片管理高效:批量重算 + 指针切换,兼顾效率与公平](#3. 时间片管理高效:批量重算 + 指针切换,兼顾效率与公平)
前言
在 Linux 操作系统中,进程的 调度 与 切换 是操作系统核心功能之一,它涉及到如何有效地利用CPU资源,保证系统的响应速度和吞吐量。
那么 Linux 是如何完成进程的调度与切换的呢? 本篇博客将会带大家一起了解一下 Linux 下的进程 调度 与切换。
一、补充概念-竞争、独⽴、并⾏、并发
- 竞争性: 系统进程数⽬众多,⽽CPU资源只有少量,甚⾄1个,所以进程之间是具有竞争属性的。为 了⾼效完成任务,更合理竞争相关资源,便具有了优先级
- 独⽴性: 多进程运⾏,需要独享各种资源,多进程运⾏期间互不⼲扰
- 并⾏: 多个进程在多个CPU下分别,同时进⾏运⾏,这称之为并⾏
- 并发: 多个进程在⼀个CPU下采⽤进程切换的⽅式,在⼀段时间之内,让多个进程都得以推进,称 之为并发
竞争、独⽴、并⾏、并发这四个概念,是理解 Linux 进程调度与切换底层逻辑的核心基础 ------ 它们清晰地解释了 "为什么需要调度"(竞争性)、"调度要保证什么"(独立性),以及 "调度最终实现了什么效果"(并行与并发)。下面结合 Linux 系统的实际运行场景,对这四个概念进行更深入的拆解和关联说明,帮助串联起 "概念" 与 "实际机制" 的联系:
1️⃣竞争性(Competitiveness):调度存在的根本原因
进程的竞争性,直接源于 "CPU 资源稀缺" 与 "进程数量庞大" 的矛盾 ,这也是 Linux 需要 "进程调度器" 的核心动因。
- 核心矛盾:Linux 系统中,可运行的进程(如终端命令、后台服务、应用程序)数量往往远多于物理 CPU 核心数(可能是 1 个、2 个、8 个等)。
例如,一台单核服务器上可能同时运行着sshd
(远程服务)、nginx
(Web 服务)、bash
(终端进程)等十几个进程,但同一时间 CPU 只能 "专心" 处理一个进程。- 调度的解决方案:为了避免进程因 "抢不到 CPU" 而无限等待,调度器通过 "优先级机制" 和 "时间分配策略"化解竞争:
- 优先级:像 Linux 的实时进程(优先级 0-99)会抢占普通进程(Nice 值 - 20~19),确保关键任务(如工业控制、音频处理)优先获得 CPU;
- 时间分配:普通进程通过 CFS 调度器的 "虚拟运行时间(vruntime)",确保高优先级进程(Nice 值小)获得更多 CPU 时间片,避免低优先级进程 "饿死"。
- 本质:竞争性决定了 "调度必须存在",而调度策略则是 "公平且高效解决竞争的规则"。
2️⃣ 独立性(Independence):调度的核心保障目标
进程的独立性,是指多个进程在运行过程中,各自的资源(内存、CPU 上下文、文件句柄等)互不干扰,即使一个进程崩溃,也不会直接导致其他进程或系统崩溃 ------ 这是 Linux 多任务运行的 "安全底线",也是调度切换时必须严格保证的特性。
- Linux 如何实现独立性❓
- 地址空间隔离:每个进程有独立的虚拟地址空间(通过页表机制实现),进程 A 无法直接访问进程 B 的内存数据,避免 "内存越界" 干扰;
- 上下文私有:进程切换时,调度器会将当前进程的 "上下文"(寄存器值、程序计数器 PC、栈指针 SP 等)完整保存到进程控制块(PCB,Linux 中是task_struct结构体) 中,再加载新进程的上下文 ------ 确保新进程运行时,完全使用自己的 "执行状态",不会继承上一个进程的残留信息;
- 资源独占:进程打开的文件、网络连接等资源,会记录在自己的
task_struct
中,其他进程无法直接修改,只能通过内核提供的 "进程间通信(IPC)" 机制(如管道、信号、共享内存)间接交互。
- 例子:如果一个
vim
编辑器进程因错误崩溃,不会影响后台运行的mysql
数据库进程 ------ 这就是独立性的直观体现,而调度器在切换vim
和mysql
时,正是通过 "上下文隔离" 和 "地址空间隔离" 保证了这种独立性。3️⃣并行(Parallelism):多 CPU 的 "真正同时"
并行是 "物理层面的同时运行",依赖于多 CPU 核心(或多处理器),是 Linux 充分利用硬件资源的关键方式。
- 核心特征:
- 硬件基础:必须有多个物理 CPU 核心(如双核、四核 CPU);
- 运行状态:多个进程 "真正同时" 执行 ------ 例如,CPU 核心 1 正在运行
mysql
进程,CPU 核心 2 同时运行nginx
进程,两个进程的指令在同一时刻被不同 CPU 执行,没有 "等待" 或 "切换";- Linux 中的体现:Linux 调度器会自动将可运行进程 "分配到不同 CPU 核心"(称为 "负载均衡"),避免某一个核心过载、其他核心空闲。例如,CFS 调度器会监控每个 CPU 核心的 "运行队列长度",将进程迁移到负载较轻的核心,最大化并行效率。
- 误区澄清⚠️:并行≠"速度翻倍"------ 如果多个进程都需要访问同一块共享内存(如多个线程写同一个文件),会因 "锁竞争" 导致并行效率下降,但这是 "资源竞争" 问题,而非并行本身的局限。
4️⃣并发(Concurrency):单 CPU 的 "伪同时",调度切换的直接结果
并发是 "时间层面的交替推进",即使只有 1 个 CPU 核心,通过 Linux 的 "进程切换" 机制,也能让多个进程" "看起来同时在运行"------ 这是调度器最核心的功能体现。
- 核心逻辑(以单核 CPU 为例):
- 进程 A 先获得 CPU 时间片(如 10ms),开始执行;
- 10ms 后时间片用完,调度器触发 "进程切换":保存进程 A 的上下文到 PCB,加载进程 B 的上下文;
- 进程 B 执行 10ms 后,再次切换到进程 C;
- 一段时间内(如 1 秒),进程 A、B、C 都得到了执行,虽然 "不是真正同时",但从用户视角看,"终端在输入、浏览器在加载、音乐在播放",仿佛同时进行。
- 与调度的关联:并发的 "流畅度" 完全依赖于调度策略:
- 时间片过短:切换过于频繁,上下文切换开销(保存 / 加载 PCB、刷新缓存等)占比过高,系统吞吐量下降;
- 时间片过长:进程响应延迟增加(如用户点击鼠标后,需要等 1 秒才出现反馈);
- Linux 的优化:CFS 调度器会动态调整时间片(默认最小 1ms,最大 200ms),平衡 "响应速度" 和 "切换开销",确保并发时用户无明显卡顿。
- 并行与并发的关系:
- 单核 CPU:只能实现 "并发"(通过切换),无法实现 "并行";
- 多核 CPU:"并行" 和 "并发" 同时存在 ------ 不同核心上的进程并行运行,同一核心上的进程通过切换实现并发;
- 本质:并行是 "空间上的多任务"(多 CPU),并发是 "时间上的多任务"(单 CPU 切换),Linux 调度器的目标是 "在多核下最大化并行效率,在单核下优化并发体验"。
🧐总结:四个概念的逻辑串联
|---------|----------------------|---------------------------------------|
| 概念 | 核心作用 | 与 Linux 调度 / 切换的关联 |
| 竞争性 | 解释 "为什么需要调度" | 催生优先级、时间片等调度策略,解决 CPU 资源竞争问题 |
| 独立性 | 定义 "调度要保证什么" | 通过 PCB 上下文隔离、地址空间隔离,确保进程互不干扰 |
| 并行 | 体现 "多 CPU 的硬件潜力" | 调度器通过负载均衡,将进程分配到多核心,实现真正同时运行 |
| 并发 | 体现 "单 CPU 的调度价值" | 通过进程切换,让单 CPU 在一段时间内推进多个进程,模拟同时运行 |理解这四个概念后,再去看 Linux 的 CFS 调度器、实时调度策略、进程切换机制,就能更清晰地明白:这些机制本质上都是为了 "在竞争性前提下,保证独立性,同时最大化并行与并发效率"。
二、🔥进程切换🔥
我们知道 一个CPU在 同一时间只能运行一个进程,而 -- 并发 -- 实际上就是 -- 利用时间片,让每个进程在CPU上只能运行一个时间片的时间,然后就被切换到另一个进程,所以我们计算机虽然看起来似乎是非常流畅的运行每个进程,而实际上则是一卡一卡的运行的,只不过这个时间非常短,我们感觉不到罢了。
那 进程 首次调度完成 被切换走,当 CPU 二次调度该进程 时,是如何记得上次执行到哪里了呢❓
CPU上下⽂切换:当多任务内核决定运⾏另外的任务 时, 它保存正在运⾏任务的当前状态, 也就是CPU寄存器中的全部内容。这些内容被保存在任务⾃⼰的堆栈中, ⼊栈⼯作完成后就把下⼀个将要运⾏的任务的当前状况从该任务的栈中重新装⼊CPU寄存器, 并开始下⼀个任务的运⾏, 这⼀过程就是context switch。
CPU中存在有大量的寄存器,进程运行产生的临时数据都被保存在这些寄存器中,这些临时数据被称为进程的硬件上下文,当时间片消耗完的时候,进程会保存这些上下文,现阶段大家可以理解为保存到PCB中,当进程被二次调度时,进程会将曾经保存的硬件上下文进行恢复(将之前保存的硬件上下文覆盖到CPU的寄存器中)。
参考⼀下Linux内核0.11代码
进程切换包括以下几个关键步骤:
- 上下文保存: 当操作系统决定要切换到另一个进程时,首先需要保存当前进程的上下文信息,包括程序计数器、寄存器内容、栈指针等。这些信息存储在进程的控制块(PCB)中。
- 选择新进程: 在确定要切换到哪个新进程之前,操作系统会根据调度算法从就绪队列中选择一个合适的进程。这个选择可能基于进程的优先级、先到先服务(FIFO)、轮转法等。
- 加载新进程的上下文: 一旦确定了新进程,操作系统就会从其对应的PCB中恢复该进程的上下文信息。这包括将新进程的程序计数器值加载到CPU中,以便执行新进程的代码。
总结:
- 虽然CPU中的寄存器只有一套,但是寄存器内部保存的数据可以有多套。
- CPU是被所有进程共享的,但内部的数据却是进程私有的。
大家可以理解为:在任意一个时刻,CPU中 的数据只属于一个进程。所以看起来,我们用的是同一个设备,但实际上进程之间是具有独立性的。
独立性:进程运行需要独享各种资源,多进程运行期间互不干扰。
注意🗝️:
时间⽚:当代计算机都是分时操作系统,每个进程都有它合适的时间⽚(其实就是⼀个计数
器)。时间⽚到达,进程就被操作系统从CPU中剥离下来。
三、🔥进程调度🔥
进程调度是操作系统根据一定的调度策略从就绪队列中选择下一个要执行的进程的过程。调度策略的选择会影响系统的性能、响应速度和资源利用率
这是Linux系统下对运行队列的设计。
不考虑其他成员,我们只看圈出来的两个部分:
红色部分为活动队列,蓝色部分为过期队列,他们两个你可以认为是完全相同的两个结构。
- 进程队列数组 queue[140]:这个数组用于存储不同优先级的进程队列。每个队列按照先进先出(FIFO)规则进行排队调度。数组的下标表示进程的优先级,因此可以直接根据优先级来访问对应的进程队列,提高了访问效率。
- 进程队列状态位图 bitmap[5]:为了快速判断哪些队列是非空的,使用了一个位图来表示每个队列的状态。每个比特位对应一个队列,如果该队列非空,则对应的比特位为1;否则为0。这样,查找非空队列的操作变得高效,时间复杂度为常数级别。
- active指针和expired指针:这两个指针用于指示当前活跃队列和过期队列。随着调度的进行,它们的内容可以交换,从而实现活跃队列和过期队列的动态切换。
- 活跃队列 和 过期队列:活跃队列中包含当前活跃的进程,而过期队列包含一段时间内未被调度的进程。Linux 内核根据需要从活跃队列和过期队列中选择进程进行调度,以平衡优先级和资源利用效率。
- O(1) 调度算法:Linux 内核的调度器通常采用 O(1) 调度算法(使用了位图(bitmap)来实现),该算法在常数时间内选择下一个要执行的进程,而不受进程数量的影响。这确保了调度器的高效性,使得系统在任何负载情况下都能快速响应。
3.1活动队列(Active Queue)🧐
活动队列(Active Queue)是 Linux 早期 O (1) 调度器的核心数据结构,其设计目标是高效管理 "时间片未耗尽的就绪进程",并通过优先级调度保证高优先级进程优先执行,同时借助位图优化将调度决策效率提升至 O (1) 级别。
1️⃣核心定义:什么是活动队列?
活动队列是操作系统内核中专门存储 "时间片尚未用完、处于可运行状态(TASK_RUNNING) " 进程的容器。它的核心作用是:
- 按优先级对就绪进程进行分类管理,确保高优先级进程优先获得 CPU;
- 快速统计就绪进程总数,辅助判断系统负载;
- 为调度器提供 "快速挑选下一个运行进程" 的接口,降低调度开销。
简言之,活动队列是 "就绪进程的管理中心",是连接进程状态与 CPU 调度的关键桥梁。
2️⃣核心结构拆解:3 个关键组成部分
活动队列的结构围绕 "优先级管理" 和 "高效查找" 设计,包含 3 个核心字段,具体功能如下表所示:
cppstruct active_queue { int nr_active; // 运行状态进程总数 struct task_struct *queue[140]; // 按优先级划分的进程队列(0-139) unsigned long bitmap[5]; // 队列状态位图(5×32=160位,覆盖140个优先级) };
|------------------|------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------|
| 字段名 | 数据类型 / 规模 | 核心作用 |
| nr_active | 整数(int) | 统计活动队列中所有就绪进程的总数,无需遍历队列即可快速获取系统就绪进程数量,辅助判断负载。 |
| queue[140] | 进程队列数组(共 140 个) | 按优先级划分的进程存储容器: -数组下标 = 进程优先级(0 最高,139 最低); -每个下标对应一个 "FIFO 队列",相同优先级的进程按 "先进先出" 规则排队,避免同优先级进程饥饿。 |
| bitmap[5] | 位图数组(共 5×32=160 位) | 优化 "查找非空队列" 的效率: - 每 1 个比特位对应 1 个queue
队列; - 比特位 = 1 → 对应队列非空(有就绪进程); - 比特位 = 0 → 对应队列空(无就绪进程); - 用 160 位覆盖 140 个优先级,预留冗余空间。 |总结:活跃队列 ---- 表示当前CPU正在执行的运行队列,而 正在执行的运行队列(也就是活跃队列)是不可以增加新的进程的。
3️⃣调度核心流程:从 "选进程" 到 "执行" 的 4 步逻辑
活动队列的核心价值体现在 "调度器挑选下一个运行进程" 的过程中,分为未优化版和位图优化版,两者的效率差异是设计的关键。
1. 未优化版流程:遍历查找(效率较低)
未优化时,调度器通过 "遍历优先级队列" 挑选进程,流程如下:
- 遍历优先级:从最高优先级(
queue[0]
)开始,依次检查queue[0]
→queue[1]
→...→queue[139]
;- 找非空队列:第一个 "非空的队列" 就是 "当前最高优先级的就绪进程队列"(因为从最高优先级开始遍历);
- 取队首进程:从该非空队列中取出 "第一个进程"(FIFO 规则),该进程就是下一个要运行的进程;
- 执行调度:将该进程的上下文恢复到 CPU 寄存器,开始执行。
问题:最坏情况下需要遍历 140 个队列(如只有
queue[139]
非空),虽为 "常数时间",但仍有优化空间。2. 位图优化版流程:位运算加速(效率跃升)
为解决 "遍历 140 个队列" 的低效问题,
bitmap
位图成为核心优化手段,流程如下:
- 扫描位图找最高优先级:调度器通过 "位运算指令"(如 x86 的
bsf
指令,查找第一个为 1 的比特位)快速定位bitmap
中 "第一个值为 1 的比特位";
- 例:若
bitmap[0]
的第 5 位为 1,则对应queue[5]
是当前最高优先级的非空队列;- 计算队列下标:通过比特位的位置,直接计算出对应的
queue
数组下标(无需遍历);- 取队首进程:从该下标对应的
queue
队列中取出第一个进程;- 执行调度:恢复进程上下文,开始运行。
效率提升:将 "最多 140 次队列检查" 简化为 "最多 5 次整数比较 + 1 次位运算",调度决策时间从 "依赖优先级分布" 变为 "固定的 O (1) 时间",这也是 O (1) 调度器名称的由来。
4️⃣关键细节:2 个容易混淆的核心逻辑
1. 优先级与队列的映射:下标即优先级
活动队列中 "优先级" 与 "队列" 的映射是 "硬绑定":
- 优先级 0 →
queue[0]
(最高优先级,如内核关键进程);- 优先级 139 →
queue[139]
(最低优先级,如后台闲置进程);- 同优先级进程按 FIFO 排队,避免 "高优先级进程长期占用 CPU 导致同优先级进程饥饿"。
2. 位图的冗余设计:160 位覆盖 140 个优先级
bitmap
数组用 5 个 32 位整数(共 160 位)覆盖 140 个优先级,而非刚好 140 位(需 5 个 32 位整数,因 4×32=128 位不足):
- 设计考量:避免 "为节省 20 位而额外处理部分整数",用冗余空间简化代码逻辑,同时为后续优先级扩展预留空间;
- 实际使用:仅前 140 位对应有效队列,后 20 位恒为 0,不影响调度逻辑。
5️⃣核心价值:为何活动队列是 O (1) 调度器的关键?
活动队列的设计直接支撑了 Linux 早期多任务调度的高效性,核心价值体现在 3 点:
- 优先级调度的准确性:通过
queue[140]
按优先级分类,确保高优先级进程优先执行,满足实时性需求(如内核中断处理进程);- 调度决策的高效性:借助
bitmap
位图将调度决策时间降至 O (1),即使系统有大量就绪进程,调度器也能快速挑选下一个进程,减少 CPU 空耗;- 状态管理的简洁性:
nr_active
快速统计就绪进程数,queue
按规则存储进程,bitmap
优化查找,三者协同让 "就绪进程管理" 逻辑清晰、易于维护。
3.2过期队列(Expired Queue)🧐
- 过期队列 和 活动队列 --- 结构一模一样
- 过期队列上放置的进程,都是时间片耗尽的进程、
- 当活动队列上的进程都被处理完毕之后,对过期队列的进程进行时间片重新计算
总结:
- 某个处在活跃队列中的进程的时间片消耗完,该进程就会从活跃队列中剥离,然后被添加到过期队列。
- 当活跃队列正在执行时如果有进程需要添加进运行队列,那么就会添加至过期队列当中,也就是说 活跃队列的进程一直在减少,而过期队列中的进程一直在增多!
3.3活动队列(Active Queue) and 过期队列(Expired Queue)🗝️
活动队列(Active Queue) 与过期队列(Expired Queue) 是一对 "协同工作的双队列结构",二者通过 "分离时间片未耗尽与已耗尽的进程",结合 "时间片重算机制",实现了高效、公平的多任务调度。
1️⃣核心定义:双队列的本质分工
活动队列与过期队列的核心差异在于 "存储的进程状态",二者分工明确,共同覆盖 "就绪进程" 的全生命周期管理:
|----------|---------------------------------------------|-------------------------------------------------------------------------------------------------------|
| 队列类型 | 核心功能 | 存储的进程特征 |
| 活动队列 | 管理 "可立即调度执行" 的进程,是调度器挑选下一个运行进程的 "主要数据源" | 1. 时间片尚未耗尽; 2. 处于可运行状(TASK_RUNNING); 3. 按优先级 FIFO 排队,高优先级进程优先被调度。 |
| 过期队列 | 暂存 "暂时无法调度" 的进程,等待时间片重算后重新进入调度循环 | 1. 时间片已耗尽(从活动队列转移而来) 2. 处于可运行状(TASK_RUNNING),但需等待时间片重置; 3. 新加入的就绪进程(若活动队列正在调度,新进程直接入过期队列) |2️⃣结构共性:完全一致的设计基础
正如你提到的,活动队列与过期队列的数据结构完全相同------ 这是 O (1) 调度器的关键设计,目的是简化 "队列切换" 和 "时间片重算" 的逻辑,二者的结构均包含 3 个核心字段:
- nr_active: 总共有多少个运行状态的进程
- queue[140]: 一个元素就是一个进程队列,相同优先级的进程按照FIFO规则进行排队调度,所以,数组下标就是优先级!
- bitmap[5]:一共140个优先级,一共140个进程队列,为了提高查找非空队列的效率,就可以用5*32个比特位表示队列是否为空,这样,便可以大大提高查找效率
结构一致的优势:后续 "活动队列与过期队列角色互换" 时,无需修改任何数据结构相关的代码,仅需交换两个队列的指针,极大简化了调度逻辑。
3️⃣核心流转逻辑:进程在双队列间的 3 种关键移动
进程的生命周期中,会在活动队列与过期队列间根据 "时间片消耗" 和 "调度状态" 进行移动,核心流转场景有 3 种:
1. 场景 1:活动队列进程时间片耗尽 → 转移至过期队列
这是最常见的流转,具体步骤:
- 活动队列中的某进程,在执行过程中耗尽了分配的时间片;
- 调度器将该进程从活动队列的对应优先级
queue
中移除(同时更新活动队列的nr_active
和bitmap
,若队列空则将对应比特位置 0);- 直接将该进程插入过期队列的相同优先级
queue
中(同时更新过期队列的nr_active
和bitmap
,将对应比特位置 1);- 活动队列的进程数减少(
nr_active--
),过期队列的进程数增加(nr_active++
)。2. 场景 2:新就绪进程加入 → 直接入过期队列
当活动队列 "仍有进程在调度"(即活动队列非空)时,若有新进程变为就绪状态(如从阻塞态唤醒、新进程创建),调度器会直接将其加入过期队列,而非活动队列:
- 原因:避免 "频繁向活动队列插入新进程" 打乱当前的调度顺序,同时保证活动队列的 "纯度"(仅包含时间片未耗尽的进程);
- 操作:根据新进程的优先级,将其插入过期队列对应优先级的
queue
,并更新过期队列的nr_active
和bitmap
。3. 场景 3:活动队列为空 → 双队列角色互换 + 过期队列时间片重算
当活动队列中的所有进程都被调度完毕(nr_active=0),此时触发 "双队列切换",具体步骤:
- 时间片重算:调度器遍历过期队列中的所有进程,为每个进程重新计算新的时间片(时间片长度与进程优先级相关,高优先级进程时间片更短,保证调度频率;低优先级进程时间片更长,减少切换开销);
- 角色互换:将 "活动队列" 与 "过期队列" 的指针互换 ------ 原过期队列变为新的 "活动队列",原活动队列(空)变为新的 "过期队列";
- 重新调度:新的活动队列(原过期队列)开始提供进程给 CPU 调度,循环进入下一个调度周期。
4️⃣调度周期:双队列驱动的 "循环调度模型"
活动队列与过期队列的协同,构成了 O (1) 调度器的完整调度周期,可概括为 "单周期消耗→双队列切换→新周期启动" 的循环:
周期 1:消耗活动队列
调度器从活动队列中挑选最高优先级进程执行,进程时间片耗尽后转移至过期队列;新就绪进程直接入过期队列。此阶段活动队列进程持续减少,过期队列进程持续增多,直到活动队列为空。
切换阶段:时间片重算 + 队列互换
活动队列为空后,调度器为过期队列所有进程重算时间片,然后交换双队列指针,原过期队列成为新活动队列。
周期 2:消耗新活动队列
重复周期 1 的逻辑,从新活动队列(原过期队列)挑选进程执行,直到新活动队列为空,再次触发切换。
通过这种循环,实现了 "所有就绪进程都能被公平调度",同时保证高优先级进程能优先执行。
5️⃣核心价值:双队列设计为何支撑 O (1) 调度效率?
活动队列与过期队列的设计,是 O (1) 调度器实现 "调度决策时间为常数" 的核心原因,其价值体现在 3 点:
调度决策 O (1):双队列均通过
bitmap
位图快速定位最高优先级非空队列,无论系统中有多少就绪进程,挑选下一个进程的时间均为 "固定位运算时间",不随进程数增加而变慢。时间片管理高效:分离 "时间片未耗尽" 和 "已耗尽" 的进程,避免在活动队列中频繁修改时间片;仅在队列切换时批量重算时间片,批量操作的时间复杂度为 O (1)(因优先级仅 140 个,遍历 140 个队列是常数时间).
公平性与实时性平衡
- 实时性:高优先级进程在活动队列中优先被调度,保证关键进程(如内核中断处理)的响应速度;
- 公平性:低优先级进程虽执行频率低,但时间片更长,且会随队列切换重新获得时间片,避免 "饥饿"(即长期无法被调度)。
总结
活动队列与过期队列是 Linux O (1) 调度器的 "双核":
- 结构上完全一致,简化切换逻辑;
- 功能上分工明确,分离 "可立即调度" 与 "需等待时间片重算" 的进程;
- 流转上通过 "消耗→切换→重算" 的循环,实现高效、公平的多任务调度;
- 最终支撑了 "调度决策 O (1)" 的核心目标,兼顾了系统的实时性与公平性,是早期 Linux 内核多任务调度的经典设计。
3.4active 和 expired 结构体指针🧐
active 和 expired 结构体指针 有什么用?
在 Linux O (1) 调度器的双队列(活动队列 / 过期队列)机制中,active 指针与expired 指针是实现 "队列角色快速切换" 的核心 "桥梁"------ 二者通过 "固定指向队列类型、动态交换指向目标" 的设计,避免了大量进程数据的拷贝,将 "队列切换" 的时间复杂度从 O (n) 降至 O (1),是支撑调度器高效运行的关键细节。
1️⃣核心定义:指针的 "身份绑定" 与 "动态指向"
active 指针与 expired 指针的核心特点是 "身份固定,指向可变"------ 它们的 "职责"(指向活动队列 / 过期队列)永远不变,但实际指向的 "队列实体" 会随调度周期动态切换,具体定义如下:
|----------------|-----------------------------|-------------------------------------------|----------------------------------------------------|
| 指针名称 | 固定职责(身份) | 动态指向的队列实体 | 关键特征 |
| active 指针 | 永远指向当前 "可调度进程所在的活动队列" | 调度周期初期:指向初始活动队列; 队列切换后:指向原过期队列(新活动队列) | 调度器仅从 active 指针指向的队列中挑选进程执行,是 "进程调度的唯一数据源"。 |
| expired 指针 | 永远指向当前 "暂存时间片耗尽进程的过期队列" | 调度周期初期:指向初始过期队列; 队列切换后:指向原活动队列(新过期队列) | 仅用于暂存进程,不参与调度挑选,直到与 active 指针交换指向后才成为 "新活动队列"。 |简单来说:指针的名字决定了它 "该做什么",而它的指向决定了 "操作哪个队列" ------ 比如 active 指针的 "职责" 是提供可调度进程,无论它实际指向的是初始活动队列还是原过期队列,只要它叫 active,调度器就从它指向的队列里选进程。
2️⃣核心作用:避免 "数据搬运",实现 "瞬时切换"
为什么需要这两个指针?核心原因是 "避免大量进程数据的拷贝"------ 如果没有指针,当活动队列为空时,需要将过期队列中的所有进程 "逐个移动" 到活动队列,这个过程的时间复杂度是 O (n)(n 为进程数),会随进程数增加而变慢,违背 O (1) 调度器的设计目标。
而 active 与 expired 指针的存在,通过 "交换指针指向" 替代 "搬运进程数据",解决了这个问题:
- 队列实体(活动队列、过期队列)本身不移动,始终在内存中固定存在;
- 仅通过交换两个指针的 "指向地址",就让原过期队列 "变" 成新活动队列,原活动队列 "变" 成新过期队列;
- 交换指针的操作是瞬时的(仅需修改两个指针变量的值),时间复杂度为 O (1),与进程数无关。
举个生活类比:你有两个箱子 A(初始活动队列)和 B(初始过期队列),A 装着 "可立即吃的水果",B 装着 "待清洗的水果"。active 指针指向 A(负责拿可吃的水果),expired 指针指向 B(负责放待洗的水果)。当 A 空了,你不需要把 B 里的水果一个个搬到 A 里,而是直接把 "active 标签" 贴到 B 上、"expired 标签" 贴到 A 上 ------ 此时 B 就成了 "新的可吃水果箱",A 成了 "新的待洗水果箱",指针的作用就相当于这两个 "可移动的标签"。
cppswap(&active,&expired);
3️⃣完整切换逻辑:从 "活动队列为空" 到 "指针交换" 的 4 步流程
active 与 expired 指针的交换,发生在 "活动队列所有进程被消耗完毕" 的关键时刻,完整流程与双队列的调度周期深度绑定,具体步骤如下:
1. 触发条件:active 指针指向的队列空了
调度器持续从 active 指针指向的队列(当前活动队列)中挑选进程执行:
- 进程时间片耗尽后,被从当前活动队列移除,插入 expired 指针指向的队列(当前过期队列);
- 新就绪进程直接插入当前过期队列;
- 直到 active 指针指向的队列中nr_active=0(无进程可调度),触发 "指针切换" 流程。
2. 前置操作:为 expired 指针指向的队列重算时间片
在交换指针前,必须先为 "当前过期队列"(expired 指针指向的队列)中的所有进程批量重算时间片------ 因为这些进程之前已耗尽时间片,需要赋予新的时间片才能重新参与调度:
- 调度器遍历 expired 指针指向的队列的queue[140]数组(仅 140 个优先级,是常数时间);
- 对每个非空队列中的进程,根据其优先级重新计算时间片(高优先级进程时间片短,低优先级进程时间片长);
- 重算完成后,当前过期队列中的所有进程都具备了 "可调度的时间片",为成为 "新活动队列" 做好准备。
3. 核心操作:交换 active 与 expired 指针的指向
这是整个切换流程中最关键的一步,操作极其简单:
- 定义临时指针temp;
- 将active指针的值(指向原活动队列的地址)赋给temp;
- 将expired指针的值(指向原过期队列的地址)赋给active;
- 将temp的值(原活动队列地址)赋给expired;
- 交换完成后:active指针 now 指向原过期队列(新活动队列),expired指针 now 指向原活动队列(新过期队列,为空)。
示例(伪代码):
cppstruct task_queue *active, *expired; // 定义两个队列指针 struct task_queue *temp; // 临时指针 // 当active指向的队列空了,执行交换 temp = active; active = expired; // active now 指向原过期队列(新活动队列) expired = temp; // expired now 指向原活动队列(新过期队列)
4. 恢复调度:从新 active 指针指向的队列挑选进程
指针交换完成后,调度器立即恢复工作:
- 从新的 active 指针指向的队列(原过期队列,已重算时间片)中,通过bitmap位图快速定位最高优先级非空队列;
- 取出队首进程执行,进入新的调度周期;
- 后续进程时间片耗尽后,仍插入当前 expired 指针指向的队列(原活动队列,此时为空,会逐渐被填满)。
4️⃣核心价值:指针设计如何支撑 O (1) 调度效率?
active 与 expired 指针的设计,是 O (1) 调度器实现 "全流程调度效率 O (1)" 的关键,其价值体现在 3 个核心层面:
1. 队列切换时间 O (1):避免进程数据拷贝的 "性能杀手"
如果没有指针,当活动队列为空时,需要将过期队列中的 n 个进程 "逐个搬运" 到活动队列,时间复杂度为 O (n)(n 为进程数,可能很大)。而指针交换仅需 3 次赋值操作,时间与进程数无关,是纯 O (1) 操作,彻底消除了 "数据搬运" 的性能开销。
2. 逻辑简洁性:固定 "角色",降低代码复杂度
指针的 "身份绑定"(active 永远对应活动队列,expired 永远对应过期队列)让调度器代码逻辑更清晰:
- 调度器挑选进程时,无需判断 "当前该操作哪个队列",只需固定操作 active 指针指向的队列;
- 进程时间片耗尽时,无需判断 "该插入哪个队列",只需固定插入 expired 指针指向的队列;
- 代码中无需大量条件判断,降低了维护成本和出错概率。
3. 时间片管理高效:批量重算 + 指针切换,兼顾效率与公平
指针交换前的 "批量时间片重算",是在 "队列切换" 这个固定节点执行的:
- 重算仅遍历 140 个优先级队列(常数时间 O (1)),而非遍历所有进程;
- 批量操作避免了 "进程运行中频繁修改时间片" 的开销,同时保证所有进程都能公平获得新时间片,避免低优先级进程 "饥饿"。
总结
active 指针与 expired 指针是 Linux O (1) 调度器的 "点睛之笔":
- 它们不存储进程数据,而是通过 "动态指向" 连接两个结构相同的队列;
- 核心作用是用 "O (1) 的指针交换" 替代 "O (n) 的进程搬运",实现队列角色的瞬时切换;
- 最终与双队列的 "分离管理""bitmap 优化" 协同,共同支撑了 O (1) 调度器 "决策快、切换快、管理高效" 的核心优势,成为早期 Linux 内核多任务调度的经典设计细节。
四、总结
📒✏️总结:
- ** 进程切换最重要的部分就是 进程上下文的保护和恢复。**
- ** 进程调度的优先级问题由 活跃进程数组的下标与进程优先级形成一种映射关系 解决。**
- ** 进程调度的时间复杂度问题由 位图和两个结构体指针 解决,时间复杂度控制在了O(1)。**
- ** 进程调度的进程饥饿问题由 活跃队列和过期队列 解决。**
结束语
以下就是我对【Linux系统编程】进程的切换与调度 的理解
感谢你的三连支持!!!
