操作系统
操作系统中最核心的概念是进程
进程管理是操作系统的核心模块,负责管理程序的动态执行过程。涵盖进程的基本概念与生命周期、进程控制原语、CPU 调度算法、并发执行中的同步与互斥问题(信号量、经典同步问题)、进程间通信机制以及死锁的处理策略(预防、避免、检测与解除)。
进程核心概念
进程概念
为什么要引入进程
- 多道程序设计系统中,CPU 在各程序之间高速切换
- 需要一种抽象实体来描述正在运行的程序,以便 OS 进行管理和调度
- 顺序执行:I1 → C1 → P1 → I2 → C2 → P2 → …(无并发,资源利用率低)
- 引入进程后,多道程序可并发执行,提高 CPU 和 I/O 利用率
进程的定义
- 对正在运行程序的一个抽象
- 进程是系统进行资源分配和调度的基本单位
- 进程是程序在数据集合上的一次运行活动
PCB(进程控制块)
- 进程在操作系统中的表示,OS 通过 PCB 管理进程
- 包含以下信息:
- 进程标识符(PID):唯一标识一个进程
- 状态:运行态、就绪态、阻塞态等
- 优先级:调度依据
- 寄存器上下文:程序计数器 PC、栈指针 SP、通用寄存器等
- 内存指针:代码段、数据段、栈段指针
- 资源清单:打开的文件、I/O 设备等
- 父进程/子进程指针:维护进程家族关系
- 其他:记账信息、消息队列指针等
进程存储器映像
- 用户空间:
- 代码段(Text):程序代码,只读
- 数据段(Data):全局变量、静态变量
- 栈段(Stack):函数调用参数、返回地址、局部变量
- 系统空间:
进程状态(5 状态模型)
- 创建态(New):进程正在被创建,OS 分配 PCB
- 就绪态(Ready):具备运行条件,等待 CPU 调度
- 运行态(Running):正在使用 CPU 执行指令
- 阻塞态(Blocked/Waiting):进程等待某个事件发生(如 I/O 完成)
- 终止态(Terminated):进程执行完毕,OS 回收资源
状态转换条件
| 转换 | 条件 |
|---|
| 创建 → 就绪 | PCB 初始化完成,进入就绪队列 |
| 就绪 → 运行 | 调度程序选中该进程 |
| 运行 → 就绪 | 时间片用完 / 被高优先级进程抢占 |
| 运行 → 阻塞 | 等待某事件(如 I/O、信号量) |
| 阻塞 → 就绪 | 所等待的事件发生 |
| 运行 → 终止 | 进程执行完毕或被撤销 |
进程组织方式
- 链接方式:按进程状态(就绪、阻塞等)将 PCB 链接成不同队列
- 索引方式:建立就绪索引表、阻塞索引表,通过索引查找 PCB
- 树形结构:UNIX/Linux 用树形结构组织进程,
pstree 可见父子关系
当前进程指针
- 系统中有一个指针指向当前正在运行的进程的 PCB
- 调度切换时,该指针指向新选中的进程 PCB
指向原始笔记的链接
进程控制
进程控制
定义
- 使用原语来创建、撤消进程,以及完成进程各状态之间的转换
- 由操作系统内核实现
原语(Primitive)
- 由若干条机器指令构成的可完成特定功能的程序段
- 原子操作:要么全部完成,要么全都不做
- 通过屏蔽各种中断保证原子性
分类
基本原语
创建原语
- 创建新进程,分配 PCB
- 步骤:
- 分配新的 PCB,获取唯一 PID
- 为进程分配内存空间,加载程序映像(代码 + 数据 + 栈)
- 初始化 PCB:填写 PID、状态(就绪)、优先级、寄存器上下文等
- 将 PCB 插入就绪队列
撤销原语
- 终止进程,回收其所占用的所有资源(内存、文件、设备等)
- 回收 PCB
阻塞原语
- 进程从运行态 → 阻塞态
- 保护当前 CPU 上下文到 PCB,将 PCB 移入等待队列
唤醒原语
- 进程从阻塞态 → 就绪态
- 将 PCB 从等待队列移到就绪队列
挂起原语
激活原语
fork() 系统调用
- UNIX/Linux 中创建进程的核心系统调用
- 返回值语义:
fork() 在父进程中返回子进程 PID,在子进程中返回 0,失败返回 -1
- 父子进程并发执行:fork() 后父进程和子进程是独立的进程,调度顺序不确定
- 写时复制(Copy-on-Write):父子进程共享同一物理内存,仅当写入时复制新页
进程树结构
- UNIX/Linux 中进程形成树形关系:
init(PID=1)是所有进程的祖先
fork() 创建子进程后,可通过 exec() 系列函数替换进程映像
模式切换 vs 进程切换
- 模式切换(Mode Switch):在同一进程中,用户态 ↔ 内核态(如系统调用、中断),不改变当前进程
- 进程切换(Process Switch/Context Switch):从一个进程切换到另一个,需保存/恢复 PCB 上下文,开销更大
指向原始笔记的链接
进程调度
进程调度
定义
- 按某种策略从就绪队列中选择一个进程,分配 CPU 给它运行
- 调度的核心目标:公平、高效、低延迟、高吞吐
调度层次
- 高级调度(作业调度):从外存中选择作业调入内存,创建进程
- 中级调度(交换调度):进程在内/外存之间换入换出,缓解内存压力
- 低级调度(进程调度):从就绪队列选进程分配 CPU — 频率最高
调度时机
- 进程从运行态 → 阻塞态(等待 I/O 或事件)
- 进程从运行态 → 就绪态(时间片用完或被抢占)
- 进程从阻塞态 → 就绪态(事件发生,可能抢占)
- 进程终止
调度方式
- 非抢占式:进程运行完毕或主动阻塞才切换,简单但响应慢
- 抢占式:时间片用完或高优先级进程就绪时可强制切换,响应快但开销大
重点 调度算法
| 算法 | 描述 | 特点 |
|---|
| 先来先服务(FCFS) | 按到达顺序排队,类似银行排队叫号 | 简单,平均等待时间长,长作业有利 |
| 短作业优先(SJF/SPF) | 选择执行时间最短的进程,非抢占/抢占两种 | 平均等待时间最短,长作业可能饥饿 |
| 优先级调度 | 分静态优先级(创建时确定)和动态优先级(随运行调整) | 通用,优先级低的可能饥饿 |
| 时间片轮转(RR) | 按时间片轮流分配 CPU,用完即切 | 分时系统,响应时间均匀,时间片大小关键 |
| 多级反馈队列 | 多个队列,优先级+时间片结合 | 通用(重点),兼顾响应与吞吐 |
先来先服务(FCFS)
- 就绪队列按到达先后排序,非抢占
- 平均等待时间长,短作业排在长作业后时非常不利
- 类比:银行柜台排队,不可插队
短作业优先(SJF/SPF)
- 非抢占:进程到达时,选择预计运行时间最短的投入运行
- 抢占(SRTF):新进程到达时,如果其剩余时间比当前进程短,则抢占
- 平均等待时间理论上最优(最优调度策略),但长作业可能无限期饥饿
- 难点:需预知进程运行时间
优先级调度
- 静态优先级:进程创建时确定,运行期间不变
- 动态优先级:随进程运行时间调整(如老化机制),防止饥饿
时间片轮转(RR)
- 每个进程分配一个固定时间片(Time Quantum),用完则切换
- 时间片过大 → 退化为 FCFS;时间片过小 → 上下文切换开销过大
- 典型值:10–100ms
多级反馈队列调度
- 设置多个就绪队列,优先级从高到低,时间片从短到长
- 新进程进入最高优先级队列
- 用完时间片未完成则降入下一级队列
- 优先级高的队列可抢占优先级低的队列中的进程
- 兼顾短作业(快速响应)和长作业(不饥饿)
Linux CFS(Completely Fair Scheduler)
- 使用红黑树(Red-Black Tree)组织就绪进程
- vruntime:进程的虚拟运行时间,CFS 选择 vruntime 最小的进程运行
- vruntime 受进程优先级(nice 值)影响:优先级高 → vruntime 增长慢
- 保证每个进程获得公平的 CPU 份额
Linux 实时调度
- SCHED_FIFO:先进先出实时调度,直到进程主动阻塞或结束
- SCHED_RR:时间片轮转实时调度,同优先级间轮转
- SCHED_DEADLINE:基于最早截止期限优先(EDF),保证实时任务按时完成
指向原始笔记的链接
进程并发
进程并发
并发需求
- 两个层次的并发:
- 应用层并发:应用程序利用 OS 支持的进程/线程,安排可并行事务并发执行
- 操作系统内核层并发:OS 核心程序本身也要尽可能地并发(如设备驱动、文件系统并发访问)
并发编程方法
- 自动并行检测:编译器自动识别可并行执行的部分
- 并发编程语言:语言本身提供并行语法(如并发语句
cobegin/coend)
- 传统语言 + OS API:使用 OS 提供的系统调用(如 fork、pthread_create)实现并发
并发表示法 — parbegin/parend
S1;
parbegin
S2;
S3;
parend
S4;
- 表示 S2 和 S3 可以并发执行,S4 必须在两者都完成后才开始
并发执行特征
- 间断性:执行—暂停—执行,运行过程不可预测
- 失去封闭性:执行结果受并发环境和相对速度的影响
- 不可再现性:多次执行可能得到不同结果(Race Condition)
并发产生的问题
- 同步(Synchronization)— 直接约束:多个进程按一定顺序执行,存在协作关系
- 例如:A 产生数据,B 消费数据,B 必须等待 A 完成
- 互斥(Mutual Exclusion)— 间接约束:多个进程不能同时访问临界资源
任务图示例(S1 / S2 / S3)
S1 → 并发 S2、S3 → S4
- S1 先执行
- S2 和 S3 可并发执行
- S4 必须在 S2 和 S3 都完成后执行
- 同步约束:S1 与 S2/S3 之间为同步关系;S2/S3 无约束,可任意并发
指向原始笔记的链接
同步与互斥
进程同步互斥
临界区
重点 进入临界区的四个准则
- 互斥使用(Mutual Exclusion):不能同时有两个进程在临界区内执行
- 让权等待:等待进入临界区的进程,应释放处理机后阻塞等待
- 有空让进:在临界区外运行的进程不可阻止其他进程进入临界区
- 有限等待:不应使要进入临界区的进程无限期等待在临界区之外
软件实现方法
硬件实现方法
关中断
- 进入临界区前关中断,退出后开中断
- 缺点:不适用于多处理器系统(关中断只对当前 CPU 有效),且用户进程关中断风险高
Test-and-Set(TS)指令
- 硬件原子指令:读旧值 → 写新值(设为 1),两个操作不可分割
- 伪代码描述:
boolean TestAndSet(boolean *lock) {
boolean old = *lock;
*lock = true; // 锁住
return old; // 返回原值
}
// 自旋锁的典型实现
while (TestAndSet(&lock) == 1) // 若锁被占用,忙等循环
;
// 进入临界区
lock = 0; // 释放锁
- 重点 忙等(Busy Waiting):进程在等待锁时不断循环测试,CPU 空转,浪费处理机时间
- 违背 ” 让权等待 ” 准则
- 优点:无需上下文切换,在临界区执行时间很短时效率高
Swap 指令
void Swap(bool *a, bool *b) {
bool temp = *a;
*a = *b;
*b = temp;
}
// 用 Swap 实现自旋锁
bool key = true;
while (key == true)
Swap(&lock, &key);
// 进入临界区
lock = false;
信号量机制
指向原始笔记的链接
信号量
信号量
定义
- 由 Dijkstra 提出的经典同步机制
- 信号量 S 是一个整型变量,代表可用资源的数量
- 除了初始化,信号量只能通过 P 操作 和 V 操作 修改(原子操作)
重点 P/V 操作
P 操作(wait / 申请资源)
P(S):
S.value = S.value - 1
if S.value < 0:
进程进入等待队列(阻塞)
V 操作(signal / 释放资源)
V(S):
S.value = S.value + 1
if S.value <= 0:
从等待队列唤醒一个进程
分类
按资源计数
- 二进制信号量(Binary Semaphore):S 取值 0 或 1,相当于互斥锁
- 计数信号量(Counting Semaphore):S≥0,表示可用资源数量
按使用目的
- 互斥信号量(公共信号量):用于保护临界区,初值 = 1
- 同步信号量(私有信号量):用于进程间同步,初值 = 0
实现机制
- 基于阻塞队列(非忙等):当 P 操作无法获取资源时,进程主动阻塞并让出 CPU
- 不使用忙等(while 循环),避免 CPU 浪费
- 每个信号量关联一个等待队列(blocked queue)
临界区(Critical Section)四个原则
- 忙则等待(互斥):临界区有进程时,其他进程不能进入
- 空闲则入(前进):无进程在临界区时,应允许申请者进入
- 有限等待:等待进入临界区的进程不会无限等待
- 让权等待:不能进入临界区时应主动阻塞,而非忙等
P/V 使用模式
指向原始笔记的链接
经典同步问题
经典同步问题
进程同步互斥
重点 生产者 - 消费者问题
问题描述
- 生产者:生产数据放入缓冲区
- 消费者:从缓冲区取数据消费
- 同步关系:缓冲区满则生产者等待,缓冲区空则消费者等待
- 互斥关系:对缓冲区的互斥访问
PV 解决方案(3 个信号量)
semaphore mutex = 1; // 保护缓冲区互斥访问
semaphore empty = N; // 空缓冲区数量
semaphore full = 0; // 满缓冲区数量
// 生产者
while (true) {
produce(data);
P(empty); // 先申请空缓冲区(资源信号量)
P(mutex); // 再申请互斥访问
put(data);
V(mutex);
V(full); // 满缓冲区 +1
}
// 消费者
while (true) {
P(full);
P(mutex);
get(data);
V(mutex);
V(empty);
consume(data);
}
P 操作顺序
- 必须先 P(资源信号量),再 P(互斥信号量)
- 如果顺序颠倒(先 P(mutex) 再 P(empty)),满缓冲区时生产者持有 mutex 阻塞,消费者无法进入,形成死锁
重点 读者 - 写者问题
读者优先
- 多个读者可同时读
- 写者必须独占访问,且只能等所有读者完成
- 使用
readcount 统计读者数,mutex 保护 readcount,wrt 实现写者互斥
semaphore mutex = 1;
semaphore wrt = 1;
int readcount = 0;
// 读者
P(mutex);
readcount++;
if (readcount == 1) P(wrt); // 第一个读者锁住写者
V(mutex);
read;
P(mutex);
readcount--;
if (readcount == 0) V(wrt); // 最后一个读者释放写者
V(mutex);
// 写者
P(wrt);
write;
V(wrt);
写者优先
- 当有写者等待时,新读者不能开始读
- 需额外的信号量
read 来阻塞新读者
重点 哲学家就餐问题
问题描述
- 5 个哲学家围坐,5 支筷子交替放置
- 每个哲学家需要两支筷子才能进餐
- 死锁风险:所有哲学家同时拿起左边筷子,导致循环等待
解决方案
- 限制并发人数:最多只允许 4 个哲学家同时就餐(信号量
limit = 4)
- 奇偶拿筷:奇数号先取左边筷子,偶数号先取右边筷子,打破循环等待
- AND 信号量:同时申请两支筷子(一次 P 操作获取全部所需资源)
重点 管程(Monitor)
- 将共享数据和对该数据的操作封装在一起
- 类似 OOP 中的类:共享变量 + 方法 + 条件变量
- 同一时间只有一个进程能进入管程,自动保证互斥
- 使用条件变量(Condition Variable)实现同步
信号量解决思路总结
- 互斥信号量 mutex 保护临界区
- 资源信号量 empty/full 表示缓冲区状态
- 同步信号量协调执行顺序
指向原始笔记的链接
进程通信
进程通信IPC
定义
- 在进程间传输数据(交换信息)
- 进程间通信(Inter-Process Communication,IPC)
分类
低级通信
- 只能传递状态和整数值(控制信息)
- P、V 操作
- 缺点:信息量小,效率低,编程复杂
重点 高级通信(三种方式)
| 方式 | 描述 | 特点 |
|---|
| 共享内存模式 | 多个进程共享一段内存区域 | 最快,需信号量同步互斥 |
| 消息传递模式 | 进程间通过消息发送/接收通信 | 无共享内存,适用于分布式系统 |
| 共享文件模式(管道) | 通过文件作为中介传递数据 | 简单,但效率较低 |
共享内存(Shared Memory)
- 最快速的 IPC 方式:进程直接读写同一块物理内存
- 无需数据拷贝(对比消息传递需要内核缓冲拷贝)
- 需配合 信号量 实现同步互斥
- 适用场景:大量数据传输
消息传递(Message Passing)
直接通信
- 指名道姓:发送方明确指定接收方 PID,接收方明确指定发送方
- 系统调用:
send(dest, msg) / recv(src, msg)
间接通信
- 通过消息队列/信箱(Mailbox)作为中介
- 多个进程可共享同一个信箱
- 系统调用:
send(M, msg) / recv(M, msg)
管道(Pipe)
- 单向数据流:数据从一端写入,从另一端读出
- 无名管道:只用于父子进程或有亲缘关系的进程之间
- 命名管道(FIFO):通过文件系统中的特殊文件,可用于任意进程间通信
- 管道本质是内核中的一个共享缓冲区
三种方式对比
| 方式 | 速度 | 同步需求 | 适用场景 |
|---|
| 共享内存 | 最快 | 需同步 | 大量数据、高性能 |
| 消息传递 | 中等 | 自动同步 | 分布式、小数据 |
| 管道 | 较慢 | 自动同步 | 简单通信、父子进程 |
指向原始笔记的链接
死锁
死锁
定义
- 两个或多个进程相互等待对方释放资源,导致所有相关进程都无法继续执行
- 若无外力作用,死锁进程将永远处于阻塞状态
死锁示例
- 交通死锁:四个方向的车互不相让,形成环路
- 哲学家就餐问题:5 个哲学家同时拿起左边筷子,循环等待右边筷子
重点 死锁产生的四个必要条件
- 互斥(Mutual Exclusion):资源一次只能被一个进程使用
- 请求与保持(Hold and Wait):进程已持有一个资源,又请求新的资源
- 不可剥夺(No Preemption):资源在未使用完前不能被强行夺走
- 循环等待(Circular Wait):存在进程 - 资源的循环等待链
重点 死锁处理策略对比
| 策略 | 方法 | 特点 |
|---|
| 死锁预防 | 破坏四个必要条件之一 | 实现简单,资源利用率低 |
| 死锁避免 | 银行家算法,动态检查安全性 | 资源利用率较高,需预知最大需求 |
| 死锁检测与恢复 | 检测到死锁后强行恢复 | 适用于容易回滚的系统 |
死锁预防(破坏必要条件)
- 破坏互斥:非共享设备不能破坏(如打印机不能共享)
- 破坏请求与保持:一次性申请所有资源(静态分配),资源利用率低
- 破坏不可剥夺:资源可被系统抢占,但可能导致进程回退
- 破坏循环等待:资源有序分配(给资源编号,必须按编号顺序申请)
重点 银行家算法(死锁避免)
基本思想
- 系统维护各进程所需资源的最大量,每次分配前检查是否处于安全状态
- 安全状态:存在安全序列使得所有进程最终能完成
单资源示例
- 10 个可用资源,3 个进程 P、Q、R,分别最多需要 8、4、9
- 当前已分配:P=5, Q=2, R=2(Available=1)
- 安全序列:Q → P → R(Q 还需 2,可得 1 + 后续回收)
数据结构(5 进程 3 资源类型)
| 矩阵 | 描述 |
|---|
| Available | 每类资源当前可用数量 |
| Max | 每个进程最多需要各类资源的数量 |
| Allocation | 每个进程当前已分配的资源数量 |
| Need | 每个进程还需要资源数量(Need = Max - Allocation) |
安全性检查算法
Work = Available,Finish[i] = false
- 找满足
Finish[i] = false 且 Need[i] ≤ Work 的进程 i
Work = Work + Allocation[i],Finish[i] = true,重复步骤 2
- 如果所有
Finish[i] = true,则系统安全
示例:T0 时刻 5 进程 3 资源
假设资源 A=10, B=5, C=7,当前系统处于安全状态,存在安全序列 P1→P3→P4→P2→P0 等
资源分配图(RAG)
- 圆形节点 = 进程,矩形节点 = 资源类
- 资源 → 进程(分配边),进程 → 资源(请求边)
- 死锁检测:化简 RAG,如果存在不可化简的进程,则系统存在死锁
- 连续化简所有不阻塞的进程,最终仍阻塞的进程是死锁进程
死锁检测与恢复
检测
- 类似安全性检查算法,检测当前是否存在死锁进程
- 周期性检测或资源请求失败时触发
恢复方法
| 方法 | 描述 | 缺点 |
|---|
| 重启系统 | 强制重启所有进程 | 简单粗暴,所有工作丢失 |
| 终止死锁进程 | 终止部分或全部死锁进程 | 可能丢失数据 |
| 剥夺资源 | 从死锁进程处剥夺资源给其他进程 | 可能导致进程回退 |
| 进程回滚 | 将死锁进程回滚到某个安全检查点 | 需系统支持 checkpoint |
指向原始笔记的链接
线程
线程
定义
- 线程(Thread)是 CPU 调度的基本单位,轻量级进程(Lightweight Process)
- 一个进程可以包含多个线程
- 同一进程的线程共享进程的地址空间和资源
- 每个线程拥有独立的:线程 ID、程序计数器(PC)、寄存器集合、栈
TCB(线程控制块)
- 包含:线程 ID、程序计数器、寄存器集合、栈指针
- 相比 PCB,内容更少,切换更快
引入原因
- 进程切换开销大(需切换地址空间、TLB 刷新)
- 进程间通信复杂(需 IPC 机制)
- 需要更轻量级的并发执行单元以支持高并发
进程 vs 线程
| 方面 | 进程 | 线程 |
|---|
| 资源拥有 | 独立的地址空间和资源 | 共享所属进程的资源 |
| 调度单位 | 资源分配单位 | CPU 调度单位 |
| 切换开销 | 大(需切换地址空间、刷新 TLB) | 小(仅保存/恢复寄存器+栈) |
| 通信方式 | 需 IPC 机制(管道、消息队列、共享内存等) | 直接读写共享内存/全局变量 |
| 拥有者 | 操作系统管理 | 用户库 或 操作系统管理 |
| 独立性 | 进程间相互独立,一个崩溃不影响其他 | 线程间共享,一个线程崩溃可能导致整个进程崩溃 |
线程优势
- 低开销:创建和撤消线程的开销远小于进程
- 快速切换:同一进程内的线程切换只需保存/恢复少量寄存器
- 高效通信:线程间通过共享内存直接通信,无需内核介入
- 高并发:一个进程可轻松创建成百上千个线程,适合高并发服务(如 Web 服务器)
线程分类
用户级线程(User-Level Thread, ULT)
- 由用户空间的线程库管理(如 POSIX Pthreads 用户态实现)
- 内核完全不知晓线程的存在,内核调度的是进程
- 线程切换无需陷入内核,切换速度极快
- 缺点:一个线程阻塞(如 I/O)会导致整个进程所有线程阻塞
内核级线程(Kernel-Level Thread, KLT)
- 由操作系统内核直接管理
- 内核知道每个线程的存在,调度以线程为单位
- 线程阻塞不影响同一进程中的其他线程
- 缺点:线程切换需要陷入内核,开销比用户级大
混合模式(Combined)
- 用户级线程复用到内核级线程上
- 应用可创建大量用户级线程,映射到较少的内核级线程
- 兼顾用户级的快速切换和内核级的阻塞不阻塞
线程切换开销分析
- 同一进程内线程切换:仅需切换寄存器和栈,无需切换地址空间(TLB 有效),开销小
- 不同进程间线程切换:需切换地址空间(TLB 刷新),开销大
指向原始笔记的链接
重点 进程调度算法对比
参考 重点 调度算法 中的 FCFS、SJF、RR、多级反馈队列对比
重点 死锁处理策略对比
参考 重点 死锁处理策略对比