OS-Revision---进程同步
Posted 给个HK.phd读
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了OS-Revision---进程同步相关的知识,希望对你有一定的参考价值。
信号量有一道12分的大题,好好复习,好好复习!
我们在进程章讲过如果程序并发执行的话或许会失去“可再现性”,当时用的例子就是:A执行x = x + 1,B执行print(x),x = 0会产生不同结果。
但并发执行显然又是能提高CPU运行效率的,所以我们现在就要考虑一种机制可以使既能并发执行也不失“可再现性”。
并发进程间的制约关系
简单来说:
间接制约就是控制进程对临界资源的访问,对于这类资源需要由系统统一分配。
直接相互制约就是使诸进程按序执行。
生产者-消费者问题
说到临界资源和信号量处理的经典例题。
我们来看看题目如下所示:
规则应该不难理解,即一方生产(往缓冲区放东西),满了就不能继续放;另一方就进行消费(从缓冲区取东西),空了就不能再取。
接下来我们声明一些变量,企图用伪代码来解决此问题:
注意:
生产者需要的是指向空缓冲区的指针,消费者则反之。
而且由于我们的缓冲区有限,不能一直执行“+1”操作,因此要对缓冲区进行取模运算。
接下来就可以根据流程和这些变量写出伪代码:
对于生产者producer,执行放入操作,如果产品数填满了缓冲区就不能继续放。
成功放入的话将产品放入buffer数组,且产品数加一,指针指向下一块空缓冲区。消费者思路是类似的。
代码看起来是没有问题的,且顺序执行是不会有错的。就是先用producer.c放好,再用consumer.c去取。
但当程序并发执行时是会有问题的:
上图就是我们将C代码翻译成机器指令时真实的操作,其实就是为了表明counter这个计数货物总量的变量在并发执行时未必完全正确。
如下图所示就是counter紊乱的现象:
按照我们的思路应该是先让register1的操作完全完毕后再进行register2的操作。但显然我们翻译成机器指令且并发执行后会混乱,也就是counter被一起占用了。
于是解决此问题的关键就是互斥访问counter,把counter变为“临界区”。
当一个进程占用临界区时确保其他进程进不来,就不会有上述现象了。
因此一个进程的流程可以如下:
repeat
——进入区,检查
critical section; ——临界区,访问资源
——退出区,恢复
remainder section;
until false;
其实就是进入临界区前对其进行检查,没有人占用才进去
但我们的等不能是傻瓜式等待,因为CPU资源很宝贵,不用就得释放出来!因此对于同步机制我们有如下的一些规则:
基于硬件实现互斥访问
我们可以通过代码,软件方式实现互斥访问,但比较困难且不普适。ppt上有一个算法,做了解即可,书上都没有这块内容:
对于硬件实现“锁”的方式,介绍了下列两种:
第一种是TS指令法,虽说是一个函数但应该视为一次“原语”操作。
其中,lock = false时表示资源空闲,反之表示忙。
所以指令内部原理也很简单,只要测试通过lock为false就进入临界区并将lock置为true。当临界区执行完毕后就重新释放lock为false。
否则的话就是直接do skip进行跳过。
还有一种办法就是基于Swap指令。
信号量机制
重点内容,涉及考题。
1.整型信号量
用变量S来代表资源数目。
包含了两个原子操作,我们成为PV操作。
分别对应的就是通过和释放,我们用wait(S)和siginal(S)来表示。
光看通过和释放很懵逼,代码就很好懂:
wait(S): while S≤0 do no-op
S∶=S-1;
signal(S): S ∶=S+1;
简单的说,wait会减少资源量,而signal会返还资源。
缺点就是while会陷入等待状态,而且是我们提到的“傻瓜式等待”,无法释放CPU资源。
2.记录型信号量
此种方式改进了上面那种方式的缺点。
其新增了一个链表L,用于链接所有等待进程。
此种信号量变成了一个结构体:
type semaphore=record
value: integer; //资源可用数量
L:list of process; //等待进程队列
end
此种形式的PV操作伪代码大致如下:
简单的解释一下。
wait操作时,S->value会减少。当这种资源分配完毕时此值就是小于0的此时会自动调用阻塞原语block,插入到信号量链表中。
signal操作时,返还信号量。如果返还后还是小于等于0,说明返还的又立马被占用啦,所以对阻塞链表的进程进行唤醒操作。
如果这儿的value设置为1,实际上就是我们的互斥型信号量。
3.AND型信号量
我们前面讲到过很多互斥型资源,就是我们初始时会把信号量值设置为1。
但很多情况下我们需要多种信号量,很容易发生“死锁”现象:
简单来说资源D、E数量都只有1。A、B进程并发执行,A占用D的同时B占用了E。
但此时A需要E,B需要D,但两者都没完成自己的任务,无法释放自己占用的资源,于是彼此等待陷入“死锁”。
而且这只是两个资源,涉及资源越多,发生“死锁”概率就越大。
于是,基于此,我们才提出的“AND”信号量。
对于此种类型的信号量,有如下一些设计准则:
其执行步骤伪代码如下图所示:
对于Swait,我们做的就是判断其所需的全部资源是否都能得到。是的话就占用所有资源(数量都减一)。不行的话,我们将此进程放入第一个缺失信号量的等待链表中,同时将程序计数器指向Swait(即下一次调度时从Swait做起)。
Ssignal操作就是返还所有的信号量,将每种资源的阻塞队列里的进程全部调出,进行唤醒操作,然后调度。
3.信号量集
这是一种对AND信号量的扩充。
主要理念就是,我们有时往往需要的信号量数量是N个而不仅仅是1个。如果我们用原始办法就是调用N次wait,显然是低效的。
我们同样用Swait操作。同时里面多了一些值,需求值d,也就是我们要减去的值。下限值t,也就是我们拿来当判断条件的。下限值就是为了系统安全考虑的。
Ssignal就很容易,大部分都和AND型一致,只不过返还的时候应该是加上d(需求值,当然是分配多少拿回多少)。
有一些值得讨论的特殊情况:
Swait(S, d, d):此时在信号量集中只有一个信号量S, 但允许它每次申请d个资源,当现有资源数少于d时,不予分配。
(信号量S,需求d,下限d)
Swait(S, 1, 1):信号量集蜕化为一般的记录型信号量(S>1时)或互斥信号量(S=1时)
(信号量S,需求1,下限1)
Swait(S, 1, 0):这是一种很特殊且很有用的信号量操作。当S≥1时,允许多个进程进入某特定区;当S变为0后,将阻止任何进程进入特定区。它相当于一个可控开关。
最后一种是说只要当前资源大于0(因为需求都只有1)就都可以进入,所以会有多个进程进入的情况。
经典同步问题
上面的同步机制,这么多类型信号量都是为了做题服务的。
结合我们刚刚在“生产者-消费者”问题中对counter的分析,我们可以这样写:
就是把counter设置为临界区,互斥访问!
;
但这样其实还是不完美的,我们没有实现缓冲区全满时生产者阻塞以及另一种消费者阻塞的情况。
为此,我们新引入信号量full和empty。分别表示满的缓冲区数量以及空缓冲区数量。同时我们加入一个互斥型信号量mutex,用来将缓冲区当作临界区,即某一时刻只能有一方进入缓冲区进行操作。
完整的程序如下:
注意的就是红色字体PV操作的顺序以及写法。
对于生产者,我们要去等到空闲块数量,只有有空闲缓冲区才能继续。继续我们得判断此时缓冲区(临界区)能否进入,能进,才执行将物品放入空缓冲区并将空缓冲区指针移动操作。全部操作完毕后,返还信号量。
对于消费者,分析方法是一样的。
这里面最需要注意的问题就是wait的信号量顺序问题了。把mutex放在第二位是必需的么,为什么呢?
这里我们有一个规则就是------“先私后公”。
私,即私有信号量。full和empty就是私用的,消费者不用考虑空的有几块,或者可以说轮不着他管。生产者同理。
公,即互斥公用信号量。两者都希望进入临界区对缓冲区进行操作,这块临界区是公有的,都可以进入。
如果采用“先公后私”,可以仿照刚刚的分析方法进行尝试,会发生“死锁”。
producer:
wait(mutex)
wait(empty)
consumer:
wait(mutex)
wait(full)
比如某时全是满的,生产者进入临界区,却阻塞在empty处,因为没有空的块了。
此时需要消费者帮助拿走一个才行,但消费者连临界区都进不去!
上面的伪代码可以简化,我们用到“AND型”信号量。用Swait(empty,mutex)就可以替代两条wait操作。Ssignal同理。
很简单就不附伪代码。
仔细理解“生产者-消费者”问题后,可以看这么一个例题:
实际上对于这样的题目,比较困难的点在于自己设置信号量,可能伪代码信号量处理并不复杂。老师也说了考试题会给你信号量的提示,所以这里给出信号量的设置情况。
司机和售票员两个角色
两个进程driver和busman
条件1:关车门 -》启动车辆
设置信号量S1对应司机能否启动车辆
S1的检查在司机进程,开锁在售票员进程
条件2:到站停车 -》 开车门
设置信号量S2对应售票员能否开车门
S2的检查在售票员进程,开锁在司机进程
所以根据信号量 + 流程 + 限制条件我们有如下的逻辑。
driver:
等待司机是否启动的信号量 wait S1
开车
正常运行
释放售票员可以打开车门的信号(即通知可以由售票员打开车门) signal S2
busman:
上下乘客
关门
释放司机可以启动的信号量(因为关上门后司机可以启动了) signal S1
售票
等待售票员可以打开车门的信号(要等到司机停车,释放信号量才可以) wait S2
上下乘客
所以才有了我们的伪代码:
这里有个设问S1能否为1。当然不行,否则的话wait就直接可以进入了,那不就开车和上下乘客可能会并行了,显然不能如此。
后面还是一个例题,会更加符合考试模式一点:
先对信号量进行理解,shelf表示空位,item表示货物数,tmutex作为互斥信号量就是为了同一时刻只有一个进程进行仓库相应操作。
同时还需要注意的就是“先私后公”规则。
要放入时,我们首先要判断是否有空位,然后判断能否进入临界区。
所以:
in
wait(shelf)
wait(tmutex)
signal(tmutex)
signal(item) // 返还的是item 因为放入后 item需要做“加一”操作
out
wait(item)
wait(tmutex)
signal(tmutex)
signal(shelf)
哲学家进餐问题
题目是这样的,很好理解,不再赘述:
简单的想法当然就是一个哲学家拿起自己的左右两边的筷子,进食完毕后筷子释放出来。这种思路的伪代码可以写成下列形式:
其中chopstick是一个数组,长度就是5和哲学家数量一致。每一个元素都视为是信号量,且都初始化为1,意为互斥型信号量,即同一时刻只能有一人占用某一只筷子。看起来仿佛也是很正确的……
试想,因为进程并行,五个哲学家同时占用了自己的左(或右也行)边的筷子,那每个人都等不到右边筷子的信号量,会出现死锁现象。
所以此方法仍需要改进。
最简单的方法就是利用AND型信号量。就是我同时申请两个信号量,同时满足才分配,吃完后全部返还,否则就进入阻塞。
伪代码的话就是把两个wait改成一个Sswait(chopstick[i], …)
同时两个signal也改为Ssignal即可,很容易。
但ppt还提供了一些其他的办法:我们上面采取了第2种。
读写者问题
读写者问题基于如下的问题及条件:
这个问题确实要比之前的问题都会麻烦一些。从这么多限制就可见一斑。
根据步骤,我们还是要先设置信号量,然后我们对信号量进行比较透彻地分析,结合上多种类型的限制。
信号量:
互斥信号量Wmutex。
整数型变量Readcount,用以记录读者的数量。对此数值分析:
Readcount > 0时,表示当前有进程正在进行读操作,且根据限制并不会有写进程,因此其余进程可以参与写操作。
Readcount = 0时,当前并无读进程,但可能有写进程。所以我们要先调wait(wmutex)进行是否能继续读的判断!如果顺利进入,则执行readcount加一的操作。
读进程退出后,要执行readcount减一的操作。同时如果自己是最后一个退出的读进程就要释放wmutex,保证写进程可以顺利进入。
由于readcount是互斥的,主要是我们要进行的加减一的操作并不是原子操作,多个读进程并行进入(或退出)时可能会造成 readcount 数值的紊乱。
所以最后还需要一个rmutex把readcount设置为临界区。
最后的写者代码如下:
写者代码很容易,因为任何时刻只能是自己在里边
procedure writer; 写者进程
begin
repeat
wait(wmutex);
perform writing operation;
signal(wmutex);
until false;
end;
读者稍困难,要针对readcount进行判断进行不同操作,且readcound本身也应该成为互斥型变量。 别忘了“先私后公”
procedure reader; 读者进程
begin
repeat
wait(rmutex);
if (readcount == 0) then wait(wmutex);
readcount := readcount + 1;
signal(rmutex);
perform reading operation;
wait(rmutex);
readcount := readcount - 1;
if (readcount == 0) then signal(wmutex);
signal(rmutex);
until false;
end;
好好理解读者进程很关键,但考试不会考这种经典原题。
读写者问题的升级版就是:
之前我们允许多用户一起读,现在我们限制最多同时读的用户数为Rn。
我们用信号量集处理此问题:
整体思路是这样的,设置信号量L为Rn即最大允许用户数量。
设置信号量mx为1,即对临界区的一个保护,就是拿来用于读写互斥的。
接下来,对于写者:
我们对于临界区占用,所以减少了一个mx。同时此时我们不允许任何读者进入,所以我们需要对读者当前数量进行一个判断,即此时读者数量不能小于底线RN,否则也进不来。
这和我们记录型不同的地方就在于,因为我们此时读者干脆就不占用mx了,所以写者无法直接通过mx信号量存在就判断此时无读者,必须还得看数量。
写操作完毕后,返还mx即可,因为我们并没有占用L信号量–需求是0。
然后就是读操作,读者先减少读者的数量,然后根据mx是否被占用来判定能否进入进行操作。若是,则进入,读完毕,返还信号量,把可以进入的读者数量增加一。
我们同样可以换种解法实现读写者问题。
最后一道例题了:
老师说难的那套卷子和这题很像
有一个盘子, 每次只能放入一个水果,小男孩A向盘中放入苹果, 小女孩A向盘中放入草莓;
小男孩B等待取盘中的苹果, 小女孩B等待取盘中的草莓。
请用wait,signal原语实现同步执行的程序。
这里一个盘子就是对应到我们的临界区,某一时刻我们只能有一个人(男/女)对盘子进行一种操作(放/取)。
所以我们设置一个互斥信号量mutex。
同时我们需要记录苹果和草莓的数量。
又因为!苹果or草莓实际上最多只能有一个,因此可以把这两个整型的也看作是信号量。我们称之为 straw 和 apple。
mutex为1,两种水果就初始化为0。
总共有四个函数,人(男/女) × 操作(放/取)。
我们理解一半即可。
比如无论哪个操作,都要先wait(mutex),确保可以进行操作方可操作。
执行完相应操作后,如果是放我们就用signal(水果),表示某种水果加一。
最后返还mutex,供其他进程进入。
如果是取操作,那么就用wait(水果),表示某种水果减一。
但此例还可以简化。
代码如下:
我们函数后缀A代表放操作,B代表取操作。
相比我们刚刚所说。我们理想中的BoyA可以这样写:
void BoyA()
while(1)
wait(mutex)
put an apple
signal(apple)
signal(mutex)
void BoyB()
while(1)
wait(mutex)
wait(apple)
take an apple
signal(mutex)
因此比较而言就是我们对于mutex处理的不同。
其实ppt上的简化代码是完全讲得通的。
比如:
我们从BoyA进入,占用了mutex,放置了苹果,苹果数量增加。
注意因为我们只有一块缓冲区(一个盘子),放置的苹果若未被取走女孩是进不来的。
所以唯一的策略就是男孩取走苹果,BoyB调用成功。结束后才再次打开临界区供男女孩使用。
所以其实按照我们起初的思路,在放置阶段结束就返还信号量的话。可能此时女孩的放置进程就进来了,但是缓冲区是满的其又放置不了,所以需要立马返还mutex并退出或者进入阻塞。
所以反倒是实现起来更加麻烦的一种,所以考试过程中自己写好伪代码流程多走几遍看看会不会有bug(死锁,一起进入了临界区等)。
以上是关于OS-Revision---进程同步的主要内容,如果未能解决你的问题,请参考以下文章