第六章 同步
协作进程可用于系统内的其他执行进程相互影响,或者能直接共享逻辑地址空间(即代码和数据),或者能通过文件或消息来共享数据。而共享数据的并发访问可能导致数据的不一致。
6.1 背景
进程可并发或者并行的执行。可能会产生一些问题,比如两个进程并发操作一个变量counter。
即多个进程并发访问和操作同一个数据并且执行的结果与特定的访问顺序有关,称为竞争条件。为了防止竞争条件,需要确保一次只有一个进程可操作变量counter。为了做出这种保证,要求这些进程按照一定方式来同步。也就是按照一定顺序来执行。
本章大多数都是讨论,写作进程如何进行进程同步和进程协作
6.2 临界区问题
假设某个系统有n个进程{p1,p2......pn}每个进程有一段代码,叫做临界区,进程在执行临界区时可能会修改公共变量。当一个进程在临界区内执行时,其他进程不允许在它们的临界区内执行。即没有两个进程可以在它们的临界区共同执行。
临界区问题是,设计一个协议来协作进程,在进入临界区之前,每个进程应请求许可。实现这一请求的代码区段叫做进入去。临界区之后可用有退出区,剩下的是剩余区。
临界区问题的解决方法需要满足以下三个要求:
互斥:一个进程在其临界区内执行,其他进程都不能在其临界区内执行
进步:如果没有进程在其临界区内执行,并且有进程需要进入临界区,那么只有哪些不在剩余区内执行的进程可参加选择,以便确定谁能下次进入临界区,而且这种选择不能无限推迟。也就是说如果临界区为空,需要进入临界区的进程可以被选择进入临界区,同时不能产生死锁。
有限等待:从一个进程做出进入临界区的请求到这个请求允许位置,其他进程允许进入其临界区的次数具有上限,如果超出上线就允许新进程执行。也就是等待时间有限制,不能永久阻塞。
有两种常用方法,用于处理操作系统临界区问题:抢占式内核和非抢占式内核。
抢占式内核允许处于内核模式的进程被抢占
非抢占式内核不允许处于内核模式的进程被抢占。处于内核模式运行的进程会一直运行,直到退出、阻塞或资源放弃CPU控制。
这里说的是单核CPU,在单核CPU上的非抢占内核往往可避免大多数的静态条件,因为任意时间点只有一个进程处于内核模式。
6.4 硬件同步
基于加锁为前提,即通过锁来保护临界区。通过硬件指令来解决临界区问题。
6.5 互斥锁
临界区问题基于硬件的解决方案不但复杂,而且不能为程序员直接使用。因此操作系统设计人员构建软件工具,来解决临界区问题。最简单的工具就是使用互斥锁来保护临界区,以防止竞争条件。
一个进程在进入临界区时应得到锁,在退出临界区时释放锁。
函数acquire()获取锁,release()释放锁

忙等待:当一个进程在临界区中,任何其他进程在进入临界区时必须不断循环判断。这种互斥锁也被称为自旋锁,进程不断的选择,以等待锁变得可用。
忙等待浪费CPU周期,优点是没有切换上下文。
6.6 信号量
一个信号量S是一个整型变量。它除了初始化之外只能通过两个标准原子操作:wait()和signal()来访问。
操作wait()和signal()来访问。操作wait()最初被称为P(proberen,荷兰语中表示测试)原语(原子性语言),测试signal()是V(verhogen,荷兰语表示增加)原语
可按如下来定义wait()
测试时进行忙等待并减小信号量

用如下定义signal()
增加信号量

