【操作系统】第十一章:死锁和进程间通信

在这里插入图片描述
一个进程需要一个被其他进程占用的资源,而另一个进程需要这一个进程已经拥有的资源。这种交叉就会产生所谓的死锁,谁都无法进一步执行。
死锁的原因是因为进程的并发执行导致的。共享资源如果不加以某种约束和规则,就容易出现死锁问题。

死锁

系统模型

死锁里有需求方(进程)和需要的资源。资源比较抽象,可以是CPU也可以是内存单元,也可以是一些IO外设,甚至是一个进程中的数据结构共享的变量。

资源个数有N个,进程的资源有几种访问情况:
1.需要资源:首先要确保有足够多的空闲资源,然后向空闲资源去申请。申请成功与否取决于空闲资源能否满足需求。
2.请求得到满足,拿到资源,资源从空闲状态变成被使用状态,一旦处于被使用状态,其他进程就无法在这个时间点使用该资源。如果
3.进程使用资源的时间一定是有限的,当进程用完资源之后需要释放资源。释放后的资源恢复到空闲状态。
在这里插入图片描述
在这里插入图片描述

资源的特征:
1.首先可重复使用【free-use-free】
当进程获得某个资源后,这个时间段内其他进程就不能使用了。没有特殊情况下,OS不应该对拥有资源的进程暴力处理。
2.进程获取资源后,使用一段时间就会被释放,只有这样才能让资源被多个进程重复地使用
3.资源的类型(包括具体的物理层资源)和很抽象的资源(文件,数据库,数据结构)都可以用资源的形式表示。如果出现某种情况,一个进程拥有一部分资源同时请求其他资源,且此时另一个进程也在进行同样的操作,就有可能出现死锁。因为两者都hold住了一部分资源。


资源的使用:
首先资源有创建和销毁的资源。比如内存,如果将内存看作资源,创建内存和释放内存就是对应的属性(由内存管理单元完成)。
I/O缓冲区的种种都是一种资源,使用完后需要释放,而且申请资源时有可能拿不到资源。
资源拿不到一般会处于等待状态,使用资源的过程中存在进程管理和调度的过程。

资源分配图

在这里插入图片描述

一个进程访问一个资源有一个link,这些边表示的两个点。第一个点的集合是进程集合,用大P表示;第二个点的集合是资源的集合,用R表示。资源的集合包含所有资源的类型。
申请关系:一个进程访问某个资源时,会在P和R之间设置一条有向边,来表明Pi需要Rj的资源。
分配关系:当一个资源分配给一个进程后,进程可以使用这个资源,用Ri到Pj来表示当前资源被这个进程使用。
两个不同集合之间的连接关系就可以形成一个图。
在这里插入图片描述
由于资源还有个数的问题,在这个例子中,资源的个数设置为4,意味着这个资源类型有四种、进程申请资源时可以指定这需要这种类型的资源多少个。资源指向进程表示当前资源有多少个分配给了所指进程。
在这里插入图片描述
分析:

序号 P R
1 拥有R2的一个资源,请求使用R1的一个资源 分配给P1和P2各一个资源
2 拥有R1的资源,请求使用R3的一个资源 分配给R2,导致P1的请求无法被满足,P1被阻塞;
3 拥有R3的资源 分配给了P3,导致P2的请求无法被满足,P2被阻塞
4 未使用 未使用

