引入进程的原因

1. 引入进程的原因
   - 为了提高资源利用率必须让多个程序并发运行。  
   -  程序的并发运行出现了新的特征：间断性、 失去封闭性、 不可再现性。  
   -  为了让程序能正确并发运行，引入进程概念。

操作系统的代码是所有进程的公共代码

运行模式和进程切换

![](https://chenalna.oss-cn-hangzhou.aliyuncs.com/img/d2818a53733fb719994bb7c40f2b4cd2201f2ffe88c5b84c92b960d355df54c3.jpg" style="zoom:50%;")  
进程1

<img src="https://chenalna.oss-cn-hangzhou.aliyuncs.com/img/41c3feaf39deb0a28146bdebdf2ee87a65fae697302b68dd62e3cc06b710b300.jpg" style="zoom:50%;" />

如何切换进程？

要从用户模式切换到内核模式

访问了内核区域，触发一般保护故障

进程：一个正在运行程序的抽象，程序的一次执行。

程序：指令的有序集合。

·Linux:创建进程使用系统调用fork()+execve()  
·Windows:CreateProcess()   
·父进程 shell   
·子进程 hello

在./hello->shell 中 fork()+execute()

父进程和子进程具有独立的但是相同的虚拟地址空间

Init——最初的父进程

树形的关系

进程运行时有独立的虚拟地址空间

![](https://chenalna.oss-cn-hangzhou.aliyuncs.com/img/11df7ec02fddceb002b766c44e5344f70bc39ff07fe885f860e2a836bebc5b74.jpg)

### 进程的创建

1. 系统初始化（操作系统启动过程中）  
2. 正在运行的程序执行了创建进程的系统调用(fork)  
3. 用户请求创建一个新进程  
4. 一个批处理作业的初始化

程序是静态的

一个程序对应一个进程

进程是具有一定独立功能的程序关于讴歌数据集合的一次运行过程，是系统进行资源分配和调度的一个独立单位。

**进程的特征**

结构特征

动态性

并发性

独立性

异步性

结构性

程序的并发执行：多个程序共享资源，并发运行

1. 间断性。并发程序并不是一气呵成的，中间总会因此彼此间的各种制约关系出现暂停，因为系统只有一个 CPU  
2.  失去封闭性而导致程序运行结果不可再现性，即对没有对资源的互斥共享  
3. 静态程序结构不能支持并发运行的实现

**进程控制**

分配内存资源、回收内存资源、控制状态转换

**进程互斥**

互斥方式：多个进程载访问某些共享资源（临界资源）应采用互斥的方式访问

同步方式：多个进程相互合作完成一些共同任务，前驱满足后继

进程通信近九成之间的信息交换

**调度**

对资源或人物进行合理分配和管理

能够后背队列中按照一定的算法选择若干个作业调入内存，为他们创建进程，分配必要资源，插入就绪队列.

4. **进程与程序的区别**

(1)从定义上看，程序是一组指令的有序集合；进程是程序的运行过程；  
(2)从结构上看，进程不仅包含程序段，还包含数据段和 PCB；  
(3)进程是动态性，而程序是静态的；

(4)进程可独立地、并发地执行，程序则不能独立、并发执行

5. **进程与程序的对应关系**

‐ 在某个时刻一个进程对应于一个程序；  
‐ 在整个生命周期中，进程可执行多个程序；( fork+exec )  
‐ 一个程序多次执行则将对应多个进程；

### 进程控制块/进程表

PCB（struct）

进程描述信息

进程控制和管理信息

资源分配清单

处理机相关信息

进程控制块/进程表

PCB struct{}

<table><tr><td>进程管理<br \>
寄存器 <br \>
程序计数器<br \>
程序状态字<br \>
堆栈指针<br \>
进程状态<br \>
优先级<br \>
调度参数<br \>
进程ID<br \>
父进程<br \>
进程组<br \>
信号<br \>
进程开始时间<br \>
使用的CPU时间<br \>
子进程的CPU时间<br \>
下次报警时间</td><td>存储管理<br \>
正文段指针<br \>
数据段指针<br \>
堆栈段指针</td><td>文件管理<br \>
根目录<br \>
工作目录<br \>
文件描述符<br \>
用户ID<br \>
组ID</td></tr></table>



进程映像是指进程实体的组成。

程序（段）正文段——代码段CS

数据集数据段DS

（有的包含栈堆栈段SP）

PCB进程描述信息

进程是动态的，进程是进程实体（进程映像）的组成

![](https://chenalna.oss-cn-hangzhou.aliyuncs.com/img/3f277a24134a1fff284952aea3bf5e3b9b9e2e23e516558143d83af7346a6e9b.jpg)

进程状态

进程被创建后，进入就绪队列，等待被调度执行

8. **内核态与用户态**

CPU 指令（特权指令，非特权指令）

特权指令：关机指令、清主存、启动外设指令、设置系统时钟时间、关中断、修改存储器管理寄存器等  
非特权指令：通用寄存器清0 指令，访问内存指令，算术运算指令等

**CPU 的执行状态**

内核态（核心态、系统态、管态）：能访问所有的内存空间和 I/O 端口，能执行特权和非特权指令。

用户态（目态）只能访问分配给自己的内存空间，只能执行非特权指令。

进程的三（五）种状态：

运行

就绪

阻塞

创建状态和终止状态

**进程状态**

进程被创建后，进入就绪队列

等待被调度执行



![](https://chenalna.oss-cn-hangzhou.aliyuncs.com/img/651bc6b37b1a89309442cffe1a9dfd057d2304036fc3f7582a89eade6c7f69fa.jpg)

![](https://chenalna.oss-cn-hangzhou.aliyuncs.com/img/8f8f6b1a0546fe3bcc56c8a2213aec037a5222b383b5575aee22751401616032.jpg)

**7．单CPU中N个进程的情况**

![](https://chenalna.oss-cn-hangzhou.aliyuncs.com/img/1b78a15a1499de4d104e751077664a772cc7e9547cf7e88e31e42211ae37a77b.jpg)



![](https://chenalna.oss-cn-hangzhou.aliyuncs.com/img/fabd57b4939f01b6eaf4906027767a9f33e5719a444ac61072ca412e94a54755.jpg)

跟踪进程状态

<table><tr><td>时间</td><td>Process0</td><td>Process1</td><td>注</td></tr><tr><td>1</td><td>运行</td><td>就绪</td><td></td></tr><tr><td>2</td><td>运行</td><td>就绪</td><td></td></tr><tr><td>3</td><td>运行</td><td>就绪</td><td>Process0发起I/O</td></tr><tr><td>4</td><td>阻塞</td><td>运行</td><td>Process0被阻塞</td></tr><tr><td>5</td><td>阻塞</td><td>运行</td><td>所以Process1运行</td></tr><tr><td>6</td><td>阻塞</td><td>运行</td><td></td></tr><tr><td>7</td><td>就绪</td><td>运行</td><td>I/O完成</td></tr><tr><td>8</td><td>就绪</td><td>运行</td><td>Process1现在完成</td></tr><tr><td>9</td><td>运行</td><td>-</td><td></td></tr><tr><td>10</td><td>运行</td><td>-</td><td>Process0现在完成</td></tr></table>

## 进程终止

正常退出（自愿）

出错退出(自愿)

例如：打开文件失

严重错误(非资源)  

除数为零，引用不存在的内存等  

被其它进程杀死(非自愿)

执行系统调用kiII杀死其他进程



单 CPU，所以一次只能有一个进程运行（交给 CPU）

但是就绪队列还是可以有多个进程

周转时间：任务完成时间减去任务达到系统时间

T周转=T完成-T到达

T周转=T等待+T执行

![](https://chenalna.oss-cn-hangzhou.aliyuncs.com/img/659cfe3993816a0e3f9ec5c5ab4f117e78ce566b3e0299509a3fb97dcac3e3fe.jpg)

Eg：假设PCB中的变量state表示进程当前所处状态，1表示就绪态，2表示阻塞态..

![](https://chenalna.oss-cn-hangzhou.aliyuncs.com/img/2a999be6a5cf15a590b2e419e7e34bd5c45f885ae6f9e939a3eba31d489a76c9.jpg)

假设此时进程2等待的事件发生，则操作系统中，负责进程控制的内核程序至少需要做这样两件事：  
①将PCB2的state设为1   。完成了第一步后收到中断信号，那么PCB2的state=1，但是它却被放在阻塞队列里

②将PCB2从阻塞队列放到就绪队列







周转时间：一个是性能指标，另外一个是指标时公平

性能和公平中往往是矛盾的

如何实现原语的原子性？

原语的执行具有原子性，即执行过程只能一气呵成，期间不允许被中断。可以用“关中断指令”和“开中断指令”这两个特权指令实现原子性

![](https://chenalna.oss-cn-hangzhou.aliyuncs.com/img/1f7ddb9dcae01810b1a00eecf6bad483af571af2df0131e683ade11ebfc12f7f.jpg)

CPU执行了关中断指令之后，就不再例行检查中断信号，直到执行开中断指令之后才会恢复检查。

这样，关中断、开中断之间的这些指令序列就是不可被中断的，这就实现了“原子性"

OS 的内核运行于核心态，应用程序则运行于用户态。（进程控制的大量原语）

**进程通信**

1、共享存储器系统通信

在存储器中划分出一块共享存储区，诸进程通过对共享存储区的读写操作来实现通信

Shm_open() 通过系统调用，申请一片共享内存区

Void * map()通过 mmap 系统调用，将共享内存区映射到自己的地址空间





共享存储

![](https://chenalna.oss-cn-hangzhou.aliyuncs.com/img/fdb9646d4742569587c516f0ede3ba8b28476971f01e63a8fa139a79e0a04d3b.jpg)

同步互斥工作

基于数据的共享

基于存储区的共享

高级通信方式

2、消息传递系统通信

以格式化的信息为单位

**直接通信方式**

由一对通信原语 send(),receive()

**间接通信方式**

进程 Q 的消息队列

如果其他进程要发送给进程Q的消息都在Q的消息队列中

进程 P 建立消息 msg，发送原语,send(Q,msg)

被复制到了Q的消息队列中

接受原语（receive (p,&msg)）

![](https://chenalna.oss-cn-hangzhou.aliyuncs.com/img/70923663158f7f516917b74eecdb0207f55655a4f8292cb14a4a1ab91a24a179.jpg)

间接通信方式，以“信箱中作为中间实体进行消息传递

Send (A,msg)发送到哪个信箱

**3、管道通信**

管道式一个特殊的共享问题件，又名为pipe文件。就是在内存中开辟一个大小固定的内存缓冲区

先进先出的

单向通信的

管道文件被读取的部分会消失

无名管道

有名管道

4、**客户服务通信**

消息缓冲队列通信机制

# 进程控制

## 进程创建

作业调度

用户登录

提供特定服务

应用请求

进程创建原语

## 进程撤销

## 进程切换

进程切换，又叫上下文切换

1.保存当前进程的上下文  
2.恢复某个之前被抢占的进程的被保存的上下文  
3.将控制传递给这个新恢复的进程

通用寄存器、浮点寄存器、程序计数器PC、程序状态字PSW、用户栈、内核栈、描述地址空间的页表、当前进程信息的进程表（PCB）

进程已打开文件的文件表

# 进程阻塞与唤醒

# 进程调度

# 调度的层次

高级：负责将进程调入内存，分配资源。从外存的后被队列中选择若干个作业调入内存，创建进程，并将新创建的进程插入就绪队列，准备执行；此外，当作业执行完毕后回收进程。

中级：提高内存利用率和 CPU 吞吐量。将进程换出到外存，挂起状态。

低级：负责分配 CPU 资源

调度频率：低级>中级 $>$ 高级

![image-20260413005107823](https://chenalna.oss-cn-hangzhou.aliyuncs.com/img/image-20260413005107823.png)

进程调度是操作系统在内核模式中进行

忙等待

While(1)

用户态一直占用CPU，无法切换到内核模式，就无法进行进程的切换。

时钟中断

CPU响应中断，处理中断处理程序

从用户态切换到内核态

用户能关中断的话，就可以一直占用，这是不行的。

#### 进程调度方式

非抢占式

抢占式

内核完全不可抢占

内核部分抢占

内核完全可抢占

**考虑的目标**

系统设计目标

调度的公平性

资源的均衡利用

合理的系统开销

**评价指标**

CPU 利用率 忙碌时间/总时间

甘特图

系统吞吐量

总共完成了多少道作业/总共花了多少时间

周转时间和带权周转时间

周转时间——从作业提交给系统开始，到作业完成为止的这段时间间隔

它包括四部分：作业在外存后备队列上等待作业调度（进入到内存并创建进程）的时间、进程在就绪队列上等待（调入到CPU）进程调度、进程在CPU上执行的时间，进程等待I/O操作完成的时间。后三项可能在一个作业处理过程中可能发生多次。

周转时间 $=$ 作业完成时间-作业提交时间

平均周转时间——各作业周转时间之和/作业数

带权周转时间——作业周转时间/实际运行的时间 $=$ 作业完成时间-作业提交时间/作业实际运行的时间

平均带权周转时间

等待时间——进程/作业处于等待处理机时间之和

对于进程来说，等待时间就是指建立后等待被服务的时间之和，在等待 I/O完成的期间其实进程也是在被服务的，所以不计入等待时间

对于作业来说，不仅要考虑建立进程后的等待时间，还要加上作业在外存后备队列中等待的时间

响应时间

响应时间：任务到达系统到首次执行的时间
$$
\mathrm {T} _ {\text {响 应 时 间}} = \mathrm {T} _ {\text {首 次 运 行}} - \mathrm {T} _ {\text {到 达 时 间}}
$$

$$
T _ {\text {响 应 时 间}} = T _ {\text {等 待 时 间}}
$$

![image-20260413005249479](https://chenalna.oss-cn-hangzhou.aliyuncs.com/img/image-20260413005249479.png)

$$
\mathrm {T} _ {\text {响 应 时 间} \mathrm {A}} = 0
$$

$$
\mathrm {T} _ {\text {响 应 时 间} \mathrm {B}} = 0
$$

$$
\mathrm {T} _ {\text {响 应 时 间} \mathrm {C}} = 1 0 \mathrm {s}
$$

对截止时间的保证

调度算法

周转时间 $=$ 完成时间-提交时间

带权周转时间 周转时间/要求执行时间

先来先服务FCFS

![image-20260413102154209](https://chenalna.oss-cn-hangzhou.aliyuncs.com/img/image-20260413102154209.png)

ABC同时到达就绪队列

缺点：有些任务先到达到时间非常长，系统平均周转时间比较高

短作业优先

最短任务优先（Shortest Job First，非抢占）

先运行最短的任务，然后是次短的任务

当所有任务同时到达时，最短任务优先是一个最优的调度算法

![image-20260413102728432](https://chenalna.oss-cn-hangzhou.aliyuncs.com/img/image-20260413102728432.png)

![](D:/文档/Obsidian Vault/note/操作系统.docx-f9f07257-a80c-4ffe-a3e0-d49fd5e7438b/images/e39c4f75d37dd96d70ad0deecfabaebf3d023a867caa5e3cbc7c07b307b131b1.jpg)

平均周转时间：

$$
(1 0 + 2 0 + 1 2 0) / 3 = 5 0
$$

不会产生饥饿现象

**非抢占式**

短作业优先 SJF

假设A在t0时到达，需要运行100s，而B和C在t=10到达，各自需要运行10s

平均周转时间：（100+（110-10）+（120-10)）/3=103.33s

![image-20260413102742376](https://chenalna.oss-cn-hangzhou.aliyuncs.com/img/image-20260413102742376.png)

抢占式

最短剩余时间优先算法SRTN

需要比较该进程的下一次运行时间是否比当前运行进程剩余运行时间段，如果是，则抢占当前运行进程的 CPU

抢占式最短完成时间优先（STCF）

每当有新的任务到达时，会确定剩余任务和新任务中，谁的剩余时间最少

然后调度该时间最少的任务

![image-20260413102937730](https://chenalna.oss-cn-hangzhou.aliyuncs.com/img/image-20260413102937730.png)

平均周转时间：

$$
(1 0 + 2 0 + 1 2 0) / 3 = 5 0
$$

产生饥饿现象

**高响应比优先调度算法**

考虑等待时间和执行时间两个因素





$$
\text {响 应 比 (R)} = \frac {\text {等 待 时 间} + \text {执 行 时 间}}{\text {执 行 时 间}}
$$

当等待时间相同，执行时间越短，响应比越高

当执行时间相同时，等待时间越长，响应比越高

<table><tr><td>算法</td><td>思想&amp;规则</td><td>可抢占?</td><td>优点</td><td>缺点</td><td>考虑到等待时间&amp;运行时间?</td><td>会导致饥饿?</td></tr><tr><td>FCFS</td><td>自己回忆</td><td>非抢占式</td><td>公平；实现简单</td><td>对短作业不利</td><td>等待时间√运行时间×</td><td>不会</td></tr><tr><td>SJF/SPF</td><td>自己回忆</td><td>默认为非抢占式，也有SJF的抢占式版本最短剩余时间优先算法（SRTN）</td><td>“最短的”平均等待/周转时间；</td><td>对长作业不利，可能导致饥饿；难以做到真正的短作业优先</td><td>等待时间×运行时间√</td><td>会</td></tr><tr><td>HRRN</td><td>自己回忆</td><td>非抢占式</td><td>上述两种算法的权衡折中，综合考虑的等待时间和运行时间</td><td></td><td>等待时间√运行时间√</td><td>不会</td></tr></table>

**优先级调度算法**

抢占式、非抢占都有

非抢占式，每次调度时选择当前已到达且优先级最高的进程，当前进程主动放弃处理机时发生调度

抢占式还需在就绪队列变化时，检查是否发生抢占

P2先到达，先上处理机

确定优先级？

系统进程优先级高于用户进程

前台进程优先级高于后台进程

操作系统更偏好I/O型进程

与 I/O 型进程相对的时计算型进程（CPU）

动态提升？

线程I/O操作结束提升优先级

在就绪队列中随等待时间延长而提升

随占用CPU时间延长而降低

随剩余运行时间缩短而提升

完成I/O操作后提升

**时间片调度（轮转调度，round robin，RR）**

每个进程被分配一个时间段，称为时间片， 允许进程在该时间段内执行如果在时间片内结束时，该进程没有执行完，接下来会将CPU分配另外进程执行

![image-20260413111016281](https://chenalna.oss-cn-hangzhou.aliyuncs.com/img/image-20260413111016281.png)

![image-20260413111025031](https://chenalna.oss-cn-hangzhou.aliyuncs.com/img/image-20260413111025031.png)

时间片大小为2（以下括号内表示当前时刻就绪队列中的进程，进程的剩余运行时间）



![image-20260413111034221](https://chenalna.oss-cn-hangzhou.aliyuncs.com/img/image-20260413111034221.png)

0时刻（P1（5））：0时刻只有P1到达就绪队列，让P1上处理机运行一个时间片

2时刻（P2（4）→P1（3））：2时刻P2到达就绪队列，P1运行完一个时间片，被剥夺处理机，重新放到此时P2排在队头，因此让P2上处理机。（注意：2时刻，P1下处理机，同一时刻新进程P2到达，如题目中遇到这种情况，默认新到达的进程先进入就绪队列）

7时刻（P2（2)→P4（6）→P1（1））：虽然P3的时间片没用完，但是由于P3只需运行1个单位的时间，完了会主动放弃处理机，因此也会发生调度。队头进程P2上处理机。

如果每个进程都在一个时间片内完成，则轮转算法退化为FCFS算法如何确定时间片的长度？

1、系统响应时间  
2、就绪进程的数量  
3、进程调度以及上下文切换的时间开销  
4、CPU指令的速度

多级队列调度

![image-20260413111539460](https://chenalna.oss-cn-hangzhou.aliyuncs.com/img/image-20260413111539460.png)

多级反馈队列调度

![image-20260413111550033](https://chenalna.oss-cn-hangzhou.aliyuncs.com/img/image-20260413111550033.png)

优先级从高到低，时间片从小到大

各队列之间采用抢占式优先级算法调度

当CPU正在运行第i个队列中的某个进程时，又有进程而进入优先级较高的队列，则系统立即调度高优先级的进程进行

<table><tr><td>算法</td><td>思想&amp;规则</td><td>可抢占?</td><td>优点</td><td>缺点</td><td>会导致饥饿?</td><td>补充</td></tr><tr><td>时间片轮转</td><td></td><td>抢占式</td><td>公平,适用于分时系统</td><td>频繁切换有开销,不区分优先级</td><td>不会</td><td>时间片太大或太小有何影响?</td></tr><tr><td>优先级调度</td><td></td><td>有抢占式的,也有非抢占式的。注意做题时的区别</td><td>区分优先级,适用于实时系统</td><td>可能导致饥饿</td><td>会</td><td>动态/静态优先级。各类型进程如何设置优先级?如何调整优先级?</td></tr><tr><td>多级反馈队列</td><td>较复杂,注意理解</td><td>抢占式</td><td>平衡优秀666</td><td>一般不说它有缺点,不过可能导致饥饿</td><td>会</td><td></td></tr></table>

**多处理器调度**

除了调度算法决定让哪个进程上 CPU，还要确定上哪个 CPU

负载均衡

处理机亲和性

XPU数据共享

缓存一致性

公共就绪队列

## 线程

·许多应用中，存在许多同时发生的多种活动  
·线程比进程更轻量级，所以比进程更容易创建，也更容易撤销   
·如果多个线程是计算密集型，那么并不能获得性能上的增加。

如果是计算和I0的处理，那么多线程允许这些操作重叠执行。

轻量级：不需要保存那么多信息

因为 CPU 只有一个，多个线程还是会竞争 CPU

如果是计算和IO的处理，那么多线程允许这些操作的重叠执行。会提升性能。

多线程≠并行

提高系统的并发程度，同一个进程中两个线程

线程作为调度和执行到基本单位，把进程作为资源分配和拥有的基本单位

![image-20260413111751048](https://chenalna.oss-cn-hangzhou.aliyuncs.com/img/image-20260413111751048.png)



每个进程中的内容

地址空间

全局变量

打开文件

子进程

即将发生的报警

信号与信号处理程序

账户信息

每个线程中的内容

程序计数器

状态

程序计数器

寄存器

堆栈

**用户级线程**

运行在用户态，由支撑线程一组应用程序代码完成， 该组代码称为线程库，运行在用户空间，

操作系统并不知道线程的单位，仍以进程为单位

很多编程语言提供了强大的线程库，可以实现线程的创建、销毁、调度等功能

线程的引入

‐将拥有资源的实体和执行的实体分开，使执行的实体具有较少的资源，从而减少并发执行的开销，从而提高系统的并发程度。

在没有线程的时候，同一个进程是能单一的使用某一个功能，比如运行代码的这一部分其他部分就没有运行，线程的引入可以同时运行代码的多个部分

‐ 拥有资源的基本单位——进程；

‐执行的基本单位（即 CPU 调度和分派的单位)——线程。

$\blacktriangle$ 线程是进程内一个相对独立的运行单位，一个进程可以有一个或多个线程（至少有一个），这些线程共享这个进程的代码、数据及大部分管理信息，但每个线程有自己的程序计数器、堆栈和线程控制块。

$\blacktriangle$ 但对用户级线程而言，内核进行 CPU 调度仍然以进程（而不是用户级线程）为单位。

内核级线程（KLT）

组合方式

线程表 TCB

左边是线程与进程共享的内容（？）

![image-20260413113418957](https://chenalna.oss-cn-hangzhou.aliyuncs.com/img/image-20260413113418957.png)

线程包的实现

1、 用户进程管理

先放置在用户空间中，内核的角度按正常的方式管理，即单线程进程

2、 内核管理线程

但是内核开销较大

3、 混合

![image-20260413113440905](https://chenalna.oss-cn-hangzhou.aliyuncs.com/img/image-20260413113440905.png)

![image-20260413113447135](https://chenalna.oss-cn-hangzhou.aliyuncs.com/img/image-20260413113447135.png)



# 同步和异步

### 临界资源

系统中某些资源一次只允许一个进程使用。这类自愿成为临界资源

如物理设备打印机、软件资源共享变量、文件、表格等

必须以互斥的方式共享

每个进程中访问临界资源的那段代码叫做临界区

每个进程在进入临界区之前应该先对欲访问的临界资源进行检查，看它是否正被其他进程访问，如果临界资源未被其它进程访问，则该进程便可进入临界区访问该临界资源，并把临界源的状态设置为“忙”；

通常把这段置于临界区之前的用于检查临界资源使用状态的代码称为进入区，相应地，当进程访问完临界资源退出临界区时，将临界资源状态恢复为“空闲”。完成该项工作的代码称为“退出区”。其余无关的称为“剩余区”

对临界资源的互斥访问，可以在逻辑上分为如下四个部分：

![image-20260413113759681](https://chenalna.oss-cn-hangzhou.aliyuncs.com/img/image-20260413113759681.png)

临界区是进程中访问临界资源的代码段。

进入区和退出区是负责实现互斥的代码段。

互斥

互斥共享

称之为临界资源。必须以互斥的方式访问

同时共享

同步

直接制约关系，某些位置上协调，相互合作的进程按一定的先后顺序

进程的制约关系

直接制约：源于进程合作

间接制约：源于资源共享

10. 同步

为了保证进程正确的并发执行，对多个相关进程在执行的次序上进行协调的过程。

竞争条件

![image-20260413114013001](https://chenalna.oss-cn-hangzhou.aliyuncs.com/img/image-20260413114013001.png)

Out：打印机即将要打印的位置

In：打印机需要把文件送入的位置

进程 A，进程 B 几乎在同一时刻向打印机发送文件

进程 A 还没有完，时间片用完，切换进程 B，此时 $\mathsf { i n } { } = 7$ 是空的，覆盖了。

临界区：

对共享内存进行访问的程序片段称为临界区。

每个进程中访问临界资源的那段代码为临界区。

临界资源：系统中某些资源一次只允许一个进程使用，多个进程共享临界资源时，必须互斥方式共享。

临界区：访问临界资源的那段代码

![image-20260413114434863](https://chenalna.oss-cn-hangzhou.aliyuncs.com/img/image-20260413114434863.png)

同步机制满足原则：

满足以下4个条件：

1.任何两个进程不能同时处于其临界区  
2.不应对CPU的速度和数量做任何假设  
3.临界区外运行的进程不得阻塞其他进程  
4.不得使进程无限期等待进入临界区

1、 空闲让进，临界区空闲时，允许一个请求进入临界区的进程立即进入临界区  
2、忙则等待，当已有进程接入临界区，其它试图进入临界区的进程必须等待   
3、有限等待，对请求访问的进程，应保证能在有限时间内进入临界区（保证不会饥饿）  
4、 让权等待，当进程不能进入临界区，应立即释放 CPU

Peterson 算法

```txt
define FALSE 0
#define TRUE 1
#define N 2/*进程数量*/
int turn;/*现在轮到谁*/
int interested[N]; /*所有值初始化为0（FALSE）*/
void enter_region(int process);/*进程是0或1*/
{
    int other;/*其他进程号*/
    other = 1 - process; /其他进程号 */
    interested[process] = TRUE; /*另一方进程*/
    turn = process; /*表明所感兴趣的*/
    while (turn == process && interested[other] == TRUE); /*空语句 */
}
void leave_region(int process)
{
    interested[process] = FALSE; /*表示离开临界区*/
} 
```

**硬件方式**

均存在让权等待

#### 禁止中断

**利用专用机器指令**

解决 Swap 指令/XCHG 指令/TSL 指令

执行的过程不允许中断，利用硬件的方式变成了原子操作

ISL指令用硬件实现的，执行的过程不允许被中断，只能一气呵成。以下是用C语言描述的逻辑

```txt
//布尔型共享变量lock表示当前临界区是否被加锁  
//true表示已加锁，false表示未加锁  
bool TestAndSet（bool *lock）{  
    bool old;  
    old = *lock; //old用来存放lock原来的值  
    *lock = true; //无论之前是否已加锁，都将lock设为true  
    return old; //返回lock原来的值  
} 
//TSL指令实现互斥
while（TestAndSet（&lock)）：//上锁并检查
临界区代码段...
lock = false;//”解锁“
剩余区代码段
```

测试并加锁-Test and set lock

![image-20260419190449166](https://chenalna.oss-cn-hangzhou.aliyuncs.com/img/image-20260419190449166.png)





CPU将锁住内存总线，禁止其他CPU在本条指令结束之前访问内存



![image-20260419190458429](https://chenalna.oss-cn-hangzhou.aliyuncs.com/img/image-20260419190458429.png)





![](D:/文档/Obsidian Vault/note/操作系统.docx-f9f07257-a80c-4ffe-a3e0-d49fd5e7438b/images/694a058f4e9470e497633f0d5ada2707aa889b5ad8c6f505d3af48508ad75066.jpg)

```asm
enter_region: {            复制锁到寄存器并将锁设为1  

TSL REGISTER,LOCK  锁是零吗？  
CMP REGISTER,#0   若不是零，说明锁已被设置，所以循环
JNE enter_region  返回调用者，进入了临界区
RET 
}
```

```asm
leave_region:  	在锁中存入0
MOVE LOCK,#0  返回调用者
RET 
```



**利用软件**

单标志、双标志先检查、双标志后检查

进入区的检查、上锁操作无法一气呵成——锁

Peterson 算法

有while循环，未遵循让权等待

**信号量机制**

锁：mutexlock

只有 true 和 false 两种表示锁是否可用

Aacquire 获得锁。Release 释放锁

每个互斥锁都有一个布尔变量



互斥锁



​	解决临界区最简单的工具就是互斥锁（mutexlock）。一个进程在进入临界区时应获得锁：在退出临界区时释放锁。函数acquire()获得锁，而函数release()释放锁。

​	每个互斥锁有一个布尔变量available，表示锁是否可用。如果锁是可用的，调用acqiure()会成功，且锁不再可用。当一个进程试图获取不可用的锁时，会被阻塞，直到锁被释放。

```c
acquire()
{
	while(!available) //忙等待
		;
	available=false;//获得锁

}
release()
{
available=true;//释放锁

}
```



acquire()或release()的执行必须是原子操作，因此互斥锁通常采用硬件机制来实现。

​	互斥锁的主要缺点是忙等待，当有一个进程在临界区中，任何其他进程在进入临界区时必须连续循环调用acquireO。当多个进程共享同一CPU时，就浪费了CPU周期。因此，互斥锁通常用于多处理器系统，一个线程可以在一个处理器上等待，不影响其他线程的执行。

![image-20260419191800964](https://chenalna.oss-cn-hangzhou.aliyuncs.com/img/image-20260419191800964.png)

**信号量**（semaphore）

P/down()/wait（S）

V/up()/signal（S）

Dijkstra提出了一个方法，用一个整型变量来记录唤醒次数，称作信号量

两种操作：分别为P操作和V操作，Proberen尝试，Verhogen(增加)

P：检查信号量的值是否大于0，若值大于0，则将其值减去1并继续 如果该值为0，则进程睡眠

V：对信号量的值加1。

原子操作：不可分割的操作

原子操作


P操作进行检查信号量的数值、修改信号量数值的过程，是不可分割的。

当信号量等于0时，检查信号量和睡眠操作也是原子操作。
V操作，信号量加1和唤醒一个进程同样不可分割。



**原语是一种特殊的程序段，不可被中断**



如何实现不可分割的PV操作？

通常将P操作和V操作作为系统调用实现。

具体操作：测试信号量、更新信号量以及需要时使某个进程睡眠

对于单个CPU可以通过屏蔽中断，多个CPU可以通过TSL或Swap指令

内核

fork()

read()

open()



1.分析并发进程的关键活动，划定临界区（如：对临界资源打印机的访问就应放在临界区）  
2.设置互斥信号量mutex，初值为1

```
/*信号量机制实现互斥*/
semaphore mutex=1;//初始化信号量
P1()
{
...
P(mutex);//使用临界资源前需要加锁
临界区代码段；
V(mutex);//使用临界资源
...
}
P2()
{
...
P(mutex);
临界区代码段...
V(mutex);
 ...
}
要会自己定义记录型信号量，但如果题目中没特别说明，可以把信号量的声明简写成这种形式
```

信号量机制实现进程互斥



信号量mutex表示“进入临界区的名额“

1.分析并发进程的关键活动，划定临界区（如：对临界资源打印机的访问就应放在临界区）  
2.设置互斥信号量mutex，初值为1   
3.在进入区P（mutex）--申请资源  
4.在退出区V（mutex)--释放资源

```
/*记录型信号量的定义*/
typedef struct
{
int value;   //剩余资源数

struct process *L;//等待队列

}semaphore;
```







注意：对不同的临界资源需要设置不同的互斥信号量。

P、V操作必须成对出现。缺少P(mutex）就不能保证临界资源的互斥访问。

缺少V（mutex）会导致资源永不被释放，等待进程永不被唤醒。





| P1进程     | P2进程     | P3进程     | P4进程     |
| ---------- | ---------- | ---------- | ---------- |
| P(mutex1)  | P(mutex1)  | P(mutex2)  | P(mutex2)  |
| 临界区     | 临界区     | 临界区     | 临界区     |
| （打印机） | （打印机） | （摄像头） | （摄像头） |
| V(mutex1)  | V(mutex1)  | V(mutex2)  | V(mutex2)  |



信号量背后的含义，一个信号量对应一种资源

用户可以通过操作系统提供的一对原语来对信号量进行操作

信号量可以是整型、记录型、AND型、信号量集

信号量的值= 这种资源的剩余数量

P(S)/down()/wait()——申请一个资源S，如果资源不够就阻塞等待

V(S)/up()/signal——释放一个资源S，如果有进程在等待该资源则唤醒一个进程

一对原语 wait(S)/signal(S)

整形信号量：只有三种，初始化、P、V

一个整数型的变量作为信号量，用来表示系统中某种资源的数量

```
int S=1;//初始化整形信号量S，表示当前系统中可用的打印机资源数
void wait(int S){//wait原语，相当于“进入区”
	while(S<=0);//如果资源数不够，就一直循环等待
S=S-1;//如果资源数够，则占用一个资源
}
void signal(int S )//signal原语，相当于“退出区”
{
S=S+1;//使用完资源后，在退出区释放资源
}



进程P0：

'''
wait(s);//进入区，申请资源
使用打印机资源...//临界区,访问资源
Signal(S);//退出区，释放资源

'''

```

用原语实现了，“检查”和“上锁”解决了并发、异步的问题

Wait原语又不能被中断。难道CPU卡在哪里了？

记录型信号量

```c
/*记录型信号量的定义*/  
typedef struct {
    int value; //剩余资源数
    struct process *L; //等待队列
} semaphore; 
```

```txt
/*某进程需要使用资源时，通过wait原语申请*/  
void wait(semaphore S)
{S.value--;
if(S.value<0)
{block(S.L);
}
如果剩余资源数不够，  
使用block原语使进程从运行态进入阻塞态，并把挂到信号量S 的等待队列（阻塞队列）中
```

```javascript
/*进程使用完资源后，通过signal 原语释放*/  
void signal (semaphore S) {  
    s.value++;  
    if (s.value <= 0) {  
        wakeup(S.L);  
    }  
}
```

wait 原语

某进程需要使用资源S

S--表示请求分配一个资源

如果S值 $\geqslant 0$ ，则表示可以为进程分配资源，该进程进入临界区继续执行，

如果 S 的值 $< 0$ ，表示-1 之前已没有资源可供分配（ $S = 0 $ ）

执行block原语，将其进程阻塞起来，插入到S的阻塞队列中，然后执行另一进程

Signal

${ { \sf S } } { + } { + }$ 表明释放一个资源

如果 $\mathsf { S } { > } 0$ ，则表示阻塞队列为空，该进程继续执行；

如果S≤0，表示阻塞队列中有阻塞的进程，wakeup原语唤醒队首进程从阻塞状态变为就绪状态

不管怎么说，先--或者 + + ，然后判断，决定阻塞or释放

$\mathsf { S } > 0$ ，其值表示当前可供分配的资源数目

$\mathsf { S } { < } 0$ ，其绝对值表示阻塞队列中的进程数目

信号量集机制

信号量机制实现进程互斥

如果题目没有特别说明，对于一个信号量的定义只需要 semaphore

smaphore这个信号量不是整型信号量而是记录型信号量，带有排队阻塞的信号量，并不会忙等

对于不同的临界资源需要设置不同的信号量

利用信号量机制可以方便地解决多个进程互斥使用临界资源的问题

| **生产者/操作** | **必须在谁之后执行**  | **依赖的信号量** |
| --------------- | --------------------- | ---------------- |
| PB 读取 buf1    | PA 写完 buf1          | full1            |
| PC 打印 buf2    | PB 写完 buf2          | full2            |
| PA 写 buf1      | 上一次 PB 读取完 buf1 | empty1           |
| PB 写 buf2      | 上一次 PC 打印完 buf2 | empty2           |



信号量机制实现进程同步

要让各并发进程按要求有序地推进

设置同步信号量S，初始为0

用信号量实现进程同步：

1.分析什么地方需要实现“同步关系”，即必须保证“一前一后”执行的两个操作（或两句代码）  
2.设置同步信号量S,初始为0   
3.在“前操作”之后执行V（S）   前V后P

4.在“后操作”之前执行P（S）

$/*信号量机制实现同步*/$

semaphore S=0；//初始化同步信号量，初始值为0

理解：信号量s代表“某种资源”，刚开始是没有这种资源的。P2需要使用这种资源，而又只能由P1产生这种资源



```C
P1()				P2()		
{					{

代码1；	--->	P(S);
代码2；	|		代码4；
V(S);-->释放资源   代码5；	
代码3；			代码6；
}				}
```

保证了代码4一定是在代码2之后执行

若先执行到V（S）操作，则 S++后S=1。之后当执行到P(S）操作时，由于S=1，表示有可用资源，会执行S--，S的值变回0，P2进程不会执行block原语，而是继续往下执行代码4。

若先执行到P(S)操作，由于S=0，S--后S=-1，表示此时没有可用资源，因此P操作中会执行block原语，主动请求阻塞。

之后当执行完代码2，继而执行V(S)操作，S++，使S变回0，由于此时有进程在该信号量对应的阻塞队列中，因此会在V操作中执行wakeup原语，唤醒P2进程。这样P2就可以继续执行代码4了.

首先可以定义一个初值为1的信号量S（互斥信号量）

当进程想要进入临界区访问临界资源时，wait（）操作申请资源。

退出临界区时执行signal操作（）释放资源。

只要把进程临界区置于 wait（）和 siganl()之间，就可以实现互斥。



该方案使用了三个信号量

1.一个称为fuIl，用来记录充满的缓冲区的数量  
2.一个称为empty，记录空的缓冲区数目  
3.一个称为mutex，用来确保生产者和消费者不会同时访问缓冲区



```C
#define N 100 /*缓冲区中的槽数目*/

typedef int semaphore; /*信号量是一种特殊的整型数据*/

semaphore mutex = 1; /*控制对临界区的访问*/
semaphore empty = N; /*计数缓冲区的空槽数目*/
semaphore full = 0; /*计数缓冲区的满槽数目*/
```



为什么 empty 和 full 也要设为信号量？

down() → sem_P()

up() → sem_V()

```


void producer(void) 
{
    int item;
    while (TRUE) { /*TRUE是常量1*/
        item = produce_item(); /*产生放在缓冲区中的一些数据*/
        down(&empty); /P(&empty);*将空槽数目减1*/
        down(&mutex); /p(&mutex);*进入临界区*/
        insert_item(item); /*将新数据项放到缓冲区中*/
        up(&mutex); /V(mutex);*离开临界区*/
        up(&full); /*将满槽的数目加1*/
    }
}
full>0 full-1 
void consumer(void) 
{
    int item;
    while (TRUE) { /*无限循环*/
        down(&full); /*将满槽数目减1*/
        down(&mutex); /*进入临界区*/
        item = remove_item(); /*从缓冲区中取出数据项*/
        up(&mutex); /*离开临界区*/
        up(&empty); /*将空槽数目加1*/
        consume_item(item); /*处理数据项*/
    }
}
```





不能相反

会导致死锁

Mutex = 0 ；

Producer 会被阻塞；

Consumer 发现 mutex=0，也会被阻塞。



信号量同步的缺点

同步操作分散

同步操作分散在各个进程中，使用不当就可能导致进程死锁

例如：P、V操作的次序错误、重复或遗漏



代码可读性差

想要了解对于一组共享变量以及信号量的操作是否正确，需要阅读整个并发程序的代码

## 经典进程同步问题

现要求学生使用 swap 指令和[布尔型](https://zhida.zhihu.com/search?content_id=236237170&content_type=Article&match_order=1&q=布尔型&zhida_source=entity)变量 lock 实现临界区互斥。lock 为线程间共享的变量。lock 的值为 TRUE 时线程不能进入临界区，为 FALSE 时线程能够进入临界区。某同学编写的实现临界区互斥的[伪代码](https://zhida.zhihu.com/search?content_id=236237170&content_type=Article&match_order=1&q=伪代码&zhida_source=entity)如题 45(a) 图所示。

![image-20260419201839026](https://chenalna.oss-cn-hangzhou.aliyuncs.com/img/image-20260419201839026.png)

(1) 题 45(a) 图中伪代码中哪些语句存在错误？将其改为正确的语句（不增加语句条数）。

(2) 题 45(b) 图中给出了两个变量值的函数 newSwap() 的代码是否可以用函数调用语句“newSwap(&key, &lock)”代替指令“swap key, lock”以实现临界区的互斥？为什么？

### 生产者消费者问题

M个生产者

N个消费者

共享一个具有k个缓冲区的循环缓冲池

生产者不断生产产品，将每个产品依次放入缓冲区中（一个缓冲区正好放一个产品）

消费者依次从缓冲区取出产品并进行消费。规定消费者不能从一个空缓冲区中取产品，生产者不能向一个已装满产品且尚未被取走的缓冲区中投放产品。

什么时候用数字 out in =0，1

什么时候用信号量 mutex ，Empty，full？

![image-20260419201907981](https://chenalna.oss-cn-hangzhou.aliyuncs.com/img/image-20260419201907981.png)

1、 count ==1 时唤醒消费者，保证至少有一个元素可消费；  
2、 count N- 1 时唤醒生产者，保证有一个空位可写；

当前缓冲区已空

消费者去执行的时候发现 coun $\scriptstyle = = 0$

但是调度程序切换到了生产者，发现 count $^ { : = 1 }$

生产者向消费者发送 wakeup

消费者还没来得及 sleep 却 wakeup

丢失唤醒（lost wakeup）问题

![image-20260419201933042](https://chenalna.oss-cn-hangzhou.aliyuncs.com/img/image-20260419201933042.png)

最后消费者 sleep

生产者也 sleep 了

可以设置等待位

但是麻烦

# 读者-写者问题

多个进程同时读数据库是可以的

当一个进程正在写数据库，其它进程既不能读也不能写

任意时刻只能由一个写数据库

```
semaphore mutex = 1;  /*控制对rc的访问*/
semaphore db = 1;     /*控制对数据库的访问*/
int rc = 0;           /*正在读的读者数目*/

void writer(void)
{
    while (TRUE) {           /*无限循环*/
        think_up_data();      /*非临界区*/
        down(&db);            /*获取对数据库的互斥访问*/
        write_data_base();    /*更新数据*/
        up(&db);              /*释放对数据库的互斥访问*/
    }
}

void reader(void)
{
    while (TRUE) {            /*无限循环*/
        down(&mutex);          /*获得对rc的互斥访问权*/
        rc = rc + 1;           /*现在又多了一个读者*/
        if (rc == 1) {         /*如果这是第一个读者...*/
            down(&db);         /*阻止写者访问数据库*/
        }
        up(&mutex);             /*释放对rc的互斥访问*/
        
        read_data_base();       /*访问数据*/
        
        down(&mutex);           /*获取对rc的互斥访问*/
        rc = rc - 1;            /*现在减少了一个读者*/
        if (rc == 0) {          /*如果这是最后一个读者...*/
            up(&db);             /*允许写者访问数据库*/
        }
        up(&mutex);              /*释放对rc的互斥访问*/
        
        use_data_read();         /*非临界区：使用读取的数据*/
    }
}
```

读者优先——有写者想写则必须等待

### 哲学家就餐问题

两种活动交替：吃饭和思考

（对哲学家而言其他活动无关紧要）

一个哲学家饿了时，他试图分两次去取左边和右边的叉子，每次拿一把，但不分次序，如果成功得到两把叉子就开始吃饭，吃完饭后放下叉子继续思考

```c
define N 5  /* 哲学家的数目 */
void philosopher(int i) /* i: 哲学家编号, 从0到4 */
{
    while (TRUE) {
        think();
        take_fork(i); //拿起左边叉子
        take_fork((i+1) % N); //拿起右边叉子
        eat();//进食
        put_fork(i); /* 将左叉放回桌上 */
        put_fork((i+1) % N); /* 将右叉放回桌上 */
}
```

同时成功取到左侧叉子，再取右侧筷子失效。死锁

如果五位哲学家同时拿起了左边的叉子，那么就没有人能够拿到他们右边的叉子，于是发生死锁

拿到左叉子后，检查右叉子是否可用，如果不可用，放下左叉子，等待一段时间后，重复这个过程。

可能在某个瞬间，所有哲学家同时拿起了左叉子，然后检查右叉子不可用，又放下左叉子。如此永远重复下去。所有程序都在不停的运行，但是都无法取得进展，称为饥饿

```
#define N 5                 /*哲学家数目*/
#define LEFT (i+N-1)%N      /*i的左邻居编号*/
#define RIGHT (i+1)%N       /*i的右邻居编号*/
#define THINKING 0          /*哲学家在思考*/
#define HUNGRY 1            /*哲学家试图拿起叉子*/
#define EATING 2            /*哲学家进餐*/

typedef int semaphore;      /*信号量是一种特殊的整型数据*/

int state[N];               /*数组用来跟踪记录每位哲学家的状态*/
semaphore mutex = 1;        /*临界区的互斥*/
semaphore s[N];             /*每个哲学家一个信号量*/

/* 函数声明 */
void test(int i);

void take_forks(int i)      /*i:哲学家编号，从0到N-1*/
{
    down(&mutex);           /*进入临界区*/
    state[i] = HUNGRY;      /*记录哲学家i处于饥饿的状态*/
    test(i);                /*尝试获取2把叉子*/
    up(&mutex);             /*离开临界区*/
    down(&s[i]);            /*如果得不到需要的叉子则阻塞*/
}

void test(int i)            /*i:哲学家编号，从0到N-1*/
{
    if (state[i] == HUNGRY && state[LEFT] != EATING && state[RIGHT] != EATING) {
        state[i] = EATING;
        up(&s[i]);
    }
}

void put_forks(int i)   
{
    down(&mutex);           /*进入临界区*/
    state[i] = THINKING;    /*哲学家已经就餐完毕*/
    test(LEFT);             /*检查左边的邻居现在可以吃吗*/
    test(RIGHT);            /*检查右边的邻居现在可以吃吗*/
    up(&mutex);             /*离开临界区*/
}
```



# 死锁

若系统中存在一组进程（两个或两个以上），且它们都无限等待该组进程中另一进程所占有的无法释放的资源，无法向前推进

1、竞争资源  
2、进程推进顺序不当



![image-20260419202533585](https://chenalna.oss-cn-hangzhou.aliyuncs.com/img/image-20260419202533585.png)

3、信号量使用不当（顺序不当，其实可以把互斥信号量、同步信号量也看成一种竞争资源）

饥饿：长进程一直得不到处理机

死循环：跳不出循环

<table><tr><td></td><td>共同点</td><td>区别</td></tr><tr><td>死锁</td><td rowspan="3">都是进程无法顺利向前推进的现象（故意设计的死循环除外）</td><td>死锁一定是“循环等待对方手里的资源”导致的，因此如果有死锁现象，那至少有两个或两个以上的进程同时发生死锁。另外，发生死锁的进程一定处于阻塞态。</td></tr><tr><td>饥饿</td><td>可能只有一个进程发生饥饿。发生饥饿的进程既可能是阻塞态(如长期得不到需要的I/O设备)，也可能是就绪态(长期得不到处理机)</td></tr><tr><td>死循环</td><td>可能只有一个进程发生死循环。死循环的进程可以上处理机运行（可以是运行态），只不过无法像期待的那样顺利推进。死锁和饥饿问题是由于操作系统分配资源的策略不合理导致的，而死循环是由代码逻辑的错误导致的。死锁和饥饿是管理者（操作系统）的问题，死循环是被管理者的问题。</td></tr></table>

指多个进程在运行过程中因争夺资源而造成的一种僵局，当进程处于这种状态时，若无外力作用，这些进程都将无法再向前推进。

```mermaid
graph LR
	A[进程A] -->B1
	subgraph B
        B1[扫描仪]
        B2[刻录机]
    end 
	
	C[进程B] -->B2
```





​	有两个进程分别将文档扫描后刻录到光盘上

我们把这一类需要排他性使用的对象称为资源

资源可以是硬件设备(打印机等)或者是一组信息(数据库中加锁记录)

可抢占资源

可以从拥有它的进程中抢断而不会产生副作用，例如内存

不可抢占资源

无法从拥有它的进程处抢夺过来，例如光盘刻录机

产生死锁的必要条件：互斥条件、占有且等待条件、不可剥夺条件、循环等待条件。必要条件：这四个条件必须同时存在才会导致死锁。

1. 充分条件（ $( A  \rightarrow { B } )$ ）：

​	A 是 B 的“充分”条件，即 A 成立足以保证 B 成立，但 B 可能由其他原因导致。

2. 必要条件（ $( B \gets A )$ ）：

​	B 是 A 的“必要”条件，即A 成立必须依赖 B 成立（或无 B 必无 A），但 B成立不一定导致 A.

3. 关键区别：

​	充分条件关注“A 能否保证 B”，必要条件关注“B 是否是 A 的前提”。

必要条件

死锁

互斥条件

每个资源要么已经分配给了一个进程，要么就是可以申请使用的

请求和保持条件（占有和等待）

进程已经保持了至少一个资源，但是又提出了新的资源请求。如果该资源

又被其他进程占有，此时请求进程阻塞，但是对已获得的资源保持不放。

·不可抢占条件

进程已获得的资源不能被强制性的抢占，只能使用完时由其自己释放

环路等待条件

发生死锁时 系统中一定有两个或者两个以上的进程组成一条环路，该环路

中每个进程都在等待着下一个进程所占用的资源

**1. 死锁预防（Deadlock Prevention）**

**死锁预防的基本思想是通过对资源分配进行限制，确保系统永远不可能进入死锁状态。它通过强制系统遵循一定的规则来预防死锁的发生。对于嵌入式系统来说，预防死锁比尝试在死锁发生后恢复更为安全和高效，原因如下：**

- **高可靠性要求：在关键任务的嵌入式系统中，死锁发生时可能导致系统不可用，甚至造成严重的任务失败。死锁预防通过限制资源请求的方式，确保系统可以避免进入死锁状态，从根本上消除了死锁的风险。**
- **实时性要求：这些系统往往需要在严格的时间限制内完成任务。死锁一旦发生，可能导致系统长时间处于挂起状态，无法继续进行任务。死锁预防策略可以确保资源按顺序分配，避免系统在任何时刻陷入死锁。**
- **无法恢复的环境：与传统计算机系统不同，嵌入式系统往往没有丰富的硬件和软件资源进行死锁恢复。尤其在卫星、航天器等环境中，一旦发生死锁，恢复机制可能不如预防机制可靠。**

**死锁预防的方法：**

- **资源有序请求：强制所有进程按照一定顺序请求资源。这避免了循环等待的发生，从而避免死锁。**
- **限制资源持有：通过限制进程能够同时持有的资源数量，防止进程持有多个资源而阻塞其他进程。**

**2. 死锁避免（Deadlock Avoidance）**

**死锁避免要求系统动态地评估每个资源分配请求，确保系统不会进入一个不安全的状态，即在任何时刻都不允许进行可能导致死锁的资源分配。对于嵌入式系统，死锁避免也是一个有效的选择，特别是当系统可以确定资源需求时。**

- **任务的可预测性：嵌入式系统通常具有明确的任务流程和资源需求，可以通过系统设计来控制死锁避免的算法。例如，银行家算法就是一个典型的死锁避免算法，它通过检查资源请求是否安全，来避免进入不安全的状态。**
- **高容错性：在复杂的嵌入式系统中，任务和资源的管理需要特别小心，死锁避免可以帮助系统在极限条件下仍然保持稳定运行，不至于因死锁导致任务失败。**

 

# 死锁避免

# 安全状态

图中a为安全状态

![image-20260419204619141](https://chenalna.oss-cn-hangzhou.aliyuncs.com/img/image-20260419204619141.png)

安全序列，就是指如果系统按照

安全序列可能有多个

找不出任何一个安全序列，就进入了不安全状态

进入不安全状态，就可能发生死锁

不安全状态并不是死锁

![image-20260419204630470](https://chenalna.oss-cn-hangzhou.aliyuncs.com/img/image-20260419204630470.png)

# 银行家算法

判断对请求的满足是否会导致进入不安全状态

如果会导致不安全状态，就拒绝该请求，否则就满足该请求

![image-20260419204637293](https://chenalna.oss-cn-hangzhou.aliyuncs.com/img/image-20260419204637293.png)

# 安全序列

<table><tr><td></td><td>最大需求</td><td>已借走</td><td>最多还会借</td></tr><tr><td>B</td><td>70</td><td>20+30=60</td><td>50-30=20</td></tr><tr><td>A</td><td>40</td><td>10</td><td>30</td></tr><tr><td>T</td><td>50</td><td>30</td><td>20</td></tr></table>

给B借30亿是不安全的,之后手里只剩10亿，如过BAT都提出再借20亿的需求，那么任何一个企业都不满足

<table><tr><td></td><td>最大需求</td><td>已借走</td><td>最多还会借</td></tr><tr><td>B</td><td>70</td><td>20</td><td>50</td></tr><tr><td>A</td><td>40</td><td>10+20=30</td><td>30-20=10</td></tr><tr><td>T</td><td>50</td><td>30</td><td>20</td></tr></table>


给A借20亿是安全的，因为存在T→B->A这样的安全序列。

所谓安全序列，就是指如果系统按照这种序列分配资源，则每个进程都能顺利完成，只要能找出一个安全序列，系统就是安全状态。当然，安全序列可能有多个。

如果分配了资源之后，系统中找不出任何一个安全序列，系统就进入了不安全状态。这就意味着之后可能所有进程都无法顺利的执行下去。当然，如果有进程提前归还了一些资源，那系统也有可能重新回到安全状态，不过我们在分配资源之前总是要考虑到最坏的情况。

可以把单维的数字拓展为多维的向量。比如：系统中有5个进程P0-P4,3种资源R0-R2，初始数量为(10,5,7)则某一时刻的情况可表示如下：

<table><tr><td></td><td>最大需求</td><td>已借走</td><td>最多还会借</td></tr><tr><td>B</td><td>70</td><td>20</td><td>50</td></tr><tr><td>A</td><td>40</td><td>10</td><td>30</td></tr><tr><td>T</td><td>50</td><td>30</td><td>20</td></tr></table>

<table><tr><td>进程</td><td>最大需求</td><td>已分配</td><td>最多还需要</td></tr><tr><td>P0</td><td>(7,5,3)</td><td>(0,1,0)</td><td>(7,4,3)</td></tr><tr><td>P1</td><td>(3,2,2)</td><td>(2,0,0)</td><td>(1,2,2)</td></tr><tr><td>P2</td><td>(9,0,2)减</td><td>(3,0,2)</td><td>(6,0,0)</td></tr><tr><td>P3</td><td>(2,2,2)</td><td>(2,1,1)</td><td>(0,1,1)</td></tr><tr><td>P4</td><td>(4,3,3)</td><td>(0,0,2)</td><td>(4,3,1)</td></tr></table>

此时总共已分配（7，2，5），还剩余（3,3,2)

可把最大需求、已分配的数据看作矩阵，两矩阵相减，就可算出各进程最多还需



资源总数（10,5，7），剩余可用资源（7，4，3）

<table><tr><td>进程</td><td>最大需求</td><td>已分配</td><td>最多还需要</td></tr><tr><td>P0</td><td>(7,5,3)</td><td>(0,1,0)</td><td>(7,4,3)</td></tr><tr><td colspan="4"></td></tr><tr><td>P2</td><td>(9,0,2)</td><td>(3,0,2)</td><td>(6,0,0)</td></tr><tr><td colspan="4"></td></tr><tr><td>P4</td><td>(4,3,3)</td><td>(0,0,2)</td><td>(4,3,1)</td></tr></table>

此时系统是否处于安全状态？

思路：尝试找出一个安全序列。P1,P3,P0,P2,P4。

依次检查剩余可用资源（3，3，2）是否能满足各进程的需求

可满足P1需求，将P1加入安全序列，并更新剩余可用资源值为(5,3,2)

依次检查剩余可用资源（5，3，2）是否能满足剩余进程（不包括已加入安全序列的进程）的需求

可满足P3需求，将P3加入安全序列，并更新剩余可用资源值为（7，4，3）

依次检查剩余可用资源（7，4，3)是否能满足剩余进程（不包括已加入安全序列的进程）的需求...

以此类推，共五次循环检查即可将5个进程都加入安全序列中，最终可得一个安全序列。该算法称为安全性算法。可以很方便地用代码实现以上流程，每一轮检查都从编号较小的进程开始检实际做题时可以更快速的得到安全序列。

如果导致不安全状态，就拒绝该请求，否则就满足该请求。

死锁避免从本质上说是不可能的，因为很少由进程在运行之前就知道运行所需的最大资源数，进程数也是不固定的。

资源总数(10,5,7)，剩余可用资源(3,3,2)

<table><tr><td>进程</td><td>最大需求</td><td>已分配</td><td>最多还需要</td></tr><tr><td>P0</td><td>(7,5,3)</td><td>(0,1,0)</td><td>(7,4,3)</td></tr><tr><td>P1</td><td>(3,2,2)</td><td>(2,0,0)</td><td>(1,2,2)</td></tr><tr><td>P2</td><td>(9,0,2)</td><td>(3,0,2)</td><td>(6,0,0)</td></tr><tr><td>P3</td><td>(2,2,2)</td><td>(2,1,1)</td><td>(0,1,1)</td></tr><tr><td>P4</td><td>(4,3,3)</td><td>(0,0,2)</td><td>(4,3,1)</td></tr></table>

实际做题（手算）时可用更快速的方法找到一个安全序列：

经对比发现，（3,3,2）可满足P1、P3，说明无论如何，这两个进程的资源需求一定是可以依次被满足的，因此P1、P3一定可以顺利的执行完，并归还资源。可把P1、P3先加入安全序列。

资源总数(10,5,7)，剩余可用资源(7,4,3)

<table><tr><td>进程</td><td>最大需求</td><td>已分配</td><td>最多还需要</td></tr><tr><td>P0</td><td>(8,5,3)</td><td>(0,1,0)</td><td>(8,4,3)</td></tr><tr><td colspan="4"></td></tr><tr><td>P2</td><td>(9,5,2)</td><td>(3,0,2)</td><td>(6,5,0)</td></tr><tr><td colspan="4"></td></tr><tr><td>P4</td><td>(4,3,6)</td><td>(0,0,2)</td><td>(4,3,4)</td></tr></table>

再看一个找不到安全序列的例子：

经对比发现，（3,3,2）可满足P1、P3，说明无论如何，这两个进程的资源需求一定是可以依次被满足的，因此P1、P3一定可以顺利的执行完，并归还资源。可把P1、P3先加入安全序列。

（2,0,0)+（2,1,1)+（3,3,2）=（7,4,3）



假设系统中有n个进程，m种资源

每个进程在运行前先声明对各种资源的最大需求数，则可用一个n\*m的矩阵（可用二维数组实现）表示所有进程对各种资源的最大需求数。不妨称为最大需求矩阵Max，Max[i,j]=K表示进程Pi最多需要K个资源Rj。同理，系统可以用一个n*m的分配矩阵Allocation表示对所有进程的资源分配情况。Max-Allocation=Need矩阵，表示各进程最多还需要多少各类资源。另外，还要用一个长度为m的一维数组Available表示当前系统中还有多少可用资源。

某进程Pi向系统申请资源，可用一个长度为m的一维数组Request表示本次申请的各种资源量。

Available=(1,2,1)   Request0=（2，1，1）

<table><tr><td>进程</td><td>最大需求Max矩阵</td><td>已分配Allocation矩阵</td><td>最多还需要Need矩阵</td></tr><tr><td>P0</td><td>(7,5,3)</td><td>(2,2,1)</td><td>(5,3,2)</td></tr><tr><td>P1</td><td>(3,2,2)</td><td>(2,0,0)</td><td>(1,2,2)</td></tr><tr><td>P2</td><td>(9,0,2)</td><td>(3,0,2)</td><td>(6,0,0)</td></tr><tr><td>P3</td><td>(2,2,2)</td><td>(2,1,1)</td><td>(0,1,1)</td></tr><tr><td>P4</td><td>(4,3,3)</td><td>(0,0,2)</td><td>(4,3,1)</td></tr></table>



可用银行家算法预判本次分配是否会导致系统进入不安全状态：

①如果$Request_i[j]<Need[i，j] \left(0 \le j \le m \right)$向②：否则认为出错。（因为它所需要的资源数已超过它所宣布的最大值）

②如果$Request_i[j]<Available[j](0 \le j \le m)$，便转向③：否则表示尚无足够资源，Pi必须等待。

③ 系统试探着把资源分配给进程Pi，并修改相应的数据（并非真的分配，修改数值只是为了做预判）：

Available $=$ Available-Request; 

$Allocation[i ,j] = Allocation[i,j]+ Request_i[j]$

$Need[i,j ] = Need[i,j]-Request_i[j]$

④操作系统执行安全性算法，检查此次资源分配后，系统是否处于安全状态。若安全，才正式分配：否则，恢复相应数据，让进程阻塞等待。





数据结构：

长度为m的一维数组Available表示还有多少可用资源

n*m矩阵Max表示各进程对资源的最大需求数

n*m矩阵Allocation表示已经给各进程分配了多少资源

Max-Allocation $=$ Need矩阵表示各进程最多还需要多少资源

用长度为m的一位数组Request表示进程此次申请的各种资源数

银行家算法步骤：

①检查此次申请是否超过了之前声明的最大需求数  
②检查此时系统剩余的可用资源是否还能满足这次请求   
③试探着分配，更改各数据结构  
④用安全性算法检查此次分配是否会导致系统进入不安全状态

安全性算法步骤：

检查当前的剩余可用资源是否能满足某个进程的最大需求，如果可以，就把该进程加入安全序列，并把该进程持有的资源全部回收。 

鸵鸟算法

采用假脱机打印技术（SPOOLing）可以允许多个进程同时产生输出



预防死锁

死锁避免/死锁检测与恢复

- 破坏互斥条件
- 破坏请求和保持条件（占有和等待条件）
- 破坏不可抢占条件
- 破坏环路等待条件



破坏互斥条件

假脱机技术

![image-20260419205554850](https://chenalna.oss-cn-hangzhou.aliyuncs.com/img/image-20260419205554850.png)

操作系统采用SPOOLing技术把独占设备在逻辑上改造成共享设备

在各进程看来，自己对打印机资源的使用请求立即被接受处理了，不需要再阻塞等待在磁盘上开一个缓冲区实现共享

![image-20260419205620541](https://chenalna.oss-cn-hangzhou.aliyuncs.com/img/image-20260419205620541.png)

破坏请求和保持条件

规定所有进程在开始执行前请求所需的全部资源

如果所需的全部资源可用，那么进程肯定可以运行结束

如果有一个或者多个资源正被使用那么就不进行分配，进程等待

当一个进程请求资源时，先暂停释放其当前占用的所有资源

请求和保持条件：进程已经保持了至少一个资源，但又提出了新的资源请求，而该资源又被其他进程占有，此时请求进程被阻塞，但又对自己已有的资源保持不放。

可以采用静态分配方法，即进程在运行前一次申请完它所需要的全部资源，在它的资源未满足前，不让它投入运行。一旦投入运行后，这些资源就一直归它所有，该进程就不会再请求别的任何资源

该策略实现起来简单，但也有明显的缺点

有些资源可能只需要用很短的时间，因此如果进程的整个运行期间都一直保持着所有资源，就会造成严重的资源浪费，资源利用率极低。另外，该策略也有可能导致某些进程饥饿。

```mermaid
graph LR
    subgraph 左
        A[A类进程] --> B(资源1)
        C[B类进程] --> D(资源2)
    end
    subgraph 右
        E[C类进程]
    end
    E --> B
    E --> D
```



破坏不剥夺条件

破坏不可抢占条件

将设备虚拟化，避免发生混乱 （打印机）

假设一个进程已经分配到了一台打印机，且正在打印输出。

如果它需要的绘图仪无法获得，而强制性的把它占有的打印机抢占掉，会引起一片混乱。

不过，并不是所有的资源都可以进行类似的虚拟化。

例如数据库中的记录是必须被锁定的，因此还是会出现死锁的可能



不剥夺条件：进程所获得的资源在未使用完之前，不能由其他进程强行夺走，只能主动释放。

破坏不剥夺条件：

方案一：当某个进程请求新的资源得不到满足时，它必须立即释放保持的所有资源，待以后需要时再重新申请。也就是说，即使某些资源尚未使用完，也需要主动释放，从而破坏了不可剥夺条件。

方案二：当某个进程需要的资源被其他进程所占有的时候，可以由操作系统协助，将想要的资源强行剥夺。这种方式一般需要考虑各进程的优先级（比如：剥夺调度方式，就是将处理机资源强行剥夺给优先级更高的进程使用）

该策略的缺点：

1.实现起来比较复杂。  
2.释放已获得的资源可能造成前一阶段工作的失效。因此这种方法一般只适用于易保存和恢复状态的资源，如CPU。  
3.反复地申请和释放资源会增加系统开销，降低系统吞吐量。  
4.若采用方案一，意味着只要暂时得不到某个资源，之前获得的那些资源就都需要放弃，以后再重新申请。如果一直发生这样的情况，就会导致进程饥饿。





## 破坏环路等待

当一个进程请求资源时，先暂停释放其当前占用的所有资源然后再尝试一次获得所需的全部资源。

如果进程正在把一个大文件从磁带机读入并送至打印机，那么这个限制是不可接受的。

将所有资源统一编号，进程可以在任意时刻提出资源请求，但是所有请求必须按照资源编号的顺序取出。



循环等待条件：存在一种进程资源的循环等待链，链中的每一个进程已获得的资源同时被下一个进程所请求。

可采用按顺序资源分配法，首先给系统中的资源编号，规定每个进程必须按编号递增的顺序请求资源，同类资源（即编号相同的资源）一次申请完。

原理分析：一个进程只有已占有小编号的资源时，才有资格申请更大编号的资源。按此规则，已持有大编号资源的进程不可能逆向地回来申请小编号的资源，从而就不会产生循环等待的现象。



假设系统中共有10个资源，编号为1,2.....10

P1：1、3号

P2：2、4号

P3：5、7号

在任何一个时刻，总有一个进程拥有的资源编号是最大的，拿这个进程申请之后的资源必然畅通无阻，不可能出现所有进程都阻塞的死锁现象。



该策略的缺点：  
1.不方便增加新的设备，因为可能需要重新分配所有的编号：  
2.进程实际使用资源的顺序可能和编号递增顺序不一致，会导致资源浪费：  
3.必须按规定次序申请资源，用户编程麻烦。

死锁的检测和恢复

允许死锁发生，当检测到死锁发生后，采取措施进行恢复

资源分配图

假设一个系统包含7个进程(A~G)，6种资源(R-W)

5.进程E持有T资源，且需要V资源

2.进程B没有持有资源，但需要T资源

6.进程F持有W资源，且需要S资源

3.进程C没有持有资源，但需要S资源

7.进程G持有V资源，且需要U资源

4.进程D持有U资源，且需要S和T资源

1.进程A持有R资源，且需要S资源



![image-20260419210039498](https://chenalna.oss-cn-hangzhou.aliyuncs.com/img/image-20260419210039498.png)



死锁恢复

利用抢占恢复

抢占是否可行，取决于资源的类型

利用回滚恢复

周期性的对进程进行检查点检查。

检查点检查就是把进程的状态写入一个文件以备以后重启

通过杀死进程恢复



1、撤销进程  
2、抢占资源

​	1、利用抢占恢复  
​	2、利用回滚恢复

死锁的数学公式

1. 死锁条件公式推导

系统参数：

N：进程数

W：每个进程最大资源需求

M：系统资源总数

最小安全资源数： $\mathsf { M } \geq \mathsf { N } \times ( \mathsf { W } - 1 ) + 1$

逻辑证明：

每个进程获得(W−1)个资源（共占用 N×(W−1)个）

系统至少剩1个资源

该剩余资源可分配给任一进程使其完成

完成进程释放W个资源，保障其他进程执行



# 管程

和之前的PV操作一样，实现进程间的互斥和同步

1、局部于管程的共享数据结构  
2、 对于该数据结构进行操作的一组过程  
3、对于局部于共享数据设置初始值的语句

4、管程的名字

基本特征：

1、局部于管程的数据结构只能被管城内的过程访问，任何外部过程不能访问，而管程内的过程也只能访问该管程内的数据结构  
2、一个进程若想访问管程内的数据结构（共享资源）们只能通过调用管程内地某个过程实现间接访问  
3、任意时刻，管程中只能有一个进程在管程中执行管程的某个过程，其它任何调用管程的进程都将被阻塞，直到管程变成可用，这一特性使管程能有效地实现互斥

引入管程的目的无非就是要更方便地实现进程互斥和同步。

1. 需要在管程中定义共享数据（如生产者消费者问题的缓冲区）  

2. 需要在管程中定义用于访问这些共享数据的“入口”一一其实就是一些函数（如生产者消费者问题中，可以定义一个函数用于将产品放入缓冲区，再定义一个函数用于从缓冲区取出产品）

3. 只有通过这些特定的“入口”才能访问共享数据

4. 管程中有很多入口，但是每次只能开放其中一个“入口”，并且只能让一个进程或线程进入（如生产者消费者问题中，各进程需要互斥地访问共享缓冲区。管程的这种特性即可保证一个时间段内最多只会有一个进程在访问缓冲区。注意：这种互斥特性是由编译器负责实现的，程序员不用关心）

5. 可在管程中设置条件变量及等待/唤醒操作以解决同步问题。可以让一个进程或线程在条件变量等待在条件变量上的进程或线程唤醒。（此时，该进程应先释放管程的使用权，也就是让出“入口”）；通过唤醒操作将等待在条件变量上的进程或线程唤醒。

程序员可以用某种特殊的语法定义一个管程（比如monitor Producer Consumer..endmonitor;），之后其他程序员就可以使用这个管程提供的特定“入口”很方便地使用实现进程同步/互斥了。



管程的基本思想是：把信号量和操作原语封装在一个对象的内部

也就是把共享变量和共享变量能够进行的所有操作集中在一个模块中

管程是一个由过程、变量及数据结构等组成的一个集合，它们组成了一个特殊的模块

```
monitor example

	integer i;

	condition c;

	procedure producer();

	end;

	procedure consumer();

	end;

end monitor;
```



进程可以在任何需要的时候调用管程中的过程;

管程结构确保每次只有一个进程在管程内处于活动状态。

管程是一个编程语言的概念。

编译器必须要识别管程并用某种方式对其互斥做出安排

因此，管程比信号量更容易保证并行编程的正确性

![image-20260419232116644](https://chenalna.oss-cn-hangzhou.aliyuncs.com/img/image-20260419232116644.png)

管程里的 wait 和 signal 操作和信号量的 wait 和 signal 并不一样(P/V wait/signal)

condition x;

 对于条件变量x，只有操作wait()和signal()可以调用

对于操作x.wait() 调用这一操作的进程会被挂起 P(x.wait())

一直到另外一个进程调用x.signal()

如果没有挂起进程，那么操作signal就没有作用，也就是说x的状态态如同没有执行任何操作