在wait和signal执行过程中信号量整数值的修改被不可分割的执行。当一个进程修改信号量值时,没有其他进程可用同时修改同一个信号量的值。
此外,对于wait(S),S整数值的测试(S<=0)和修改(S--)也不能被中断。
6.6.1 信号量的使用
操作系统通常区分计数信号量与二进制信号量
计数信号量的值不受限制,二进制的信号量只能是0/1.因此二进制信号量类似于互斥锁。
计数信号量可用用于控制访问具有多个实例的某种资源。信号量的初始值为可用资源数量。例如:可用打印机数量。
当进程需要使用资源是,需要对信号量执行wait()操作(减少信号量计数)
当进程释放资源时,需要对该信号量执行signal()操作(增加信号量的计数)当信号量的计数为0时,所有资源都在是一种。之后需要使用资源的进程被阻塞,直到计数大于0.
6.6.3 死锁与饥饿
具有等待队列的信号量实现可能导致:两个或多个进程无限等待一个时间,而该事件只能由这些等待进程之一来产生。当出现这样的状态时,这些进程就是死锁。
假设有一个系统,有两个进程P0P1,访问共享信号量S和Q,这两个信号量的初始值为1:
P0先占用信号量S,P1占用信号量Q,P0需要先等待P1释放Q占用Q后再释放信号量,相同的P1也在等待S然后才能释放Q,就导致了两个进程无限等待的死锁。

与死锁相关的另一个问题是无限阻塞或饥饿,即进程无限等待信号量
6.6.4 优先级的反转
如果一个较高优先级的进程需要读取或修改内核数据,而且该内核数据正在被低优先级进程访问(也可能涉及更多进程)那么就会出现一个调度挑战。
由于内核数据通常是用锁保护的,较高优先级的进程不得不等待较低优先级的进程用完资源。
如果较低优先级的进程被较高优先级的进程抢占,那么情况就会变得复杂。
假设有三个进程LMH,优先级顺序为L<M<H。H需要资源R,但R正在被L访问。
通常H将等待L用完R。但是假如进程M进入可运行状态并抢占了L。间接的具有较低优先级的M影响了H应等待多久才会使得L释放资源R
这个问题叫做优先级反转。只出现在用于两个以上优先级的系统中,一个解决方案是只采用两个优先级,但是对于大多数操作系统不可行。
这些系统再解决这个问题采用有限继承协议。根据这个协议,所有正在访问资源的进程需要访问它的更高优先级进程的优先级,直到它们用完了有关资源为止。当他们用完时,它们的优先级恢复到原始值。在上面的例子中,优先级基础协议先允许L临时继承H的优先级,从而防止进程M抢占。当L释放R后,就会放弃继承的优先级并采用原来的优先级。因为现在资源R可用,进程H就会接下来执行而不是进程M。
6.7 经典同步问题
使用信号量来解决同步的问题
有界缓冲问题
读写问题
哲学家就餐问题
6.7.1 有界缓冲问题
假设生产者和消费者共享以下数据结构

假设缓冲池有n个缓冲区,每个缓冲区可存一个数据项。信号量mutex提供缓冲池访问的互斥要求,并初始化为1.信号量empty和full分别表示空和满的缓冲区数量。信号量empty初始化为n信号量full初始化为0
生产者进程结构如下:

消费者:

6.7.3 哲学家就餐问题
哲学家只做两件事思考和吃饭。饥饿时需要拿起两个相邻的叉子才能进餐,但是一个哲学家一次只能拿一个叉子,吃完后会放下两个叉子。

这些叉子是共享资源,也就是竞争条件。
如果发生死锁,所有科学家都会饿死。
解决方案:每个叉子都用一个信号量来表示。一个哲学家通过执行wait()操作试图获取相应的叉子,他会通过执行signal()操作以释放相应的叉子。因此,共享数据为semaphore fork[5] fork的所有元素初始化为1。
用0-4为哲学家及叉子编号:

