【哈工大李治军】操作系统课程笔记7:进程同步、信号量、临界区、死锁 + 【实验】信号量的实现和应用

1、进程同步与信号量

(1)信号

通过使用信号量,来让多个进程合理有序的推进工作。
在这里插入图片描述
我们的目标是让多个进程合理有序的共同完成一个任务,而不是各干各的。因此,对他们进行约束,就需要确保他们合作的有序性,即谁先做谁后做。

信号就是用于双方互相发出对方可执行或对方可等待信息,来实现多个进程合作推进。等待是进程共同合作的核心。

生产者-消费者是多进程合作的经典示例。
在这里插入图片描述
BUFFER_SIZE是一个共享缓冲空间buffer[n]的空间容量,生产者不断地往buffer[n]里放东西,不断地加一;消费者不断地往buffer[n]里取东西,不断地减一。

在这里插入图片描述
需要让“进程走走停停”来保证多进程合作的合理有序。分析时,什么时候停是重点,知道什么时候停了,就能顺着推出什么时候走了。通过信号来控制,当对方之后有收到对方**“可以继续走”**的信息后,进程才可以继续执行。

(2)信号量

在这里插入图片描述
但只有信号还不能解决全部问题。
(1)当缓冲区满以后生产者P1生产一个item放入后,会sleep。
(2)又一个生产者P2生产一个item放入,发现counter==BUFFER_SIZE后,也会sleep。
(3)然后,消费者C执行一次循环,首先判断counter不等于0,则不sleep,而去执行后续内容。消费了buffer[n]中的一个东西后,如果判断到counter==BUFFER_SIZE-1(意味着刚才缓冲区满了,可能有生产者被阻塞),则就给生产者P1发出wakeup信号,唤醒P1。
(4)当消费者C再执行1次循环时,此时counter在之前已被上一个消费者减1。当再遇到counter==BUFFER_SIZE-1时,counter=BUFFER_SIZE-2不满足执行wakeup条件,那么就会认为刚才没有缓冲区没有满,则没有进程被阻塞,没有进程在缓冲区。 就会导致不会发出wakeup信号,P2不会被唤醒

原因counter仅反映缓冲区空余的个数,但还需要一个变量可以反应被阻塞进程的个数。因此,当进程发送信号时,不能仅通过判断counter就决定是否法信号,应该引入另外一个跟 睡眠和唤醒 有关的量来判断是否法可以发信号。这个量就是信号量
在这里插入图片描述

在这里插入图片描述
(1)用sem作为信号量。缓冲区第一次满时,因为是1个进程等待,看起来像是缺少一个东西,所以记录为-1
(2)当P2再执行时,由于没有消费者进行释放,因此继续被阻塞,sem减一,为-2。
(3)当消费者执行1次循环时,发现sem小于0,则从阻塞队列的队头使用wakeup唤醒一个进程,即为唤醒P1。然后,将sem加一,就变为了-1。
(4)当消费者再执行1次循环时,发现sem还是小于0,则再唤醒一个进程,即唤醒进程P2,sem加一,变为0。
(5)当消费者再执行1次循环时,发现sem大于0,没有进程处于睡眠状态。sem加一后,变成1,意味着当前还有一个空闲位置可供生产者使用,来存放东西。
(6)P3再执行生产出一个东西,放入缓冲区中,sem会被减1,此时为0,意味着当前没有空余位置可存放生产者生产的东西。

使用信号量作为记录:
(1)负值: 有多少(生产者)进程因为缺少资源而被阻塞
(2)正值: 当前还有多少资源可被(生产者)进程使用
因为信号量很好的关联了睡眠唤醒动作,故使用信号量而不是信号来控制是否发送信号,根据 信号量的值 来决定进程什么时候等,什么时候走,从而实现进程的同步和等待。

