
往期《Linux系统编程》回顾:/------------ 入门基础 ------------/ 【Linux的前世今生】 【Linux的环境搭建】 【Linux基础 理论+命令】(上) 【Linux基础 理论+命令】(下) 【权限管理】 /------------ 开发工具 ------------/ 【软件包管理器 + 代码编辑器】 【编译器 + 自动化构建器】 【版本控制器 + 调试器】 【实战:倒计时 + 进度条】 /------------ 系统导论 ------------/ 【冯诺依曼体系结构 + 操作系统基本概述】 /------------ 进程基础 ------------/ 【进程入门】 【进程状态】 【进程优先级】
hi ~,小伙伴们大家好啊!(✪ω✪) 今天我们终于迎来了关于进程基础内容的最后一节了,啊对没错咱们花了 4 节课铺垫的这些内容,其实都还只是进程的 “入门基础知识”∑(O_O;),关于进程的深度学习还远没结束哦~ 但不管怎样,先让我们把基础篇的最后一节稳稳拿下!💪🎯
----- 2025 年 11 月 24 日(十月初五)周一,小雪后的第二天 |
|---|
正式开讲前,鼠鼠先揭晓今天的核心知识点:【进程切换 + 进程调度】🚀! 这俩内容可是理解操作系统 “高效干活” 的关键,咱们用通俗的方式剧透核心: ฅ^ •ﻌ• ^ฅ
进程切换:进程调度:搞懂这两个知识点,就能彻底打通进程基础的 “任督二脉”,为后续进程通信、线程学习埋下关键伏笔~ 咱们赶紧进入正题,搞定这最后一节基础内容吧! (´• ω •)ノ(ᗒᗨᗕ)`
进程切换:(也称“进程上下文切换”)是指 CPU 从当前正在执行的进程,切换到另一个需要执行的进程的过程。
“多任务并发” 的核心机制 —— 即便在单核 CPU 上,通过频繁的进程切换,也能让多个进程 “看似同时运行”要理解进程切换,首先需要明确一个关键概念:进程上下文
“进程上下文” 是进程运行所需的全部信息集合,包括两类核心内容:
平时我们在集成开发环境(IDE)里写过死循环代码,当这个死循环运行起来时,系统确实会变得卡顿一些,但绝对不会完全卡死。
要理解为什么是这样的,需先明白这个问题:一个进程获得 CPU 使用权后,会一直把自己的代码执行完毕吗? 答案是否定的,除非代码非常简短,能在操作系统分配的一个 “时间片” 内执行完。 操作系统会为每个进程分配时间片,时间片就是 CPU 给进程的一段有限执行时长。
正因为每个进程都基于时间片来运行,所以不会出现一个进程长期独占 CPU 的情况。
有些小伙伴可能会疑惑:要是在单核机器上运行死循环,会不会卡死?
但事实上,系统确实会变卡,却不会彻底卡死。
不然我们怎么能用Ctrl + C这样的快捷键去结束掉死循环进程呢?这就说明,其实死循环进程并没有一直在独占 CPU资源。
寄存器:是CPU内部极小、极快的内存单元,用于临时存放当前正在执行的指令、数据以及指令的地址
“临时工作台”代码和数据这些寄存器各有其特定功能比如我们常见的:
下一条要执行的指令地址,确保 CPU 能按顺序执行代码进程的栈空间运算数据或地址代码段、数据段等内存区域CPU的运行状态,比如:运算是否溢出、是否产生进位、中断是否允许等CPU的工作模式和系统功能,比如:内存分页管理、保护模式切换等当一个进程运行时,CPU 的这些寄存器会被填充上与该进程相关的临时数据:
这些寄存器就像 CPU 的 “临时工作台”,实时保存着进程运行中的关键信息,确保指令能连续、正确地执行。
想象一个顶尖
厨师(CPU)在厨房(计算机)里做一道菜(执行一个程序)
一本烹饪书(内存),里面记录了做菜的所有步骤和原料清单(程序指令和数据)一张很小的操作台(寄存器)大型储物区(硬盘),存放着所有食材你是一名大学生,某天偶然看到学校附近在招募大学生士兵,政策是服役一年后可以返校继续完成学业。你平时身材高高壮壮,外形也还算周正,抱着试试看的心态报了名,没想到顺利通过了选拔。 这时候你要是头脑一热,直接一脚踹开宿舍门对舍友喊:“兄弟们拜拜了,我现在就去当兵,明年见!”,就这么不管不顾地走了, 那可就麻烦了。 等一年后退伍返校,大概率会收到学校的退学通知 —— 毕竟服役的这一年里,上课点名每次都没到,也没参加期末考试,一学年下来挂科累积了二十多门,学校按规定只能作退学处理。 所以,你正确的做法是:通过选拔后,第一时间找到辅导员申请 “保留学籍”,这一步才是重中之重。辅导员核实情况后,第二天会给你一个厚实的牛皮档案袋,里面装着你的成绩单、课程进度表、学籍信息表等所有和学习相关的材料,并且他会特意叮嘱你:“这个档案你自己收好,退伍回来时再交给我,我帮你恢复学籍。” 做好这些准备,你才能安心去服役。 一年后服役期满返校,你还不能直接走进教室坐下来听课,而是要先拿着档案袋找到辅导员办理学籍恢复手续。因为你入伍前读的是大二上学期,辅导员核对完材料后,会把你安排到该年级的某一个班级里,你要跟着他们继续完成大二后续的课程。
这个生活案例,其实和操作系统里的 “进程切换” 逻辑高度相似,我们可以一一对应来看:
资源协调与状态管理”(辅导员管理学籍,调度器管理进程) 通过这个类比就能清晰理解:
进程切换不是 “直接停、直接启”,而是先 “保存状态”,再 “让出资源”,最后 “恢复状态续跑”—— 就像学生入伍前要先保留学籍,回来后才能无缝衔接学业一样。

比如说现在进程 A 正在 CPU 上执行,已经运行到代码的第 100 行,它基于原始数据 4 和 6,计算得出了结果 10 此时,CPU 的各个寄存器里都保存着与进程 A 相关的各类数据:
这些 CPU 寄存器内的全部数据,我们称之为当前进程的硬件上下文数据
当进程 A 的时间片耗尽时,不能直接就让它退出 CPU。
接着操作系统会选择进程 B,把它调度到 CPU 上运行。
如果之后需要再次调度进程 A 执行,也不能直接把它放到 CPU 上。
由此可见:进程切换最核心的操作,就是对当前进程硬件上下文数据(即 CPU 内寄存器的内容)的保存与恢复

在早期的操作系统设计中,任务状态段(TSS,Task State Segment) 承担的核心角色,其实就是存储进程的硬件上下文 —— 也就是说,进程运行时 CPU 寄存器里的所有临时数据(如:程序计数器、通用寄存器、状态寄存器等),都会被保存到对应的 TSS 对象中。
task_struct)里,会导致task_struct的体积变得异常庞大task_struct作为描述进程的核心数据结构,每次创建新进程时都需要被分配和初始化,过大的体积会直接增加内存开销,同时拖慢进程创建的速度因此:为了优化性能、让进程创建更高效,设计就
这样既精简了task_struct的结构,又能让硬件上下文的管理更聚焦、更高效。
进程调度:操作系统通过特定策略,决定 “哪个进程能获得 CPU”、“能占用 CPU 多久”、“什么时候把 CPU 让给其他进程” 的管理过程。
为了让这些进程 “看起来同时运行”、避免 CPU 资源浪费,并且保证系统响应及时,操作系统就需要一套“合理分配CPU使用权”的机制 —— 这就是进程调度
它是操作系统实现 “多任务并发” 的关键,也是连接 “进程管理” 与 “CPU 硬件” 的核心桥梁。

在 Linux 系统的进程调度设计中,通常一个 CPU 核心对应一个独立的 “运行队列”(Runqueue),用于管理该核心上等待调度的进程。 而运行队列中会包含一个关键的数据结构 ——
queue[140],它是理解 Linux 优先级调度的核心。
一、queue[140] 是什么?
queue[140]的完整定义是 struct task_struct *queue[140],本质是一个 “结构体指针数组”:
queue[i])都是一个指针,指向一个struct task_struct(即进程控制块 PCB,描述进程的核心数据,如:PID、状态、优先级等)queue[140]中的每一项对应一个 “优先级链表头”—— 相同优先级的进程会被串联成一个链表,而queue[i]就指向该优先级链表的第一个进程 queue(即queue[140])的本质:是 Linux 运行队列(runqueue)中 按 “进程优先级” 分类的 “链表头数组”
它的核心作用是 “归类收纳”—— 把相同优先级的进程,串联成一个双向链表,而queue[i]就指向 “优先级为i的进程链表” 的表头。
二、为什么是 140 项?
queue[140]的长度之所以是 140,核心原因是 Linux 系统将进程优先级划分为 140 个等级(优先级数值越小,优先级越高)
这 140 个优先级又进一步分为两大类,对应不同的调度场景:
1. 实时优先级(0~99 级,共 100 个)
queue[0]~queue[99],分别对应 0~99 级实时优先级的进程链表2. 分时优先级(100~139 级,共 40 个)
queue[100]~queue[139],分别对应 100~139 级分时优先级的进程链表 简单说:queue[140]就是 Linux 运行队列中 “按优先级分类的进程收纳架”:
调度器调度时,会从优先级最高的非空队列开始,依次选取进程分配 CPU—— 这样既保证了实时进程的时效性,又兼顾了普通进程的公平性。
实时操作系统与分时操作系统的核心区别: 上面提到的“实时优先级”和“分时优先级”,本质对应了两种不同的操作系统设计理念 这里简单补充两者的核心差异,帮助理解优先级划分的逻辑:
维度 | 实时操作系统(RTOS) | 分时操作系统(Time-Sharing OS) |
|---|---|---|
核心目标 | 保证关键任务在规定时间内必须完成(时效性) | 让多个用户/进程公平共享 CPU 资源(公平性) |
调度特点 | 优先调度实时进程,可抢占非实时进程 | 按时间片轮转或优先级调度,兼顾公平与响应性 |
适用场景 | 工业控制、医疗设备、自动驾驶、航空航天 | 个人电脑(Windows/macOS)、服务器(Linux) |
失败后果 | 错过时间可能导致严重事故(如:设备失控) | 卡顿或延迟,但不会造成安全问题 |
Linux 系统是 “通用操作系统”,同时支持实时进程和普通分时进程 —— 通过
queue[140]的优先级划分,既能满足少量实时任务的需求,又能保证普通用户进程的公平与流畅,实现 “一机多用” 的灵活性。
一、遍历queue[140]找进程,时间复杂度太高? 如果只靠
queue[140],调度器每次选进程时,都要从最高优先级(0 级)开始,逐个检查queue[0]、queue[1]…… 直到找到第一个 “非空的链表”—— 这个过程本质是 “遍历数组”,最坏情况下要检查 140 次(比如:所有高优先级队列都空,只有queue[139]有进程),时间复杂度是
对于需要快速响应的调度器来说,
的效率虽然不算差,但仍有优化空间
疑问:能不能不用遍历,直接 “一眼找到” 有进程的最高优先级队列?
这就需要runqueue中的另一个关键成员:bitmap[5](优先级位图)
二、bitmap[5]如何让查找效率降到 O (1)?
bitmap[5]是专门为“快速定位非空队列”设计的“状态标记工具”1. bitmap[5]的基础属性:为什么是 5 个无符号整型?
bitmap是unsigned int(无符号整型)数组,每个unsigned int占 4 字节(32 个比特位)unsigned int共占 5 × 32 = 160个比特位queue[140](140 个优先级队列)的 “空/非空” 状态 ——140 个状态需要 140 个比特位,而 160 个比特位刚好能覆盖(多余的 20 个比特位闲置不用,不影响功能) unsigned int(仅 128 个比特位),128 <140,无法覆盖所有优先级 2. bitmap[5]的核心作用:比特位与queue[140]一一对应
bitmap的每个比特位,都和 queue[140]的下标(即:优先级)一一绑定,比特位的 “0/1” 状态代表对应队列的 “空/非空”:
queue[i]对应的链表有进程(非空),则bitmap中第i个比特位设为 1queue[i]对应的链表无进程(空),则bitmap中第i个比特位设为 0比如:
queue[62]有进程 → bitmap的第 62 个比特位是 1queue[100]没进程 → bitmap的第 100 个比特位是 0 3. 调度器如何用bitmap快速找进程?
有了bitmap,调度器找 “最高优先级非空队列” 时,再也不用遍历queue[140],而是通过 “位运算指令” 直接定位第一个为 1 的比特位:
bsf指令,即 “Bit Scan Forward”,从低位到高位扫描第一个为 1 的比特位)i,再直接通过queue[i]拿到该优先级的进程链表三、总结:queue与bitmap的配合逻辑
queue[140]是 “按优先级分类的收纳架”,负责归类进程
bitmap[5]是 “收纳架的状态指示灯”,负责标记哪个收纳架有进程
两者配合,让调度器实现了 “高效归类 + 快速查找”:
i,直接挂到queue[i]的链表,并将bitmap第i位设为 1bitmap,找到第一个为 1 的比特位,直接定位到最高优先级的queue[i],取链表头的进程执行 通过bitmap,将查找最高优先级队列的时间复杂度从
降为
,保证调度器的快速响应。

在 Linux 运行队列(runqueue)的设计中:
queue[140](优先级链表数组)和bitmap[5](优先级位图)nr_active 它的核心作用是 统计当前运行队列中处于 “就绪态” 的进程总数(即:所有queue[i]链表中,可被调度执行的进程总和)
这个变量看似简单,却能在调度器工作的第一步起到 “快速预检” 的作用,大幅减少不必要的操作。
当调度器需要挑选进程分配 CPU 时,并不会直接去扫描bitmap或queue数组,而是先检查nr_active的值:
idle)bitmap查找 “最高优先级的非空队列”—— 也就是我们之前说的,用位运算定位bitmap中第一个为 1 的比特位,进而找到对应的queue[i]链表,最终从链表中选取进程执行 简单来说:nr_active就像运行队列的 “总开关指示灯”:
nr_active是否大于 0),亮了再去细找具体的 “待执行进程”(通过bitmap和queue)这时候可能有小伙伴会产生疑问:
为了解决 “低优先级进程饥饿” 的问题,Linux 的运行队列(runqueue)设计了一个关键优化 ——引入 “双队列机制”
queue[140],而是有两份完全相同的 “优先级调度单元”nr_active(就绪进程计数)、bitmap[5](优先级位图)和queue[140](优先级链表数组)这三个核心成员具体实现上:
rqueue_elem(可理解为 “优先级队列单元”),结构体内部就封装了这三个成员rqueue_elem类型的数组prio_array[2]—— 这就相当于创建了两个独立的 “调度队列池”同时,runqueue 里还会定义两个指针,用来管理这两个队列池:
struct rqueue_elem *active:指向当前 “正在调度的队列池”(简称 active 队列),调度器只会从这个队列池里挑选进程struct rqueue_elem *expired:指向 “已调度过、暂时待激活的队列池”(简称 expired 队列),用来存放 “时间片耗尽后重新入队的进程”初始状态下:
active = &prio_array[0](active 队列指向第一个队列池,里面存放初始就绪进程)expired = &prio_array[1](expired 队列指向第二个队列池,初始为空)双队列如何解决 “低优先级进程饥饿”? 回到之前 60 号和 99 号进程的例子,双队列机制会这样工作:
active->nr_active == 0,active 队列空了),调度器会执行一次简单的 “指针交换”active指向原本的 expired 队列(此时 expired 队列里已经存满了之前转移过来的进程),让expired指向原本空掉的 active 队列这样一来:
通过这种 “双队列交替激活” 的机制,既能保证高优先级进程的优先执行,又能避免低优先级进程永远得不到调度的 “饥饿问题”,实现了调度的公平性。
在 Linux 的运行队列(runqueue,常简称为 rq)中:
nr_running、cpu_load、nr_switches是三个核心的状态统计变量,分别用于记录 CPU 调度的关键信息,帮助操作系统感知负载、优化调度策略
nr_running:当前 CPU 的 “就绪进程数”
nr_running 就是 3cpu_load:当前 CPU 的 “负载统计”
nr_running 的 “瞬时值”)cpu_load 远高于 B CPU,调度器会把 A CPU 上的部分进程迁移到 B CPU,避免单 CPU 过载top 或 uptime 命令看到的 “CPU 负载值”(如:load average: 1.2, 0.8, 0.6),核心数据就来自 cpu_loadnr_switches:当前 CPU 的 “进程切换次数”
nr_switches 过高:说明 CPU 大部分时间在 “保存/恢复 进程上下文”,而非执行进程任务,会导致系统效率下降(常见于进程数量过多的场景)nr_switches 过低:可能说明 CPU 长期被单个进程占用(如:死循环高优先级进程),需检查调度策略是否合理nr_switches 就会 + 2一句话总结三者关系:
nr_running:看 “当下有多少进程等 CPU”(瞬时)cpu_load:看 “最近 CPU 平均有多忙”(趋势)nr_switches:看 “CPU 在进程间切换了多少次”(开销)三者共同构成了 CPU 调度的 “状态仪表盘”,帮助操作系统平衡效率与公平性。