第十七讲:同步互斥
17.1 背景
- 并发进程的正确性
- 进程并发执行的好处
- 并发中的错误情况举例
- 原子操作
需要在并发执行的同时,保证一些操作是原子的!!!
17.2 现实生活中的同步问题
-
买面包举例
…略,见视频:现实生活中的同步问题 -
进程的交互关系
关注表中第二行,进程间资源共享带来的问题:
互斥:一个进程占用资源,其他进程不能使用;
死锁:多个进程各占用部分资源,形成循环等待;
饥饿:其他进程可能轮流占用资源,而某个进程可能一直得不到资源(比如优先级过低的进程)
17.3 临界区及同步实现方式
17.3.1 临界区
-
临界区
临界区:进程访问临界资源(这个资源必须是多个进程共享的)的一段互斥执行的代码,每个进程有自己的临界区
进入区:检查可否进入临界区的条件对应的代码(如可进入,设置正在访问临界区标志);
退出区:清除“正在访问临界区”的标志;
剩余区:进程剩下的代码,与同步互斥无关… -
临界区的访问规则
空闲则入:没有进程在临界区时,任何进程可进入;
忙则等待:有进程在其临界区时,其他进程不能进入临界区;
有限等待:等待进入临界区的进程不能无限等待;
让权等待(可选):不能进入临界区的进程,需要释放CPU(如进入阻塞状态)
17.3.2 禁用硬件中断实现同步
- 禁止硬件中断
- 缺点
仅在不得不用的时候才使用关中断
!
17.3.3 基于软件的同步
- 软件同步原理
=> 在进程的进入区和退出区修改共享变量来实现同步!
- 方法一:不可行
- 方法二:不可行
不满足忙则等待 => 可能两个进程同时判断flag[i]/flag[j],结果两个都满足,同时进入临界区!
- 方法三:不可行
不满足空闲则入 => 可能两个进程同时修改flag[i]/flag[j],结果循环判断时,两个进程都不能进入临界区!
- Peterson算法
- Dekkers算法
可扩展到多个进程
- N进程的软件同步方法
- 基于软件的同步方法分析
复杂:需要多个进程间共享数据项
需要忙等待:浪费CPU时间
17.3.4 高级抽象的同步方法(锁)
- 概述
- 锁
锁是一个抽象的数据结构
用一个二进制变量表示锁定/解锁,它的内部基于原子操作指令;
Lock::Acquire()
锁被释放前其他进程一直处于等待状态,Acquire返回后某个得到锁;
Lock::Release()
释放锁,唤醒任何等待该锁的进程;
- 原子操作指令
重点关注:TS指令和exchange指令
- 使用TS指令实现自旋锁(spinlock)
- TS指令实现无忙等待锁
通过交换指令也可以实现
- 基于原子操作指令的锁的特征
对比:中断禁用只适用于一个处理机(关中断只能禁止当前处理机?)
第十八讲:信号量与管程
18.1 信号量
-
回顾 & 概述
信号量与管程属于 => 高层次的编程抽象
信号量与锁是并列的高层抽象
-
信号量
信号量与软件同步的区别
软件同步:各进程间是平等的,他们遵守同一个规则进行同步,没有仲裁者;
信号量同步:此时操作系统是作为一个仲裁者!
-
信号量的特性
自旋锁不能实现先进先出 => 它没有组织需要资源的进程进行排队。锁释放后,哪个进程请求锁,则能先获得资源。 -
信号量的实现
信号量的P、V操作由操作系统来保证原子性!
18.2 信号量的使用
-
信号量分类
互斥可以理解为特殊的同步
-
信号量实现临界区的互斥访问
使用二进制信号量
-
信号量实现条件同步
使用资源信号量
-
存在困难
只有在写程序时尽量避免死锁,执行过程中无法处理死锁问题! -
例:生产者-消费者问题
见18.4.1
18.3 管程
-
回顾 & 概述
-
管程
管程可中途放弃,而临界区方法不能中途退出临界区 -
管程的组成(4个部分)
如下面的图中所示,管程主要由四个部分组成:1.共享变量; 2.条件变量; 3.并发执行的进程代码; 4.初始化共享变量的代码
-
条件变量
-
条件变量的实现
类似于信号量
-
例:管程解决生产者-消费者问题
见18.4.1 -
管程条件变量的释放处理方式
前者少了一个切换,更加高效;后者从优先级来说更加合理;
以生产者-消费者为例,对应代码实现(对比18.4.1):
18.4 经典进程同步
18.4.1 生产者-消费者问题
- 问题描述
- 问题分析
- 代码实现:信号量方式
P、V操作不能交换,且必须先检查空/满,然后再申请和占用缓冲区 - 代码实现:管程方式
与信号量方式相比:
信号量必须将互斥访问紧挨着临界区(即mutex->P、V);
管程却可以将互斥操作直接放在管程代码的外部(lock->Acquire);
这是因为管程可以中途退出,而信号量确不能中途退出临界区!!!
18.4.2 读者-写者问题
-
问题描述
-
信号量解决读者-写者问题
优先策略
-
管程解决读者-写者问题
解决方案:读者
(写者优先)
解决方案:写者
(写者优先)
18.4.3 哲学家就餐问题
-
问题描述
-
方案1
-
方案2
-
方案3
18.5 个人补充:信号量与管程的区别
- 管程是为了解决信号量在临界区的PV操作上的配对的麻烦,把配对的PV操作集中在一起管理的方法,它使用了条件变量
第十九讲(实验7):同步互斥
19.1 总体介绍
19.2 底层支撑
- 定时器
- 屏蔽中断
- 等待队列
19.3 信号量设计实现
- 信号量原理
以P操作为例:
上面是sem的更改,由于关中断保证互斥性;下面是阻塞进程的代码,同样需要关中断
在ucore中,sem信号量的互斥性是由关中断机制保证的!
19.4 管程和条件变量设计与实现(难且重要)
-
管程的定义
管程包含的内容:1共享变量、2条件变量、3并发执行的进程代码、4初始化共享数据的代码
-
管程的数据结构
-
管程的实现
-
条件变量的定义
条件变量看起来与信号量几乎一样,但是在实现上有很大的不同!
由图可知,条件变量直接使用了信号量! -
条件变量的signal和wait操作
图中,上部分是原理,下部分是实现 => 可见,二者是有差异的!
-
管程和信号量的设计与实现
-
条件变量wait、signal原理与实现的对比
1.关于等待信号量的进程数量
2.关于等待队列
3.关于管程中互斥信号量的释放
与之对应的其实在另一个管程中,如下:
4.关于管程中互斥信号量的释放
-
管程中互斥信号量不会导致死锁和重入
-
执行过程分析
19.5 哲学家就餐问题(管程实现)
- 管程定义和初始化
- 哲学家线程
- 哲学家线程调用的管程操作
注意它类似于信号量,但是不同于信号量,主要区别就是下面标红的部分!
第二十讲:死锁和进程通信
20.1 死锁概念
-
死锁示例
-
进程访问资源的流程
-
资源分类
-
资源分配图
-
死锁的必要条件
互斥:任何时刻只能有一个进程使用一个资源实例(共享资源不可能死锁);
持有并等待:进程至少持有一个资源,并且正在等待其他进程持有的资源;
非抢占:资源只有在使用后自愿放弃,不可剥夺;
循环等待:等待进程的集合中存在循环
20.2 死锁处理方法
-
方法概述
死锁预防与死锁避免的区别
死锁预防:是设法至少破坏产生死锁的四个必要条件之一,严格的防止死锁的出现
死锁避免:在系统运行过程中注意避免死锁的最终发生,它不那么严格的限制产生死锁的必要条件的存在,因为即使死锁的必要条件存在,也不一定发生死锁。
-
死锁预防:限制资源申请方式
互斥:把互斥的共享资源封装成可同时使用的资源(互斥在资源内部实现)
持有并等待:进程请求资源时,要求它不持有任何其他资源;或者仅允许进程在开始执行时间,一次请求所有需要的资源; => 资源利用率低
非抢占:如果进程请求不能立即分配所有资源,则释放已占有的资源; 只在能够获得所有需要资源时,才执行分配资源操作
循环等待:对资源排序,要求进程按顺序请求资源 -
死锁避免
-
安全与死锁
20.3 银行家算法
- 银行家算法(死锁避免)
- 数据结构
- 安全状态判断
基本思路:如果能够找到一个安全序列,对于序列中的每个线程Ti,资源的需求量都小于当前可用的资源数量 => 按照这个序列执行,最终每个线程都能获得需要的资源,从而是安全的!
- 算法描述
- 示例
…略
20.4 死锁检测
-
概述
-
数据结构
-
检测算法
类似于死锁避免中算法的安全判断的算法!
分析算法的时间复杂度可知,开销较大 => 所以操作系统通常不管死锁 -
示例
…略 -
死锁检测算法的使用
-
死锁恢复:进程终止
终止进程的选择:
-
死锁恢复:资源抢占
20.5 进程通信
- 概述
- 通信方式
间接通信:利用内核,使用消息队列,可以多对多
直接通信:利用一片共享的内存区域,只能一对进程间通信
- 直接通信
- 间接通信
- 阻塞(同步)与非阻塞通信(异步)
1.阻塞通信方式
阻塞发送:发送者在发送消息后进入等待,知道接受者成功收到
阻塞接收:接收者在请求接受消息后等待,直到成功收到一个消息
2.非阻塞通信方式
非阻塞发送:发送者发送消息后,可立即进行其他操作
非阻塞接收:接收者在请求接收后没有收到任何消息(可能因为没有发送),不用等待,可直接进行其他操作 - 通信链路缓冲
20.6 信号和管道
- 信号
信号其实就是一个特定的中断 => 可结合csapp异常控制流理解…
- 信号的实现
- 信号使用示例
- 管道
1.管道就是多个进程共享的一个内存文件,发送方只需向管道写数据,而不用关心接收方是谁,接收方同理…
2.管道虽然说是“文件”,但是它不是文件系统/硬盘上的文件,它本质上只是内存中的一个缓冲区
3.管道是半双工的,数据只能向一个方向流动(仅针对匿名管道);
4.只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程)(仅针对匿名管道)
- 管道相关的系统调用
- 管道示例
20.7 消息队列和共享内存
- 消息队列
- 消息队列的系统调用
注意:消息队列是独立于创建它的进程,进程结束后,消息队列并不会自动删除,所以需要有单独的控制消息队列的系统调用!
- 共享内存
- 共享内存的实现
注意:需要由程序员提供同步(比如信号量机制)!!!
- 共享内存的系统调用