这个图里,第一阶段:P1和P2暂时拿不到资源,这些资源受到P3限制。由于【资源的特征】使用一段时间后会释放,也就意味着R3在一定时间段后就会释放,因为P3在执行且没有约束P3执行的,此后P2就被唤醒并执行,然后释放R1唤醒P1,继续执行。
第二阶段:增加一条边,就会发生死锁。令P3也指向R2,,就会出现死锁了。一旦出现死锁则资源分配图中一定有一个有向环。
因为P1会等待P2,P2等待P3,P3也需要等待P1。相互等待,进入死锁。这里的大环:是P1等P2,P2等P3,P3等P1;小环是P2等P3,P3等P2。
在这里插入图片描述
在看一个例子:
这里虽然有向图成环,但是因为这里有P2和P4且没有约束P2和P4执行的,所以一开始P3和P1进入相互等待状态是确实的。
【注意互相等待不是死锁!死锁是指无限互相等待无法继续执行,而这里只是一段时间内互相等待,然后可以正常执行。】
但是根据有限时间内必然释放资源这一特征,P2和P4执行完毕后退出会释放出R2和R1的资源,此时对于相互等待的两个进程而言,有了新加入的资源可以调用,导致不需要继续等待可以成功执行。也就说不会锁死,会处于一段时间的死锁状态。
即便R1和R2中的资源种类只有一个是两个,也就说允许该图中R1和R2的资源类只有1个实例,另一个至少为2,则该图不会锁死。(默认资源调用使用过1个资源,如果使用复数个资源仍然会进入死锁)

死锁一定会令资源分配图存在一个有向环;但是存在有向环不一定会出现死锁
资源分配图中每个资源类只有一个实例则出现死锁,如果每个资源类有几个实例则可能死锁


死锁的特征

在这里插入图片描述

互斥:出现死锁之后,在一个时间段内只能有一个进程使用资源。A进程使用资源则B进程绝不会在同一时刻使用该资源
持有并等待:进程至少有一个资源在等待其他进程持有的额外资源。
无抢占:资源一旦被某个进程持有,除非进程主动释放资源,否则其他进程无法抢占使用该资源
循环等待:进程和资源的连接关系形成了环。
只有这四个条件都满足,才有可能出现死锁。这是死锁的必要条件,而不是充分条件。
在这里插入图片描述
以此来分析上图。
左图:满足4个条件。右图:P2/P4持有但是并不等待,不会出现死锁

死锁处理

在这里插入图片描述
约束性:死锁预防最强,死锁恢复最弱。
:::::::预防>避免>检测>恢复
在这里插入图片描述

如果让检测随意申请和释放资源,难以避免会出现死锁,如果确保系统永远不进入死锁状态,那么对性能将会有非常大的限制,所以需要由一定的办法来处理死锁。同时,也有鸵鸟算法,假装没有死锁来进行处理,因为判断死锁出现和恢复死锁需要的开销比较大,且确保不出现死锁又会令系统性能大大受限,所以实际OS中也经常采用这种方法。

死锁预防

让死锁不会出现。死锁不会出现,也就意味着打破死锁出现的某一个条件就可以。前面所说的一旦出现死锁,4个必要条件一定存在,只要把其中的一个打破也就意味着死锁不会出现了。
在这里插入图片描述

1.互斥:把共享资源变得不互斥,操作起来比较麻烦,而且会导致产生一些进程执行不确定性
2.占用并等待:
不拿一部分资源,变成拿所有资源。我需要的资源要么全部拿到要么一点都不拿,确实不会出现死锁。但是这种情况下,一个进程执行过程中,可能不同阶段需要的资源数不多,但是整个过程中需要很多资源,占用所有资源并开始执行的话,其他进程长时间等待引发饥饿现象,系统的利用率会很低。
在这里插入图片描述
3.不抢占:为了可以抢占他人资源。因为资源是互斥的,所以无法抢占他人资源,这种情况下为了获取自身需要的资源,杀死现在持有所需资源的进程,暴力获取资源。手段过于暴力
4.循环等待:对所有资源进行排序,要求进程按照资源顺序来进行申请,打破环状结构,根据资源ID的顺序来申请资源。这种方法在嵌入操作系统里用的比较多,传统OS里用得不多。

死锁避免