这里也发现,信号量是对一个角色(生产者)因使用某一资源所产生的状态变化的控制。所以,一个信号量应该对应着一个资源或一个进程所使用的资源。
在这里插入图片描述
每个信号量要关联一个进程队列,用于记录阻塞进程。

P操作

P(semaphore s) {
	s.value--;
	if(s.value < 0)
		sleep(s.queue)
}

V操作

V(semaphore s) {
	s.value++;
	if(s.value <= 0)		// 加完后还为0或负数,则说明之前有进程在阻塞队列里
		sleep(s.queue);
}

对于先加或先减时,用if应该考虑加前减前的状态进行判断。
在这里插入图片描述
生产者
(1)首先查看一下是否有空闲位置可以使用P(empty),如果没有则阻塞进程,如果有就使用则往文件里写内容。
(2)但因为文件内容为临界资源,所以此时消费者的读和生产者的写是互斥的访问,因此需要对文件内容再使用信号量进行控制。先使用P(mutex)来判断是否有消费者正在读内容,如果有的话,生产者会被阻塞,等待消费者发出V信号释放后,可写入。如果没有的话,生产者上锁,此时消费者不能读取文件内容。然后,生产者将内容写到itemin位置上。完成后,再V(mutex)发出解锁信号。
(3)生产完后,生产者已经往空闲缓冲区里添加了内容,就给消费者释放出信号V(full)

消费者
(1)首先上来先测试一下,看缓冲区中是否有内容P(full),如果没有则阻塞进程
(2)但因为文件内容为临界资源,所以此时消费者的读和生产者的写是互斥的访问,因此需要对文件内容再使用信号量进行控制。对mutext的操作同理生产者此位置处的操作,然后,消费者从out位置上读出item并打印出来。完成后,再V(mutex)发出解锁信号。
(3)消费完后,消费者空闲缓冲区里增加了一个空位,就给生产者释放出信号V(full)

2、信号量临界区保护

(1)临界区

在这里插入图片描述
当我们共同修改信号量时,会引发出问题。当两个生产者刚好共同要修改缓冲区内容时,其中一个生产者得到的可能不是需要的原信号量的值,而是被另一个生产者刚修改完的值。
在这里插入图片描述
这不是因为编程而出现的错误,而是因竞争条件而出现的错误。

:通过加一些空循环来使时间片到达预期的点,而保证不出现上述错误,也许仅能解决本次启动过程中的问题,但下一次开机可能会因为进程启动次序不一样导致又会出错,所以这种方法不能解决根本错误。

所以,我们必须要引入保护机制。
在这里插入图片描述
想法就是生产者在修改empty时,需要上锁来保证此时该资源仅能被一个生产者所使用。只有当除以开锁状态时,其余生产者才能对empty进行访问修改。最终实现,一个进程在修改empty这段代码时,其他进程不能参与修改。对于empty的修改“要么直接做完,要么一点都不做”,即为原子操作
在这里插入图片描述
临界区就是一次只允许一个进程进入的该进程里的那段代码,其中读写信号量的代码一定是临界区。
在这里插入图片描述
互斥进入是临界区代码保护的基本原则。而好的临界区保护原则是有空让进
有限等待(某一进程不能一直处于等待其他进程的状态)。

(2)两个进程同步

(1)轮换法

在这里插入图片描述
一种方式是轮换法,一人进去一次,轮流挨个的执行。其中有一个问题,就是当该P1执行时,可能会因为一些情况P1去执行别的事了,P1就没有执行,但此时P0也不能进入,这就不符合有空让进原则。
在这里插入图片描述
上面的轮换法类似于值日,更好的方法是立即去买,留一个便条。这就引出了标记法

(2)标记法