这种方式可能会造成死锁。
补救措施:
最多允许4个哲学家同时坐在桌子上
只有一个哲学家的两个叉子都可用时,才能拿起它们
使用非对称解决方案,即单号的哲学家会先拿起左边的叉子,接着右边的叉子。双号的哲学家先拿起右边的叉子,接着左边的叉子
解决方案应保证:没有一个哲学家会欸四。没有死锁的解决方案不一定能消除饥饿的可能性。
第七章 死锁
在多道程序环境中,多个进程可用竞争有限数量的资源。当一个进程申请资源时,如果这时没有可用资源,那么这个进程进入等待状态。有时,如果所申请的资源被其他等待进程占用,那么该等待进程可能就再也无法改变其状态。这种情况被称为死锁。
7.1 系统模型
系统用于有限数量的资源,需要分配到多个竞争进程。即多进程对共享资源的竞争可能导致死锁。
在正常模式下,进程只能按照如下顺序使用资源:
申请:进程请求资源。如果申请不能被允许,那么申请进程应等待,直到它能获取资源为止
使用:进程对资源进行操作
释放:进程释放资源
7.2死锁特征
被四所示,进程永远无法完成,系统资源被阻塞使用,以至于阻止了其他作业的开始执行。
如果在一个系统中以下四个条件同时成立,那么就能引起死锁:
互斥:至少有一个资源必须处于非共享模式,即一次只有一个进程可使用。如果另一进程申请该资源,那么申请进程应等到该资源被释放为止。
占有并等待:一个进程应占用至少一个资源,并等待另一个资源,而该资源被其他进程占有。
非抢占:资源不能被抢占,即资源只能被进程在完成任务后自愿释放
循环等待:有一组等待进程{p0,p1......pn}P0等待的资源为P1占用,P1等待的资源为P2占用,P2等待的资源被P3占用......
存在这些条件也不一定产生死锁,只是有可能会产生死锁。所有的四个条件必须同时成立才可能出现死锁。
循环等待条件意味着占用并等待条件,这四个条件并不完全独立。
7.4 死锁预防
发生死锁有四个必要条件,只要有一个不成立,就能预防死锁发生
7.4.1 互斥
互斥条件必须成立。也就是说,对共享资源(可能被同时访问的资源)的互斥(但是不可以同时访问)申请。相反,可共享资源(可以同时访问的资源)不要求互斥访问,因此不会死锁。比如,只读文件。
7.4.2 持有且等待
为了确保持有并等待条件不会出现在系统中,应保证:当每个进程申请一个资源时,它不能占有其他资源。
一种可以采用的协议是,每个进程在执行前申请并获得所有资源。这可以这样实现:要求进程申请资源的系统调用在所有其他系统调用之前进行。
另外一种协议允许进程仅在没有资源时才可申请资源。一个进程可申请一些资源并使用它们。然而,在他申请更多其他资源之前,它应释放现已分配的所有资源。
这两个协议有两个主要缺点:
第一:资源利用率低,许多资源可能已分配,但是长时间没有被使用
第二,可能发生饥饿,一个进程如果需要多个常用资源,可能必须永久等待,因为在它所需要的资源中,至少有一个已分配给其他进程。
7.4.3 无抢占
不能抢占已分配的资源。为了确保这一条件不成立,可以使用以下协议:如果一个进程持有资源并申请另一个不能立即分配的资源(也就是说整个进程应等待),那么它现在分配的资源都可以被抢占。
换句话说就是,这些资源都被隐式释放了。被抢占的资源添加到进程等待的资源列表上,只有当进程获得其原有资源和申请的新资源时,他才可以重新执行。
如果应该进程申请一些资源,首先检查它们是否可用。如果可用,那么就分配它们。如果不可用,那么检查这些资源是否已分配给等待额外资源的其他进程。如果是,那么从等待资源中抢占这些资源,并分配给申请进程。如果资源不可以且也不被其他等待进程持有,那么申请进程应等待。当一个进程处于等待时,如果其他进程申请其拥有的资源,那么该进程的部分资源可被抢占。只有当应该进程分配到申请的资源,并且恢复在等待时被抢占的资源时,它才能重新执行。这个协议通常用于状态可保存和恢复的资源,如CPU寄存器和内存。一般不适用于其他资源如互斥锁和信号量。
7.4.4 循环等待
确保整个条件不成立的方法是:对所有资源类型进程完全排序,且要求每个进程按照增序顺序来申请资源。
7.5 死锁避免
死锁避免要求操作系统事先得到有关进程申请资源和使用资源的额外信息。有了这些额外信息,系统可以判定:对于一个请求,进程是否应该等待。
为了判断当前请求是否能够满足或是否应该延迟,系统必须考虑:当前可用资源、已分配给进程的资源,以及进程将来申请和释放的资源。
7.5.1 安全状态
系统状态分为安全状态和不安全状态。
安全状态: 系统能够按某个顺序来为每个进程分配资源(不超过可用资源),并能避免死锁。更准确地说,如果系统能找出一个安全序列,那么系统处于安全状态。
安全序列: 对于进程序列 {P0, P1, ..., Pn},如果对于每个 Pi,它所需要的资源可以由当前可用资源加上所有进程 Pj(j < i)所拥有的资源来满足。如果 Pi 所需资源不能立即得到,那么 Pi 可等到所有 Pj 完成。当 Pj 完成时,Pi 可得到所需资源,完成任务,返还分配的资源,并终止。
重要结论:
- 如果系统处于安全状态 → 没有死锁
- 如果系统处于不安全状态 → 可能死锁
- 避免死锁 → 确保系统永远不进入不安全状态
7.5.2 资源分配图算法
如果资源类型只有单个实例,那么可以采用资源分配图的变形来进行死锁避免。
除了前述的请求边和分配边外,还引入了一种新型的边,称为需求边(claim edge)。需求边 Pi → Rj 表示进程 Pi 可能在将来某个时候申请资源 Rj。
算法规则:
- 进程开始执行前,所有需求边必须出现在资源分配图中
- 需求边是虚线表示
- 当进程申请资源时,需求边转换为请求边
- 当资源分配给进程时,请求边转换为分配边
- 当进程释放资源时,分配边重新转换为需求边
死锁避免检查:
假定进程 Pi 申请资源 Rj。只有在将请求边 Pi → Rj 转换为分配边 Rj → Pi 不会导致资源分配图形成环时,才允许申请。如果没有环,那么分配不会导致系统进入不安全状态。如果有环,那么分配会导致系统进入不安全状态,因此拒绝申请。
7.5.3 银行家算法
资源分配图算法不适用于资源有多个实例的情况。银行家算法可用于多个实例。
核心数据:
系统需要跟踪每个进程的最大资源需求、当前已分配的资源、还需要的资源,以及系统当前可用的资源数量。
7.5.3.1 安全性算法
算法逻辑:
尝试找到一个进程执行序列,使得每个进程都能完成。具体做法是:不断寻找一个进程,它当前请求的资源能被当前可用资源满足。假设这个进程完成后会释放所有资源,更新可用资源数量,继续寻找下一个可完成的进程。如果所有进程都能按某个顺序完成,系统就是安全的。
7.5.3.2 资源请求算法
处理逻辑:
当进程请求资源时,首先检查请求是否超过其声明的最大需求,以及系统是否有足够的可用资源。如果都满足,则试探性地分配资源,然后运行安全性算法检查新状态是否安全。若安全则真正分配;若不安全则拒绝请求,进程需要等待。
7.6 死锁检测
如果系统不采用死锁预防或死锁避免算法,那么死锁可能会发生。在这种情况下,系统应提供:
- 一个检查系统状态以确定死锁是否发生的算法
- 一个从死锁状态中恢复的算法
7.6.1 单个资源类型实例
如果所有资源只有单个实例,那么可以定义死锁检测算法,该算法采用了资源分配图的变形,称为等待图。
等待图: 从资源分配图中去掉资源节点,并合并相应的边而获得。
构造规则: 如果资源分配图中存在 Pi → Rq 和 Rq → Pj 两条边,那么在等待图中就有边 Pi → Pj。
死锁检测: 系统处于死锁状态,当且仅当等待图有环。为了检测死锁,系统需要维护等待图,并周期性地调用在图中搜索环的算法。
7.6.2 多个资源类型实例
等待图不适用于资源类型有多个实例的情况。
检测算法逻辑:
算法模拟进程的执行过程。系统记录每个进程当前已分配的资源和正在请求的资源,以及系统当前可用的资源。检测从可用资源开始,寻找一个进程,它当前的请求能被满足。假设该进程完成后释放所有资源,更新可用资源数量。继续寻找下一个可满足的进程。如果最终所有进程都能完成,说明无死锁;如果存在进程无法完成,这些进程就处于死锁状态。
与银行家算法的区别:
银行家算法检查的是"如果满足所有进程的最大需求,是否都能完成";死锁检测算法检查的是"根据当前的实际请求,是否都能完成"。前者是预测性的,后者是检查当前状态。
7.6.3 检测算法的使用
何时调用检测算法? 取决于两个因素:
- 死锁多久发生一次?
- 当死锁发生时,有多少进程会受到影响?
调用频率:
- 每当有资源请求不能立即满足时就调用检测算法。这样可以尽早识别出引起死锁的特定进程。但这种频繁调用会带来相当大的开销。
- 较不频繁的调用,如每小时一次,或只在 CPU 使用率降到 40% 以下时调用。
如果死锁确实发生,那么可能会有许多进程卷入死锁。这些进程累积资源的时间越长,卷入死锁的进程就可能越多。
7.7 死锁恢复
当检测算法确定死锁存在时,有几种可选方案。一种是通知操作员死锁已发生,由操作员手工处理死锁。另一种是让系统自动从死锁中恢复。
打破死锁的两种选择:
- 中止一个或多个进程以打破循环等待
- 从一个或多个死锁进程处抢占一个或多个资源
7.7.1 进程终止
两种方法:
1. 中止所有死锁进程
- 这种方法明显会打破死锁循环
- 代价很大,这些进程可能已计算了很长时间,结果现在必须丢弃,以后还要重新计算
2. 一次中止一个进程直到取消死锁循环为止
- 这种方法会带来相当大的开销,因为在中止每个进程之后,必须调用死锁检测算法以确定是否还有进程处于死锁状态
中止进程顺序的选择因素:
在确定应该中止哪个进程时,应根据如下因素确定中止顺序:
- 进程的优先级
- 进程已运行时间,以及进程还需多少时间才能完成
- 进程已占用资源的类型和数量
- 进程还需要多少资源才能完成
- 有多少进程需要被终止
- 进程是交互的还是批处理的
7.7.2 资源抢占
为了通过抢占来消除死锁,需要逐步从进程那里抢占资源并分配给其他进程,直到打破死锁循环为止。
需要考虑三个问题:
1. 选择牺牲品(Selecting a victim)
- 应抢占哪些资源、哪些进程?
- 与进程终止一样,应根据代价确定抢占顺序
- 代价因素可能包括:进程所拥有资源的数量、进程至今所消耗的时间等
2. 回滚(Rollback)
- 如果从进程那里抢占资源,应该怎样处理这个进程?
- 显然,它不能继续正常执行,因为它缺少某些所需资源
- 应将进程回滚到某个安全状态,并从该状态重新启动进程
- 确定安全状态并非易事
- 最简单的解决方法是完全回滚:中止进程并重新启动它
- 然而,更有效的方法是将进程回滚到足以打破死锁的程度。这种方法需要系统保存更多的有关所有运行进程的状态信息
3. 饥饿(Starvation)
- 如何确保饥饿不会发生?
- 即如何确保资源不会总是从同一个进程那里被抢占?
- 在基于代价因素的系统中,可能总是选择同一个进程作为牺牲品
- 结果,这个进程永远也不能完成其任务
- 必须确保一个进程只能被选作牺牲品有限的次数
- 最常用的方法是:在代价因素中加上回滚次数