在这里插入图片描述
约束性比预防差。并不是保证一定不出现死锁,而是当进程运行过程中会申请资源,在申请时就会判断获得资源会不会出现死锁,如果会,则不会分配资源给发起申请的进程。
1.确定进程需要的某个类型的资源的最大数量
2.用一种方法来限制提供分配资源的数量,提供的数量一定不会大于进程的最大需求。进程说要最多要10个资源,如果申请(2,4,5)三次超过10,则第三次请求将无法实现。
3.死锁避免算法会动态检查资源分配状态,和死锁预防的防止成环不同(只要遵循方法一定不出现死锁),通过算法来确保不出现环形等待的情况。即如果分配资源给进程,可能出现环形等待时,就不会分配这个资源。 【判断条件是是否成环而不是是否形成死锁,约束性弱于预防】
在这里插入图片描述
安全状态:针对所有进程,存在一个时间序列。这个序列表示进程的结束顺序,第一个进程执行完毕第二进程再执行结束……按照这个序列执行下来一定会正常执行。只要按照这个序列执行的话,需要的资源都能得到满足,有限时间内一定执行结束。
在这里插入图片描述
系统的状态:安全状态,不安全状态(包含死锁)。死锁避免就是确保系统不进入不安全状态。

银行家算法

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

m是资源类型数量,不是资源数量!每一个资源类型还有一个量。这里通过设计一个矩阵来表示:进程最多需要某一类资源的个数。

总需求空间:
Max[i,j]=kMax[i,j]=k
PiRjkP_i这个进程总共需要R_j类资源k个

剩余空间:随着运行过程中会动态变化的向量。Available表示剩余空闲量。是长度为m的向量。
Available[j]=kAvailable[j]=k
kRj有k个类型的R_j资源当前可用。
一旦进程不断使用,RjR_j会不断减少。

Allocation[i,j]=knmAllocation[i,j]=k, n*m矩阵
PiRjkP_i这个进程当前拥有R_j类资源k个

Need[i,j]=knmNeed[i,j]=k, n*m矩阵,未来需要量
PiRjkP_i这个进程当前还需要R_j类资源k个来完成任务

变量间的关系:
Need[i,j]=Max[i,j]Allocation[i,j]Need[i,j]=Max[i,j]-Allocation[i,j]
在这里插入图片描述
Work是当前系统中可用资源,一直在变。finish代表当前进程是否结束,F未结束,可能在sleep;T表示的拥有所需要的所有资源,可以正常执行并结束,一段时间后释放资源。

一开始所有进程都是false,都在申请资源中,申请未必成功

e
Needi是一个向量,Pi所需要的各种类型的资源对比当前可用资源Work这个向量,如果Work所有项均大于Needi则说明进程Pi获得了他想要的所有资源,可以开始执行,此时可以执行,将finish数字状态变更为true。因为执行完毕了,所以要把资源释放,也就说将现在持有的资源加入到当前可用资源的数组中。
如果所有进程最后变为ture状态,意味着该安全序列已完成,只要按照这个安全序列来执行进程,一定会处于安全状态下完成所有进程。
但是,如果有一个进程的finish值为false,则说明这系统不安全,一旦系统不安全,就不应该把资源分配给发出请求的进程。


在这里插入图片描述
实例1:
在这里插入图片描述
判断是否安全:我们要判断是否有一个执行序列能够使进程都可以正常结束。
首先V:
1.要找到一个Need<V的向量,发现T2<Need
2.T2正常结束,V={6,2,3}(T2的所需资源释放到可用资源中)
3.此时T1和T3都是可以满足的,这里选择哪一个都可以,因为我们 要找的就是一个能够满足安全的时间序列
4.资源都可以满足,T3和T4也可以随意挑一个
5.找到一个安全执行序列,2134

在这里插入图片描述


接下来分析一下这个不安全序列的例子
在这里插入图片描述如果这个时候T1发出请求,请求为1 0 1的资源分配。如果这时候将1 0 1资源分配给T1,就会发生:
在这里插入图片描述
此时V无法满足任何一个线程的需求来继续执行,说明这种排序方式是不安全的,跳回到上一步。

死锁检测

在这里插入图片描述
进一步放宽条件,允许系统进入死锁状态,到了某一个阶段会判断一下是否当前出现死锁。如果判断出是死锁,就启动恢复机制。这里将死锁检测的阶段放到了系统运行中而不是每次发出请求时。所以折中情况下系统有可能处于死锁状态下,我们需要查出死锁现象。