在这里插入图片描述
标记法是给每个进程在标记数组中保留一个标记位,当准备使用临界资源时,就把自己对应的标记位置置为true,当进入访问时首先查看对方是否也标记为true,如果对方也已标记为true,则进入循环等待中,等待对方访问完变为false后自己才可访问,并于访问完后将自己的标记位置为false
在这里插入图片描述
但问题是仅满足互斥访问要求,当两个进程一起把自己的标记位置为true时,会造成双方都无法进入临界区,临界区此时空闲,但两个进程都无法进入,不满足空闲让进的要求。

(3)非对称标记法

在这里插入图片描述
由此引入非对称标志,让两个进程中其中一个更加勤劳一些,已知循环等待,而让另一个先进入。

(4)Peterson算法

在这里插入图片描述
实现方式是标记+轮转(值日和标记)两种思想的结合。

进程P0想要访问临界区时,首先将标记为置为true,然后每次也要保持“谦让”,让trun=1使其发现对方进程想访问是让自己陷入循环等待。当进行到while()时,发现对方进程已置true标记且此时的turn1,意味着对方进程想要访问临界区且该自己循环等待(值日)了,便让自己处于循环状态而让对方进入访问。当对方访问完临界区后,又将它自己的标记置为false,从而让进程P0可以访问临界区,访问完后将自己的标记置为false。进程P1访问时也同理。

此方法主要由关键两步: 标记和轮转。用标记控制是否想要访问邻接资源,用标记控制自己是否此可以访问邻接资源,“每次都监控对方意图且保持谦让”(while(flag[1] && turn == 1))。
在这里插入图片描述
Peterson算法满足互斥进入有空让进有限等待

(3)多个进程同步

(1)面包店算法

在这里插入图片描述
借鉴生活中取号排队的思想,取得的号不为0,则表示想要进去,按号码从小到大授予访问权限。

首先,标记正在取号(choosing[i]=true),然后取号并且取得是当前进程的最大号再加一max(num[0], ... , num[n-1]) + 1),不再取号(choosing[i]=false)。

然后,对其余进程进行遍历,如果有进程正在取号,则等待该进程取完号本进程再操作(while(choosing[j]))。当有其余进程想进入临界区(num[j]!=0)并且该进程的号比本进程号小((num[j],j) < (num[i], i])),那么本进程就陷入循环等待,让对方更小的号进程内部再去以此方式执行判断。当每个进程都以此方式工作时,最终会有一个号最小且想要访问临界区的进程没有陷入循环等待,进入临界区。
在这里插入图片描述
总结来说,每次当进程想要访问临界区时,先取号,然后再判断其余进程的情况,查看其余进程是否想要访问临界区?是否比自己更小? 对于想要临界区的进程,如果其余进程更小,则自己循环等待。如果与所有进程比较完后,发现自己进程的号最小,那么自己就可以去访问临界区了。
在这里插入图片描述

(2)单CPU关中断法

在这里插入图片描述
回想一下我们要解决的问题是一个进程在临界区时,另一个进程也进入临界区。那么,为什么另一个进程也会进入临界区呢?原因就是调度。另一个进程只有被调度才能执行,才可能进入临界区。那么我们就会想,是否可以再一个进程进入临界区时,阻止另一个进程的调度

最直观的想法就是阻止中断,因为去调度另一个进程的前提是使用中断,所以只要我们在进入临界区前关闭中断(cli())就可以阻止调度。当使用完后,再开中断(sti())。

这种情况在但CPU里好使,但在多CPU里不好使。

(3)硬件原子指令法

在这里插入图片描述
对临界资源上锁,但如果使用信号量作为锁的话,我们就还需要再对锁进行保护。为了实现上锁,修改empty,开锁成为原子操作,那么就把这三个操作合成一条指令即可。

TestAndSet()实现的就是判断x之前是否为true并且也将本次的x置为true。如果之前x就为true,则说明已经有进程在访问临界区,需要循环等待。如果为false说明之前没有进程在访问临界区,则自己进入临界区,其他进程再访问时,由于已经被本进程改为了true,所以其他进程也进不来。直到访问完毕后,将lock置为false,其他进程可进入。在执行过程中TestAndSet()为原子操作,要么一下子执行完,要么一下都不执行,中途不可被打断。