判断方法1:进程等待图
在这里插入图片描述
某一个进程需要一个资源且需要的资源被另一个进程拥有,则会在拥有这个资源的进程之间做一个有向连线,将资源分配图变成进程等待图,然后看这张图有没有环,如果有就可能死锁了。

判断方法2:死锁检测算法
在这里插入图片描述
和银行家算法的死锁避免算法类似。系统会周期性执行这个算法来检测是否会处于死锁。
在这里插入图片描述
算法复杂度比较大,这就是为什么会有鸵鸟算法来忽略死锁问题的原因之一。对于银行家算法来说,它需要提前知道每一个进程需要的最大资源个数是多少,这一点对于一般程序而言信息很难获得且开销比较大,使得银行家算法在实际操作系统里几乎没有用到。所以死锁检测的过程更多是调试OS和APP时会用到,真正的跑某个系统特别是单机系统一般不会用死锁检测算法。
在这里插入图片描述
由于0和2的需求量是0,所以可以先执行其中的任意一个。执行完毕后,回收已分配资源。执行一圈下来发现,所有进程都可以顺利执行完毕.
在这里插入图片描述
执行GIF:
在这里插入图片描述


下面在看一个死锁的例子,对上述例子微调
在这里插入图片描述
发现T0执行后,即便回收T0的资源,剩余的线程也没有任何一个可以被满足需求,此时就出现了死锁。死锁包含所以没有正常执行完毕的进程或线程,这里是T1~ T4.


在这里插入图片描述
在用这个算法的时候,因为开销比较大,所以需要考虑频度问题。过大影响系统性能,过小可能效果不如预期。以及哪些进程需要被杀死或者回滚,本着公平的原则为什么要杀死A而不是B等都是需要专门研究的。大多数情况下,死锁检测还是用于开发阶段。

死锁恢复

在这里插入图片描述
如果整个系统sleep或忙等,我们为了打破这种状态,需要死锁恢复。
1.杀死所有死锁进程。过于暴力
2.杀死其中一个进程,本着公平的原则需要设立一个规则。基于规则不同,杀死顺序不同。
3.死锁是一种少见的现象,如果一直死锁说明系统有问题。通过重启或者杀死可以在长时间段内有效解决问题。
4.回滚也存在饥饿问题。

进程间通信IPC

通信

在这里插入图片描述
进程之间要相对保持独立,一个进程不能随便访问另一个进程的内存地址空间。另一方面,进程之间可能需要协作完成某些大的任务,这时候需要有一些数据传递和交互,所以在保存进程独立性的同时也要进行通信。
IPC有很多种机制,最基本的有发送和接受两种功能。如果进程之间想要收发消息,他们之间需要建立一个通信的个管道,这个管道有很多种情况,可以基于内存、硬件、逻辑资源等。
在这里插入图片描述
间接通信和直接通信。
在这里插入图片描述
直接通信必须明确收发两方的正确性。同时要建立链路,一般需要OS支持,因为链路打破了进程之间的隔离,没有OS的支持几乎无法做到。
在这里插入图片描述
把消息发送到OS指定的共享区域,并且指定区域接受。对于发送方而言不需要关注谁收,接收方而言也不用关注谁送。
操作:
在这里插入图片描述


在这里插入图片描述
阻塞发送:发送者在发送消息后进入等待,直到接收者成功收到
阻塞接受:接收者在请求接收消息后进入等待,直到成功收到一个消息
非阻塞发送:发送者在消息发送后,可立即进行其他操作
非阻塞接收:没有消息发送时,接收者在请求接收消息后,接收不到任何消息


信号/管道/消息队列/共享内存

把消息采取某种手段将他缓存起来。
缓存最主要的目的是提高效率,来避免发送方和接收方之间的不匹配(可能某些时刻发的快收的慢,或者收得快发的慢)。如果把临时不能处理的数据放在一块缓存区会提高效率。
在这里插入图片描述
1.0容量。如果是直接发送,必须等待接收方接收。类比于阻塞发送方式/同步发送方式,如果立刻返回,由于缓存为0则有可能数据丢失,接收方收不到消息。
2.有限容量:更多的实际情况,缓存有限,如果缓存已满发送方必须等待;如果缓存已空必须等待。
3.无限:发送方不需等待,接收方如果没有数据还是需要等或者返回一个错误信息。