(4)总结

用临界区去保护信号量,用信号量来实现同步(访问共享资源)。

对于多进程同步中,单CPU可用关中断保护临界区,多CPU可用硬件原子指令法保护临界区。

3、信号量代码的实现

在这里插入图片描述if中是把阻塞队列中的一个唤醒。
在这里插入图片描述
while是把阻塞队列中的全部都唤醒。
在这里插入图片描述
在这里插入图片描述

4、死锁处理

在这里插入图片描述
多个进程 互相等待对方持有的资源 而造成的谁都无法执行的情况叫 死锁

(1)死锁三个成因

在这里插入图片描述
死锁三个成因:

  • 资源互斥
  • 进程已有一些资源又不释放,还再去申请其他资源
  • 各自占有的资源和互相申请的资源形成了环路等待

(2)四个必要条件

在这里插入图片描述

  • 互斥使用
  • 不可抢占
  • 请求和保持
  • 循环等待

(3)死锁处理方法

在这里插入图片描述

  • 死锁预防:破坏死锁出现的四个条件。
  • 死锁避免:检测每个资源请求,如果造成死锁就拒绝。
  • 死锁检测+恢复:检测到死锁出现时,让一些进程回滚,让出资源。
  • 死锁忽略:忽略死锁出现(因死锁出现是小概率事件),出现后直接重启。

(1)死锁预防

在这里插入图片描述

  • (1)在进程执行前,一次性申请所有需要的资源,不会占有了一部分资源后,再去申请其他资源

    • 缺点1:需要预知未来,编程困难;
    • 缺点2:许多资源分配后很长时间以后才会使用,资源利用率低。
  • (2)对资源类型进行排序,资源申请必须按序排序,不会出现环路等待

    • 缺点:仍然造成资源浪费。

(2)死锁避免:银行家算法

在这里插入图片描述
判断系统中按一个预定的执行序列P1P2…Pn执行后,提前判断是否可以正确完成且不会出现死锁情况。如果可以正确完成,则认为系统处于安全状态

在图上判定是否为安全序列。其中,Allocation是该进程可分配的资源、Need是该进程需要的资源、Available是系统中现有的资源。
在这里插入图片描述
每次判断该任务是否没有执行完当前所需资源量系统是否满足,如果其中有一个不满足就判断下一个任务,如有一个任务满足时,就将资源分配后释放使用过的资源给系统,同时置该任务已完成,再进行下一次循环。
在这里插入图片描述
在这里插入图片描述
请求出现时,先假装分配,然后查看后续分配是否可以满足其他任务。如果不满足,则此次申请被拒绝。

(3)死锁检测+恢复:发现问题再处理

在这里插入图片描述
由于每次都执行死锁避免的银行家算法时,时间复杂度为 O ( m n 2 ) O(mn^2) O(mn2),效率太低。所以只有当发现问题时,再处理。

定时检测或者是发现资源利用率低是检测,将死锁的进程都存放到deadlock死锁进程组。

对于死锁的进程,首先是挑选一个进程进行回滚。但应该选择哪种方式回滚,按优先级或者占用资源多的等等便是一个问题。另一个是如何实现回滚,对于已经修改的文件如何处理,例如银行的客户信息已经被写入,不能随意修改。处理起来比较复杂。
在这里插入图片描述
所以,对于许多通用的操作系统,都会采用死锁忽略方法。因为死锁在PC机上出现的概率比较小、死锁忽略的处理代价最小、可以通过重启来解决且重启对PC机的影响较小。

(4)死锁忽略

在这里插入图片描述

[实验 6]:信号量的实现和应用

参考:
操作系统实验六 信号量的实现和应用(哈工大李治军)

Linux 0.11下信号量的实现和应用

  • 3
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

辰阳星宇

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值