信号Signal

在这里插入图片描述
对比于硬件中断。信号相当于软件中断,打断当前运行的应用程序,发出通知信息去处理其他事情。

接收到信号后:
1.退出执行。2.忽略信号,不处理。
3.指定一个专门的信号处理函数。一旦信号来了,特殊函数会响应对应的处理,由程序员管理。
信号很灵活但是不适合用来传递数据,因为信号是个很小的位比特,不是数据交换而是通知作用,效率高,异步打断机制。处理完信号要求的任务后,一般会回到被打断的程序重新或继续执行。
在这里插入图片描述
1.应用程序如果要针对某个信号做单独处理的话,则程序执行开始时要注册一个针对某一类信号的处理函数。然后作为系统调用发给OS,OS看到后,当产生对应信号时会调用此专门的信号处理函数来执行。
2.由OS来完成。当OS收到信号时或者处理信号时,会处于内核态。当从内核态返回到用户态去执行信号处理函数时,要把系统调用返回的用户空间的堆栈进行修改,使得本来应该返回到调用系统调用的下一条语句处改到信号处理函数的入口。同时再把信号处理函数执行后要执行的地址作为后面栈针的返回地址。这样一旦从OS内核返回到应用程序时,会根据留的栈信息跳到信号处理函数的入口去执行,这时候回到用户态,函数执行完毕后,会继续返回到被打断的地方。
为了实现信号机制需要修改应用程序的堆栈,这一点一般应用程序不会这么做,木马病毒会这样做来完成一些特定的功能。

管道

在这里插入图片描述

管道是用于数据交换的。早期管道机制:从一个程序结束后接入另一个程序的开始,如此可以令多个小程序形成一个整体实现大的功能。程序本身并不会在编写的时候考虑这些问题,而是由管道来完成这些操作的。
在这里插入图片描述
Shell收到了一条命令(ls|more),会发现是两个命令连接到一起,这两个命令对应两个进程。ls的输出并没有输出到屏幕上,而是输出到管道里,这个管道是内存中的一块缓存,ls现在是往管道内部写,more来接收。
管道 具有双重身份,对于ls他认为是在往屏幕上输出,对于more他认为从屏幕上的数据在给他输入。管道实际是内核中的一块缓存。管道的缓存是有限的,如果more来不及填满缓存填满后,会出现阻塞。同样more也会因为管道内无数据而阻塞。
ls和more能协同是因为有个共同的父进程shell,子进程可以继承父进程的一些资源,这里面管道是一种文件形式存在的,通过这种方式就能完成这种功能。

消息队列

在这里插入图片描述
管道的缺点:管道是父进程帮子进程接的通道,如果进程之间没有父子关系,管道就无法工作了。管道里的数据是一种字节流,没有结构化的表示方式,需要接收方解析数据,额外开销。

而这些缺点消息队列可以克服,可以将不相干的进程通过消息队列来传递数据;传输的数据是结构化的数据而不是字节流。

相同点:缓存有容量限制,有阻塞情况发生。

共享内存

在这里插入图片描述
是一种直接通信的方式,两个进程都可以访问到一个特殊的内存空间。一个进程写入这个内存空间,另一个进程可以马上看到。中间不需要做发送和接受操作。直接读写内存就可以完成数据共享,相对于前面的几种方式而言,传输的量最大,速度最快,不需要OS介入。只是最开始时需要创建一块共享区域。方便高效没有多余拷贝。
不足:必须同步互斥机制来保证数据的安全性。多个进程同时写会出现意想不到的错误。需要内存管理的充分支持(页表等相应机制)
在这里插入图片描述


HINT)TCP协议栈实现在内核态,在网络原理里会讲,在操作系统课中不展开讲解。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章