操作系统第二章——进程与线程(下)

目录

2.9调度算法Ⅰ

2.9.1先来先服务(FCFS,First Come First Serve)

2.9.2短作业优先(SJF,Shortest Job First)

2.9.3高响应比优先(HRRN,Highest Response Ratio Next)

2.10调度算法Ⅱ

2.10.1时间片轮转(RR,Round-Robin)

2.10.2优先级调度算法

2.10.3多级反馈队列调度算法

2.11进程同步与进程互斥

2.11.1什么是进程同步?

1.知识点回顾

2.基本概念

2.11.2什么是进程互斥?

1.知识点回顾

2.基本概念

2.12进程互斥的软件实现方法

2.12.1单标志法

2.12.2双标志先检查法

2.12.3双标志后检查法

2.12.4Peterson算法

2.13进程互斥的硬件实现方法

2.13.1中断屏蔽方法    

2.13.2TestAndSet指令

2.13.3Swap指令

2.14信号量机制

2.14.1信号量机制的基本概念     

2.14.2信号量机制——整型信号量

2.14.3信号量机制——记录型信号量

2.15用信号量机制实现进程互斥、同步、前驱关系

2.15.1信号量机制实现进程互斥

2.15.2信号量机制实现进程同步

2.15.3信号量机制实现前驱关系

2.16经典同步问题

2.16.1生产者—消费者问题

2.16.2读者—写者问题

2.16.3哲学家进餐问题

2.17管程

2.17.1为什么要引入管程

2.17.2管程的定义和基本特征

1.管程的定义

2.管程的基本特征

2.18死锁的概念

2.18.1什么是死锁

2.18.2死锁、饥饿、死循环的区别

2.18.3死锁产生的必要条件

2.18.4什么时候会发生死锁

2.18.5死锁的处理策略

2.19死锁的处理策略

2.19.1死锁的处理策略——预防死锁

1.破坏互斥条件

2.破坏不剥夺条件

3.破坏请求和保持条件

4.破坏循环等待条件

2.19.2死锁的处理策略——避免死锁

1. 什么是安全序列

2.安全序列、不安全状态、死锁的联系

3.银行家算法

2.19.3死锁的处理策略——检测和解除

1.死锁的检测

2.死锁的解除

2.9调度算法Ⅰ

Tips:各种调度算法的学习思路

1.算法思想

2.算法规则

3.这种调度算法是用于作业调度还是进程调度?

4.抢占式?非抢占式?

5.优点和缺点

6.是否会导致饥饿(饥饿:某进程/作业长期得不到服务)

知识总览

2.9.1先来先服务(FCFS,First Come First Serve)

       FCFS调度算法是一种最简单的调度算法,它既可用于作业调度,有可用于进程调度。在作业调度中,算法每次从后备队列中选择最先进入该队列的一个或几个作业,将它们调入内存,分配必要的资源,创建进程并放入就绪队列。

       1.算法思想:主要从“公平”的角度考虑(类似于我们生活中排队买东西的例子)。

       2.算法规则:按照作业/进程到达的先后顺序进行服务。

       3.用于作业/进程调度:用于作业调度时,考虑的是哪个作业先后到达后备队列;用于进程调度时,考虑的是哪个进程先到达就绪队列。

       4.是否可抢占:非抢占式算法。

       5.优缺点:优点:公平、算法实现简单。

                         缺点:排在长作业(进程)后面的短作业需要等待很长时间,带权周转时间很大,对短作业来说用户体验不好。即,FCFS算法对长作业有利,对短作业不利。(Eg:排队买奶茶,前面有个客人要买20杯奶茶)

       6.是否会导致饥饿:不会。(某进程/作业长期得不到服务)

       例题:各进程达到就绪队列的时间、需要的运行时间如下表所示。使用先来先服务调度算法,计算各进程的等待时间、平均等待时间、周转时间、平均周转时间、带权周转时间、平均带权周转时间。

进程到达时间运行时间
P107
P224
P341
P454

       分析:先来先服务调度算法:按照到达的先后顺序调度,事实上就是等待时间越久的越优先得到服务。(即作业来的越早,等待时间即越长,就越优先得到服务)因此,调度顺序为:P1—>P2—>P3—>P4。

       1.周转时间=完成时间—到达时间:P1=7—0=7;P2=11—2=9;P3=12—4=8;P4=16—5=11

       2.带权周转时间=周转时间/运行时间:P1=7/7=1;P2=9/4=2.25;P3=8/1=8;P4=11/4=2.75

       3.等待时间=周转时间-运行时间:P1=7—7=0;P2=9—4=5;P3=8—1=7;P4=11—4=7    

       注意:本例题中的进程都是纯计算型的进程,一个进程到达后要么在等待,要么在运行。如果是又有计算、又有I/O操作的进程,其等待时间就是周转时间—运行时间—I/O操作的时间。

       4.平均周转时间=(7+9+8+11)/4=8.75

       5.平均带权周转时间=(1+2.25+8+2.75)/4=3.5

       6.平均等待时间=(0+5+7+7)/4=4.75

2.9.2短作业优先(SJF,Shortest Job First)

       短作业(进程)优先调度算法是指对短作业(进程)优先调度的算法。短作业优先(SJF)调度算法从后备队列中选择一个或若干估计运行时间最短的作业,将它们调入内存运行;短进程优先(SPF)调度算法从就绪队列中选择一个估计运行时间最短的进程,将处理机分配给它,使之立即执行,直到完成或发生某事件而阻塞时,才释放处理机。

       1.算法思想:追求最少的平均等待时间、最少的平均周转时间、最少的平均带权周转时间。

       2.算法规则:最短的作业/进程优先得到服务(所谓“最短”,是指要求服务时间最短)。

       3.用于作业/进程调度:既可用于作业调度,也可用于进程调度。用于进程调度时成为“短进程优先(SPF,Shortest Process First)算法”。

       4.是否可抢占:SJF和SPF是非抢占式的算法。但是也有抢占式的版本——最短剩余时间优先算法(SRTN,Shortest Remaining Time Next)

       5.优缺点:优点:“最短的”平均等待时间、平均周转时间。

                        缺点:不公平。对短作业有利,对长作业不利。可能产生饥饿现象。另外,作业/进程的运行时间是由用户提供的,并不一定真实,不一定能做到真正的短作业优先。

        6.是否会导致饥饿:会。如果源源不断地有短作业/进程到来,可能是长作业/进程长时间得不到服务,产生“饥饿”现象。如果一直得不到服务,则成为“饿死”。

        例题1:各进程达到就绪队列的时间、需要的运行时间如下表所示。使用非抢占式短作业优先调度算法,计算各进程的等待时间、平均等待时间、周转时间、平均周转时间、带权周转时间、平均带权周转时间。

进程到达时间运行时间
P107
P224
P341
P454

       分析:短作业/进程优先调度算法:每次调度时选择当前已到达且运行时间最短的作业/进程。因此,调度顺序为:P1—>P3—>P2—>P4。

       1.周转时间=完成时间—到达时间:P1=7—0=7;P3=8—4=4;P3=12—2=10;P4=16—5=11

       2.带权周转时间=周转时间/运行时间:P1=7/7=1;P3=4/1=4;P2=10/4=2.5;P4=11/4=2.75

       3.等待时间=周转时间-运行时间:P1=7—7=0;P3=4—1=3;P2=10—4=6;P4=11—4=7   

       4.平均周转时间=(7+4+10+11)/4=8

       5.平均带权周转时间=(1+4+2.5+2.75)/4=2.56

       6.平均等待时间=(0+3+6+7)/4=4

       对比先来先服务调度(FCFS)算法的结果,显然SPF算法的平均等待时间/周转/带权周转时间都要更低。

       例题2:各进程达到就绪队列的时间、需要的运行时间如下表所示。使用抢占式短作业优先(抢占式的短作业优先算法又称“最短剩余时间优先算法(SRTN)”)调度算法,计算各进程的等待时间、平均等待时间、周转时间、平均周转时间、带权周转时间、平均带权周转时间。

       分析:最短剩余时间优先算法:每当有进程加入就绪队列改变时就需要调度,如果新到达的进程剩余(运行)时间比当前运行的进程剩余时间更短,则由新进程抢占处理机,当前运行进程重新回到就绪队列。另外,当一个进程完成时也需要调度因此,调度顺序为:P1—>P3—>P2—>P4。

        需要注意的是,当有新进程到达时就绪队列就会改变,就要按照上述规则进行检查。以下Pn(m)表示当前Pn进程剩余时间为m。各个时刻的情况如下:

        ①0时刻(P1到达):P1(7)//此时只有P1在就绪队列当中,当然为P1进程分配处理机,上处理机运行

        ②2时刻(P2到达):P1(5)、P2(4)//当P1运行了两个时间单位的时刻,P2到达了就绪队列,因此就绪队列发生了改变(就绪队列变为P1(5)、P2(4))。这时,系统对当前正在运行的进程P1剩余的五个单位时间与新到达的进程P2剩余的四个单位时间进行比较。P2的剩余时间更短,因此系统会选择P2这个进程,让它抢占处理机,然后P1重新回到就绪队列。

        ③4时刻(P3到达):P1(5)、P2(2)、P3(1)//同理,在4时刻P3抢占处理机,P2进入就绪队列。

        ④5时刻(P3完成且P4刚好到达):P1(5)、P2(2)、P4(4)//5时刻P3运行完成,同时P4进程到达就绪队列。这时,系统对队列中的进程P1、P2、P4剩余的时间进行比较。P2剩余时间为2,上处理机运行。

        ⑥7时刻(P2完成):P1(5)、P4(4)

   ⑦11时刻(P4完成):P1(5)

综上所述,易得例题2的答案为:  

       1.周转时间=完成时间—到达时间:P1=16—0=16;P2=7—2=5;P3=5—4=1;P4=11—5=6

       2.带权周转时间=周转时间/运行时间:P1=16/7=2.28;P2=5/4=1.25;P3=1/1=1;P4=6/4=1.5

       3.等待时间=周转时间-运行时间:P1=16—7=9;P2=5—4=1;P3=1—1=0;P4=6—4=2    

       4.平均周转时间=(16+5+1+6)/4=7

       5.平均带权周转时间=(2.28+1.25+1+1.5)/4=1.5

       6.平均等待时间=(9+1+0+2)/4=3

       对比非抢占式的短作业优先算法,显然抢占式的这几个指标又要更低。

由两道例题得出的值得注意的点:

        1.如果题目中未特别说明,所提到的“短作业/进程优先算法”默认非抢占式的。

        2.很多书上都会说“SJF调度算法的平均等待时间、平均周转时间最少”。严格来说,这个表述是错误的,不严谨的。之前的例子表明,最短剩余时间优先算法(SRTN)得到的平均等待时间、平均周转时间还要更少。应该加上一个条件“在所有进程同时可运行时,采用SJF调度算法的平均等待时间、平均周转时间最少”;或者说“在所有进程都几乎同时到达时,采用SJF调度算法的平均等待时间、平均周转时间最少”;如果不加上述前提条件,则应该说“抢占式的短作业/进程优先调度算法(最短剩余时间优先,SRNT算法)的平均等待时间、平均周转时间最少”。

  3.虽然严格来说,SJF的平均等待时间、平均周转时间并不一定最少,但相比于其他算法(如FCFS),SJF依然可以获得较少的平均等待时间、平均周转时间。

        4.如果选择题中遇到“SJF算法的平均等待时间、平均周转时间最少”的选择,那最好判断其他选项是不是有很明显的错误,如果没有更合适的选项,那也应该选择该项。

       对FCFS和SJF两种算法的思考:

       FCFS算法是在每次调度的时候选择一个等待时间最长的作业(进程)为其服务。但是没有考虑到作业的运行时间,因此导致了对短作业不友好的问题。

       SJF算法是选择一个执行时间最短的作业为其服务。但是又完全不考虑各个作业的等待时间,因此导致了对长作业不友好的问题,甚至还会造成饥饿问题。

       怎样设计一个算法,既考虑到各个作业的等待时间,也能兼顾运行时间呢?

2.9.3高响应比优先(HRRN,Highest Response Ratio Next)

       高响应比优先调度算法主要用于作业调度,是对FCFS调度算法和SJF调度算法的一种综合平衡,同时考虑了每个作业的等待时间和估计的运行时间。在每次进行作业调度时,先计算后备作业队列中每个作业的响应比,从中选择出响应比最高的作业投入运行。

       1.算法思想:要综合考虑作业/进程的等待时间和要求服务的时间。

       2.算法规则:在每次调度时先计算各个作业/进程的响应比,选择响应比最高的作业/进程为其服务。响应比=(等待时间+要求服务时间)÷要求服务时间,响应比≥1。

       3.用于作业/进程调度:既可用于作业调度,也可用于进程调度。

       4.是否可抢占:非抢占式的算法。因此只有当前运行的作业/进程主动放弃处理机时,才需要调度,才需要计算响应比。

       5.优点:综合考虑了等待时间和运行时间(要求服务时间)。等待时间相同时,要求服务时间短的优先(SJF的优点);要求服务时间(运行时间)相同时,等待时间长的优先(FCFS的优点)。无缺点//并不是说高响应比优先算法是完美无缺的算法。考虑对用户的公平性、平均周转时间、平均等待时间等评价系统中,高响应比是很优秀的算法。但是考虑到任务的紧急程度等因素,高响应比优先算法无能为力。

        6.是否会导致饥饿:不会。对于一个长作业来说,随着等待时间越来越久,其响应比也会越来越大,从而避免了长作业饥饿的问题。

       例题各进程达到就绪队列的时间、需要的运行时间如下表所示。使用高响应比优先调度算法,计算各进程的等待时间、平均等待时间、周转时间、平均周转时间、带权周转时间、平均带权周转时间。

进程到达时间运行时间
P107
P224
P341
P454

       分析:高响应比优先算法是非抢占式的调度算法,只有当前运行的进程主动放弃CPU时(正常/异常完成,或主动阻塞),才需要进行调度,调度时计算所有就绪进程的响应比,选响应比最高的进程上处理机。响应比=(等待时间+要求服务时间)÷要求服务时间

        ①0时刻:只有P1到达就绪队列,P1上处理机。

        ②7时刻(P1主动放弃CPU):就绪队列中有P2(响应比=(5+4)/4=2.25)P3响应比=(3+1)/1=4)、P4(响应比=(2+4)/4=1.5)//P2和P4要求服务时间一样,但P2等待时间长,所以必然是P2响应比更大。

        ③8时刻(P3完成):P2(2.5)、P4(1.75)。

        ④12时刻(P2完成):就绪队列只剩下P4

知识回顾

       注:这几种算法主要关系对用户的公平性、平均周转时间、平均等待时间等评价系统整体性能的指标,但是不关心“响应时间”,也并不区分任务的紧急程度,因此对于用户来说,交互性非常糟糕。因此这三种算法一般适合用于早期的批处理系统,当然,FCFS算法也常结合其他算法使用,现在也扮演这很重要的角色。适用于交互式系统的调度算法将在下个小结介绍。

2.10调度算法Ⅱ

知识总览

2.10.1时间片轮转(RR,Round-Robin)

       时间片轮转调度算法主要适用于分时操作系统。在这种算法中,系统将所有就绪进程按到达时间的先后次序排成一个队列,进程调度程序总是选择就绪队列中的第一个进程执行,即先来先服务的原则,但仅能运行一个时间片,如100ms。在使用完一个时间片后,即使进程并未完成其运行,他也必须释放出(被剥夺)处理机给下一个就绪进程,而被剥夺的进程返回到就绪队列的末尾重新排队,等待再次运行。

       1.算法思想:公平地、轮流地为各个进程服务,让每个进程在一定时间间隔内都可以得到响应。

       2.算法规则:按照各进程到达就绪队列的顺序,轮流让各个进程执行一个时间片(如100ms)。若进程未在一个时间片内执行完,则剥夺处理机,将进程重新放到就绪队列队尾重新排队。

       3.用于作业/进程调度:用于进程调度(只有作业放入内存建立了相应的进程后,才能被分配处理机时间片)。

       4.是否可抢占:若进程未能在时间片内运行完,将被强行伯多处理机使用权,因此时间片轮转调度算法属于抢占式的算法。由时钟装置发出时钟中断来通知CPU时间片已到。

       5.优缺点:优点:公平;响应快,适用于分时操作系统。

                        缺点:由于高频率的进程切换,因此有一定开销;不区分任务的紧急程度。

        6.是否会导致饥饿:不会。

        7.补充:时间片太大或太小的影响

       例题各进程达到就绪队列的时间、需要的运行时间如下表所示。使用时间片轮转调度算法,分析时间片大小分别是2、5时的进程运行情况。//时间片轮转调度算法常用于分时操作系统,更注重“响应时间”,因而此处不计算周转时间。

进程到达时间运行时间
P105
P224
P341
P456

       分析:时间片轮转调度算法:轮流让就绪队列中的进程依次执行一个时间片(每次选择的都是排在就绪队列对头的进程)。

一、时间片大小为2//注:Ⅰ、以下“Pn(m)”表示就绪队列中的进程n(进程的剩余运行时间)

                                        Ⅱ、“【】” 内  “→”  表示  “就绪队列队头→队尾”。

        ①0时刻【P1(5)】:0时刻只有P1到达就绪队列,让P1上处理机运行一个时间片。

        ②2时刻【P2(4)—>P1(3)】:2时刻P2到达就绪队列,P1运行走完一个时间片,被剥夺处理机,重新放到队尾。此时P2排在对头,因此让P2上处理机。(注意:2时刻,P1下处理机,同一时刻新进程P2到达,如果在题目中遇到这种情况,默认新到达的进程先进入就绪队列

         ③4时刻【P1(3)—>P3(1)—>P2(2)】:4时刻,P3到达,先插到就绪队尾,紧接着,P2下处理机也插到队尾。

         ⑤5时刻【P3(1)—>P2(2)—>P4(6)】:5时刻,P4到达插到就绪队尾(注意:由于P1的时间片还没用完,因此暂时不调度。另外,此时P1处于运行态,并不在就绪队列中)

         ⑥6时刻【P3(1)—>P2(2)—>P4(6)—>P1(1)】:6时刻,P1时间片用完,下处理机,重新放回就绪队尾,发生调度。

         ⑦7时刻【P2(2)—>P4(6)—>P1(1)】:虽然P3的时间片没用完,但是由于P3只需运行1个单位的时间,运行完了会主动放弃处理机,因此也会发生调度。队头进程P2上处理机。

         ⑧9时刻【P4(6)—>P1(1)】:进程P2时间片用完,并刚好运行完毕,发生调度,P4上处理机。

         ⑨11时刻【P1(1)—>P4(4)】:P4时间片用完,重新回到就绪队列。P1上处理机。

         ⑩12时刻【P4(4)】:P1运行完,出动放弃处理机,此时就绪队列中只剩P4,P4上处理机。

         ⑪14时刻():就绪队列为空,因此让P4接着运行一个时间片。

         ⑫16时刻:所有进程运行结束。

二、时间片大小为5

         ①0时刻【P1(5)】:0时刻只有P1到达就绪队列,让P1上处理机运行一个时间片。

         ②2时刻【P2(4)】:P2到达,但P1的时间片尚未结束,因此暂不调度。

         ③4时刻【P2(4)—>P3(1)】:P3到达,但P1的时间片尚未结束,因此暂不调度。

         ④5时刻P2(4)—>P3(1)—>P4(6)】:P4到达,同时,P1运行结束。发生调度,P2上处理机。

         ⑤9时刻【P3(1)—>P4(6)】:P2运行结束,虽然时间片没用完,但是会主动放弃处理机。发生调度。

         ⑥10时刻【P4(6)】:P3运行结束,虽然时间片没用完,但是会主动放弃处理机。发生调度。

         ⑦15时刻():P4时间片用完,但就绪队列为空,因此会让P4继续执行一个时间片。

         ⑧16时刻():P4运行完,主动放弃处理机。所有进程运行完。

       思考:若采用先来先服务调度算法,则调度情况为下图。这与时间片大小为5的时间片轮转调度算法一致。

       时间片轮转调度算法的时间片太大或太小的影响:

       如果时间片太大,使得每个进程都可以在一个时间片内就完成,则时间片轮转调度算法退化为先来先服务调度算法,并且会增大进程响应时间。因此时间片不能太大(如何理解“会增大进程响应时间”?答:系统中由10个进程并发执行,如果时间片为1秒,则一个进程被响应可能需要等9秒…也就是说,如果用户在自己进程的时间片外通过键盘发出调式命令,可能需要等待九秒才能被系统响应。)

        另一方面,进程调度、切换是有时间代价的(保存、恢复运行环境),因此如果时间片太小,会导致进程切换过于频繁,系统会花大量时间来处理进程切换,从而导致实际用于进程执行的时间比例减少。可见时间片也不能太小

2.10.2优先级调度算法

       优先级调度算法又称优先权调度算法,它既可用于作业调度,又可用于进程调度。该算法中的优先级用于描述作业运行的紧迫程度。在作业调度中,优先级调度算法每次从后备作业队列中选择优先级最高的一个或几个作业,将它们调入内存,分配必要的资源,创建进程并放入就绪队列。在进程调度中,优先级调度算法每次从就绪队列中选择优先级最高的进程,将处理机分配给它,使之投入运行。

      1.算法思想:随着计算机的发展,特别是实时操作系统的出现,越来越多的应用场景需要根据人物的紧急程度来决定处理顺序。

       2.算法规则:每个作业/进程有各自的优先级,调度是选择优先级最高的作业/进程。

       3.用于作业/进程调度:既可用于作业调度,又可用于进程调度。甚至,还会用于在之后会学习的I/O调度中。

       4.是否可抢占:抢占式、非抢占式都有。做题时的区别在于:非抢占式只需在进程主动放弃处理机时进行调度即可,而抢占式还需再就绪队列变化时,检查是否会发生抢占。

       5.优缺点:优点:用优先级区分紧急程度、重要程度,适用于实时操作系统。可灵活地调整对各种作业/进程的偏好程度。

                        缺点:若源源不断地有高优先级进程到来,则可能导致饥饿。

        6.是否会导致饥饿:会。

       例题1各进程达到就绪队列的时间、需要的运行时间、进程优先数如下表所示。使用非抢占式优先级调度算法,分析进程运行情况。(注:优先数越大,优先级越高)

进程到达时间运行时间优先数
P1071
P2242
P3413
P4542

        分析:非抢占式的优先级调度算法:每次调度时选择当前已到达优先级最高的进程。当前进程主动放弃处理机时发生调度。

         注:以下括号内表示当前处于就绪队列的进程。

         ①0时刻(P1):只有P1到达,P1上处理机。

         ②7时刻(P2、P3、P4):P1运行完成主动放弃处理机,其余进程都已到达,P3优先级最高,P3上处理机。

         ③8时刻(P2、P4):P3完成,P2、P4优先级相同,由于P2先到达,因此P2优先上处理机。

         ④12时刻(P4):P2完成,就绪队列只剩P4,P4上处理机。

         ⑤16时刻():P4完成,所有进程都结束。

       例题2各进程达到就绪队列的时间、需要的运行时间、进程优先数如下表所示。使用抢占式优先级调度算法,分析进程运行情况。(注:优先数越大,优先级越高)

       分析:抢占式的优先级调度算法:每次调度时选择当前已到达优先级最高的进程。当前进程主动放弃处理机时发生调度。另外,当就绪队列发生改变时也要检查是否会发生抢占。

        注:以下括号内表示当前处于就绪队列的进程。

        ①0时刻(P1):只有P1到达,P1上处理机。

        ②2时刻(P2):P2到达就绪队列,优先级比P1更高,发生抢占。P1回到就绪队列,P2上处理机。

        ③4时刻(P1、P3):P3到达,优先级比P1更高,P2回到就绪队列,P3抢占处理机。

        ④5时刻(P1、P2、P4):P3完成主动释放处理机,同时,P4也到达,由于P2比P4更先进入就绪队列,因此选择P2上处理机。

        ⑤7时刻(P1、P4):P2完成,就绪队列只剩P1、P4,P4上处理机。

        ⑥11时刻(P1):P4完成,P1上处理机。

        ⑦16时刻():P1完成,所有进程均完成。

补充:

        1.就绪队列未必只有一个,可以按照不同优先级来组织。另外,也可以把优先级高的进程排在更靠近队头的位置。

        2.根据优先级是否可以动态改变,可将优先级分为静态优先级动态优先级两种。静态优先级:创建进程时确定,之后一直不变;动态优先级:创建进程时有一个初始值,之后会根据情况动态地调整优先级。

        3.如何合理地设置各类进程的优先级?

         通常:系统进程优先级高于用户进程

                    前台进程优先级高于后台进程//用户体验好

                    操作系统更偏好I/O型进程(或称I/O繁忙型进程)// I/O设备和CPU可以并行工作。如果优先让I/O繁忙型进程优先运行的话,则越有可能让I/O设备尽早投入工作,则资源利用率、系统吞吐量都会得到提升。

         注:与I/O型进程相对的是计算型进程(或称CPU繁忙型进程)

         4.如果采用的是动态优先级,什么时候应该调整?

         可以从追求公平、提升资源利用率等角度考虑。如果某进程在就绪队列中等待了很长时间,则可以适当提升其优先级;如果某进程占用处理机运行了很长时间,则可适当降低其优先级。

         对时间片轮转、优先级调度两种算法的思考:

         1.时间片轮转调度算法可以让各个进程得到及时的响应。

         2.优先级调度算法可以灵活地调整各种进程被服务的机会。

         能否对其他算法做个折中权衡?得到一个综合表现优秀平衡的算法?—>

2.10.3多级反馈队列调度算法

        多级反馈队列调度算法是时间片轮转调度算法和优先级调度算法的综合与发展。通过动态调整进程优先级和时间片大小,多级反馈队列调度算法可以兼顾多方面的系统目标。

       1.算法思想:对其他调度算法的折中权衡。

       2.算法规则:①.设置多级就绪队列,各级队列优先级从高到低,时间片从小到大。

                            ②.新进程到达时先进入第1级队列,按FCFS原则排队等待被分配时间片,若用完时间片进程还未结束,则进程进入下一级队列队尾。如果此时已经是在最下级的队列,则从新放回该队列队尾。

                            ③.只有第k级队列为空时,才会为k+1级队头的进程分配时间片。

       3.用于作业/进程调度:用于进程调度。

       4.是否可抢占:抢占式的算法。在k级队列的进程运行过程中,若更上级的队列(1~k-1级)中进入了一个新进程,则由于新进程处于优先级更高的队列中,因此新进程会抢占处理机,原来运行的进程放回k级队列队尾。

       5.优点:对各类型进程相对公平(FCFS的优点);每个新到达的进程都可以很快就得到响应(RR的优点);短进程只用较少的时间就可完成(SPF的优点);不必实现估计进程的运行时间(避免用户作假);可灵活地调整对各类进程的偏好程度,比如CPU密集型进程、I/O密集型进程(拓展:可以将因I/O而阻塞的进程重新放回原队列,这样I/O型进程就可以保持较高优先级)。

        6.是否会导致饥饿:会。如果有源源不断的新进程进入,则会导致饥饿。

       例题各进程达到就绪队列的时间、需要的运行时间如下表所示。使用多级反馈队列调度算法,分析进程运行情况。

进程到达时间运行时间
P108
P214
P351

      分析:设置多级就绪队列,各级队列优先级高到低时间片小到大新进程到达时先进入第1级队列,按FCFS原则排队等待被分配时间片。若用完时间片进程还未结束,则进程进入下一级队列队尾。如果此时已经在最下级的队列,则重新放回最下级队列队尾。只有第k级队列为空时,才会为k+1级队头的进程分配时间片。被抢占处理机的进程重新放回队尾。

        假设第1级队列时间片大小为1、第2级队列时间片大小为2、第3级队列时间片大小为4。

        易得,处理机调度进程顺序为//()内为处理机运行时间

        P1(1)—>P2(1)—>P1(2)—>P2(1)—>P3(1)—>P2(2)—>P1(4)—>P1(1)

知识回顾

       注:比起早期的批处理操作系统来说,由于计算机造价大幅降低,因此之后出现的交互式操作系统(包括分时操作系统、实时操作系统等)更注重系统的响应时间、公平性等指标。而这几种算法恰好也能较好地满足交互式系统地需求。因此这三种算法适合用于交互式系统 。(比如UNIX使用的就是多级反馈队列调度算法)

       本节开头提出的问题的参考答案如下:

       调度算法有哪几种?结合第1章学习的分时操作系统和实时操作系统,思考有没有哪种调度算法比较适合这两种操作系统。

        答:本节介绍的调度算法有先来先服务调度算法、短作业优先调度算法、优先级调度算法、高响应比优先调度算法、时间片轮转调度算法、多级反馈队列调度算法6种。

        先来先服务算法和短作业优先算法无法保证及时地接收和处理问题,因此无法保证在规定的时间间隔内响应每个用户的需求,也同样无法达到实时操作系统的及时性需求。优先级调度算法按照任务的优先级进行调度,对于更紧急的任务给予更高的优先级,适合实时操作系统。

         高响应比优先调度算法、时间片轮转调度算法、多级反馈队列调度算法都能保证每个任务在一定时间内分配到时间片,并轮流占有CPU,适合分时操作系统。

2.11进程同步与进程互斥

在学习本节时,请思考以下问题:

1)为什么要引入进程同步的概念?

2)不同的进程之间会存在什么关系?

3)当单纯用本节介绍的方法解决这些问题时会遇到什么新问题吗?

知识总览

2.11.1什么是进程同步?

1.知识点回顾

      进程具有异步性的特征。异步性是指,各并发执行的进程以各自独立的、不可预知的速度向前推进。

2.基本概念

       在多道程序环境下,程序是并发执行的,不同进程之间存在着不同的相互制约关系。为了协调进程之间的相互制约关系,引入了进程同步的概念。同步亦称直接制约关系,它是指为完成某种任务而建立的两个或多个进程,这些进程因为需要在某些位置上协调它们的工作次序而产生的制约关系。进程间的直接制约关系就是源于它们之间的相互合作。

       举两个例子帮助理解:

        例1:让系统计算1+2×3,假设系统产生两个进程:一个是加法进程,一个是乘法进程。要让计算结果是正确的,一定要让加法进程发生在乘法进程之后,但实际上操作系统具有异步性,若不加以制约,加法进程发生在乘法进程之前是绝对有可能的,因此要制定一定的机制去约束加法进程,让它在乘法进程完成之后才发生。

        例2:用户需要读刚刚录入的数据。假设系统产生两个进程:一个是写数据进程,一个是读数据进程。要让用户成功读入新录入的数据,一定要让读进程发生在写进程之后。但读进程和写进程并发运行,由于并发必然导致异步性,因此“写数据”和“读数据”两个操作执行的前后顺序是不确定的。而在实际中,有必须按照“写数据—>读数据”的顺序来执行的。

        如何解决这种异步问题,就是“进程同步”所讨论的内容。

2.11.2什么是进程互斥?

1.知识点回顾

       进程的“并发”需要“共享”的支持。各个并发执行的进程不可避免地需要共享一些系统资源(比如内存,又比如打印机、摄像头这样的I/O设备)

       互斥共享方式:系统中的某些资源,虽然可以共计多个进程使用,但一个时间段内只允许一个进程访问该资源。

        同步共享方式:系统中的某些资源,允许一个时间段内有多个进程“同时”对他们进行访问

        我们把一个时间段内只允许一个进程使用的资源成为临界资源。许多物理设备(比如摄像头、打印机)都属于临界资源。此外还有许多变量、数据、内存缓冲区等都属于临界资源。

2.基本概念

        对临界资源的访问,必须互斥地进行。互斥,亦称间接制约关系进程互斥指当一个进程访问某临界资源时,另一个想要访问该临界资源地进程必须等待。当前访问临界资源地进程访问结束,释放该资源之后,另一个进程才能去访问临界资源。

       对临界资源的互斥访问,可以在逻辑上分为如下四个部分(逻辑上的划分,理解即可)

do{
   entry section;     //进入区
   critical section;  //临界区
   exit section;      //退出区
   remainder section; //剩余区
}while(true)

       进入区:负责检查是否可以进入临界区,若可以进入,则应设置正在访问临界资源的标志(可理解为“上锁”),以阻止其他进程同时进入临界区。

       临界区:访问临界资源的那段代码。

       退出区:负责解除正在访问临界资源的标志(可理解为“解锁”)

       剩余区:做其他处理。

       注意:临界区是进程中访问临界资源的代码段。

                  进入区退出区负责实现互斥的代码段。

                  临界区也可称为“临界段”。

       思考:如果一个进程暂时不能进入临界区,那么该进程是否应该一直占着处理机?该进程有没有可能一直进不了临界区?

       答:为了实现对临界资源的互斥访问,同时保证系统整体性能,需要遵循以下原则:

       1.空闲让进。临界区空闲时,可以允许一个请求进入临界区的进程立即进入临界区。

       2.忙则等待。当已有进程进入临界区时,其他试图进入临界区的进程必须等待。

       3.有限等待。对请求访问的进程,应保证能在有限时间内进入临界区(保证不会饥饿)。

       4.让权等待。当进程不能进入临界区时,应立即释放处理机,防止进程忙等待。

知识回顾

2.12进程互斥的软件实现方法

在学习本节时,请思考以下问题:

1)理解各个算法的思想、原理

2)结合上小节学习的“实现互斥的四个逻辑部分”,重点理解各算法在进入区、退出区都做了什么。

3)分析各算法存在的缺陷(结合“实现互斥要遵循的四个原则”进行分析)

知识总览

2.12.1单标志法

        算法思想:两个进程在访问完临界区后会把使用临界区的权限转交给另一个进程。也就是说每个进程进入临界区的权限只能被另一个进程赋予

        该算法设置一个公用整型变量turn,用于指示被允许进入临界区的进程编号,即若turn=0,则允许P0进程进入临界区。该算法可确保每次只允许一个进程进入临界区。

        int turn=0;//turn 表示当前允许进入临界区的进程号(turn变量背后的逻辑:表达“谦让”)

P0进程
while(turn!=0);  ①//进入区
critical section;  ②//临界区
turn=1;            ③//退出区
remainder section; ④//剩余区
P1进程:
while(turn!=1); ⑤//进入区
critical section; ⑥//临界区
turn=0;           ⑦//退出区
remainder section;⑧//剩余区

       分析一下运行情况:       

       turn的初始值为0,即刚开始只允许0号进程进入临界区。若P1先上处理机运行,则会一直卡在⑤。直到P1的时间片用完,发生调度,切换P0上处理机运行。代码①不会卡住P0,P0可以正常访问临界区,在P0访问临界区期间即使切换回P1,P1依然会卡在⑤。所以只有P0在退出区将turn改为1后,P1才能进入临界区。

        因此,该算法可以实现“同一时刻最多只允许一个进程访问临界区”

        该算法的弊端:

        两个进程必须交替进入临界区,若某个进程不再进入临界区,则另一个进程也将无法进入临界区(违背“空闲让进”)。这样很容易造成资源利用不充分。//若P0顺利进入临界区并从临界区离开,则此时临界区是空闲的,但P1并没有进入临界区的打算,turn=1一直成立,P0就无法再次进入临界区(一直被while死循环困住)

2.12.2双标志先检查法

        算法思想:设置一个布尔型数组flag[ ],数组中各个元素用来标记各进程想进入临界区的意愿,比如“flag[0]=ture” 意味着0号进程P0现在想要进入临界区。每个进程在进入临界区之前先检查当前有没有别的进程想进入临界区,如果没有,则把自身对应的标志flag[i]设为ture,之后开始访问临界区。    

        bool flag[2]; //定义一个布尔型变量的数组,表示进入临界区意愿的数组(背后含义:“表达意愿”)

       flag[0]=false;

       flag[1]=false;//刚开始设置为两个进程都不想进入临界区

P0进程:
while(flag[1]);  ①//进入区 如果P1也想进入临界区,则P0循环等待
flag[0]=true;     ②//进入区 标记为进程想要进入临界区
critical section; ③//访问临界区
falg[0]=false;     ④//访问完临界区修改标记为P0不想使用临界区
remainder section;⑤//剩余区
P1进程:
while(flag[0]);  ⑥//进入区 如果P0也想进入临界区,则P1循环等待
flag[1]=true;     ⑦//进入区 标记为进程想要进入临界区
critical section; ⑧//访问临界区
falg[1]=false;     ⑨//访问完临界区修改标记为P1不想使用临界区
remainder section;⑩//剩余区

       分析一下运行情况:

       在每个进程访问临界区资源之前,先检查临界资源是否有别的进程想进入临界区。当P0想进入临界区,P1就一直循环等待。

       该算法的优缺点:

       优点:不用交替进入,可连续使用;缺点:P0和P1可能同时进入临界区。因为进程P0和P1并发执行,若按序列①⑤②⑥③⑦…的顺序执行,P0和P1将会同时访问临界区,即在检查对方的flag后和切换自己的flag前有一段时间,检查都通过。因此,双标志先检查法的主要问题是:违反“忙则等待”原则

2.12.3双标志后检查法

       算法思想:双标志先检查法的改版。前一个算法的问题是先“检查”后“上锁”,但是这两个操作又无法一气呵成因此导致了两个进程同时进入临界区的问题。因此,人们又先想到先“上锁”后“检查”的方法,来避免上述问题。

       bool flag[2]; //定义一个布尔型变量的数组,表示进入临界区意愿的数组(背后含义:“表达意愿”)

       flag[0]=false;

       flag[1]=false;//刚开始设置为两个进程都不想进入临界区

P0进程:
flag[0]=true;     ②//进入区 标记为进程想要进入临界区
while(flag[1]);  ①//进入区 如果P1也想进入临界区,则P0循环等待
critical section; ③//访问临界区
falg[0]=false;     ④//访问完临界区修改标记为P0不想使用临界区
remainder section;⑤//剩余区
P1进程:
flag[1]=true;     ⑦//进入区 标记为进程想要进入临界区
while(flag[0]);  ⑥//进入区 如果P0也想进入临界区,则P1循环等待
critical section; ⑧//访问临界区
falg[1]=false;     ⑨//访问完临界区修改标记为P1不想使用临界区
remainder section;⑩//剩余区

       缺点:若按照①⑤②⑥…的顺序执行,P0和P1将都无法进入临界区。两个进程几乎都想进入临界区时,它们分别将自己的标志flag设置为true,并且同时检测对方的状态(执行while语句),发现对方也要进入临界区时,双方谦让,结果谁也进入不了临界区,从而导致“饥饿”现象。

       因此,双标志后检查法虽然解决了“忙则等待”的问题,但是又违背了“空闲让进”和“有限等待”原则,会因各进程都长期无法访问临界资源而产生“饥饿”现象。

2.12.4Peterson算法

       算法思想:结合双标志法、单标志法的思想。如果双方都争着想进入临界区,那可以让进程尝试“孔融让梨”(谦让)。

       为了防止两个进程为进入临界区而无限期等待,又设置了变量turn,每个进程在先设置自己的标志后再设置turn标志。这时,再同时检测另一个进程状态标志和允许进入标志,以便保证两个进程同时要求进入临界区时,只允许一个进程进入临界区。

       bool flag[2]; //定义一个布尔型变量的数组,表示进入临界区意愿的数组(背后含义:“表达意愿”)

       flag[0]=false;

       flag[1]=false;//刚开始设置为两个进程都不想进入临界区

       int turn=0;//turn 表示当前允许进入临界区的进程号(turn变量背后的逻辑:表达“谦让”)

P0进程:
flag[0]=true;              ②//进入区 标记为进程想要进入临界区
turn=1;                    ②//进入区
while(flag[1]&&turn==1);  ③//进入区 如果P1也想进入临界区,则P0循环等待
critical section;          ④//访问临界区
falg[0]=false;              ⑤//访问完临界区修改标记为P0不想使用临界区
remainder section;          //剩余区
P1进程:
flag[1]=true;              ⑥//进入区 标记为进程想要进入临界区
turn=0;                    ⑦//进入区
while(flag[0]&&turn==0);  ⑧//进入区 如果P0也想进入临界区,则P1循环等待
critical section;          ⑨//访问临界区
falg[1]=false;              ⑩//访问完临界区修改标记为P1不想使用临界区
remainder section;          //剩余区

        具体如下:考虑进程P0,一旦设置flag[0]=true,就表示它想要进入临界区,同时turn=1,此时若进程P1已在临界区中,符合进程P0中while的条件,则P0不能进入临界区。若P1不想要进入临界区,即flag[1]=false,循环条件不符合,则P0可以顺利进入,反之亦然,本算法的思想是算法一和算法三的结合。利用flag解决临界资源的互斥访问,利用turn解决“饥饿现象”

        Peterson算法用软件方法解决了进程互斥问题,遵循了空闲让进、忙则等待、有限等待三个原则,但是依然未遵循让权等待的原则(因为进程会一直卡在while循环这里)

        Peterson算法相较于之前三种软件解决方案来说,是最好的,但依然不够好。

知识回顾

2.13进程互斥的硬件实现方法

       通过硬件支持实现临界段问题的方法称为低级方法,或称元方法

       学习提示:

       1.理解各方法的原理

       2.了解各方法的优缺点

知识总览

2.13.1中断屏蔽方法    

       利用“开/关中断指令”实现(与原语的实现思想相同,即在某进程开始访问临界区到结束访问为止都不允许被中断,也就不能发生进程切换,因此也不可能发生两个进程同时访问临界区的情况)

       其典型模式为:

        ……

        关中断;//关中断后即不允许当前进程被中断,也必然不会发生进程切换。

        临界区;

        开中断;//直到当前进程访问完临界区,再执行开中断指令,才有可能别的进程上处理机并访问临界区。

        优缺点:优点:简单、高效。

                       缺点:不适用于多处理机;只适用于操作系统内核进程,不适用于用户进程(因为开/关中断指令只能运行在内核态,这组指令如果能让用户随意使用会很危险)

2.13.2TestAndSet指令

        简称TS指令,也有地方称为TestAndSetLock指令,或TSL指令。

        TSL指令是用硬件实现的,是原子操作,即执行改代码时不允许被中断,只能一气呵成。以下是用C语言描述的逻辑(了解,只是逻辑)

//布尔型共享变量lock表示当前临界区是否被加锁
//true表示已加锁,false表示未加锁
bool TestAndSet(bool*lock){
  bool old;
  old=*lock;  //old用来存放lock原来的值
  *lock=true;//无论之前是否已加锁,都将lock设为true
  return old;//返回lock原来的值
}
//以下是使用TSL指令实现互斥的算法逻辑
while(TestAndSet(&lock));//"上锁"并"检查"
临界区代码段……
lock=false;                  //"解锁"
剩余区代码段……

       若刚开始lock是false,则TSL返回的old值为false,while循环条件不满足,直接跳过循环,进入临界区。若刚开始lock是true,则执行TLS后old返回的值为true,while循环条件满足,会一直循环,直到当前访问临界区的进程在退出区进行“解锁”。

       相比软件实现方法,TSL指令把“上锁”和“检查”操作用硬件的方式变成了一气呵成的原则操作。

       优缺点:优点:实现简单,无需像软件实现方法那样严格检查是否会有逻辑漏洞;适用于多处理机环境。

                      缺点:不满足“让权等待”原则,暂时无法进入临界区的进程会占用CPU并循环执行TSL指令,从而导致“忙等”。

2.13.3Swap指令

       有的地方也叫Exchange指令,或简称XCHG指令。

       Swap指令是用硬件实现的,是原子操作,即执行改代码时不允许被中断,只能一气呵成。以下是用C语言描述的逻辑(了解,只是逻辑)

//Swap指令的作用是交换两个变量的值
Swap(bool *a,bool *b){
  bool temp;
  temp=*a;
  *a=*b;
  *b=temp;
}
//以下是用Swap指令是西安互斥的算法逻辑
//lock表示当前临界区是否会被加锁
bool old=true;
while(old==true)
   Swap(&lock,&old);
临界区代码段…
lock=false;
剩余区代码段…

       逻辑上来看Swap和TSL并无太大区别,都是线记录下此时临界区是否已经被上锁(记录在old变量上),再将上锁标记lock设置为true,最后检查old,如果old为false则说明之前没有别的进程对临界区上锁,则可跳出循环,进入临界区。

       优缺点:优点:实现简单,无需像软件实现方法那样严格检查是否会有逻辑漏洞;适用于多处理机环境。

                      缺点:不满足“让权等待”原则,暂时无法进入临界区的进程会占用CPU并循环执行TSL指令,从而导致“忙等”。

知识回顾

2.14信号量机制

       复习回顾+思考:之前学习的这些进程互斥的解决方案分别存在哪些问题?

       进程互斥的四种软件实现方式(单标志法、双标志先检查、双标志后检查、Peterson算法)

       进程互斥的三种硬件实现方式(中断屏蔽方法、TS/TSL指令、Swap/XCHG指令)

       1.在双标志先检查法中,进入区的“检查”、“上锁”操作无法一气呵成,从而导致了两个进程有可能同时进入临界区的问题。

       2.所有的解决方案都无法实现“让权等待”.

知识总览

2.14.1信号量机制的基本概念     

       信号量机制是一种功能较强的机制,可用来解决互斥与同步问题。

       信号量其实就是一个变量(可以是一个整数,也可以是更复杂的记录型变量),可以用一个信号量来表示系统中某种资源的数量,比如:系统中只有一台打印机,就可以设置一个初值为1的信号量。

       原语是一种特殊的程序段,其执行只能一气呵成,不可被中断。原语是由关中断/开中断指令实现的。软件解决方案的主要问题是由“进入区的各种操作无法一气呵成”,因此如果能把进入区、退出区的操作都用“原语”实现,使这些操作能“一气呵成”就能避免问题。(“检查”和“上锁”一气呵成)

       一对原语wait(S)原语和signal(S)原语,可以把原语理解为我们自己写的函数,函数名分别为wait和signal,括号里的信号量S其实就是函数调用时传入的一个参数。wait、signal原语常简称为P、V操作(来自荷兰语proberen和verhogen)。因此,做题的时候常把wait(S)、signal(S)两个操作分别写为P(S)、V(S)

2.14.2信号量机制——整型信号量

       用一个整数型的变量作为信号量,用来表示系统中某种资源的数量。//与普通整数变量的区别:对信号量的操作只有三种,即初始化、P操作、V操作。

Eg:某计算机系统中由一台打印机…

int S=1;//初始化整型信号量S,表示当前系统中可用的打印机资源数

void wait(int S){   //wait原语,相当于“进入区”
  while(S<=0);      //如果资源数不够,就一直循环等待
  S=S-1;             //如果资源数够,则占用一个资源
}

void signal(int S){ //signal原语,相当于“退出区”
  S=S+1;
}
进程P0申请访问临界区:
……
wait(S);    //进入区,申请资源
使用打印机资源……//临界区,访问资源
signal(S);  //退出区,释放资源
……

        “检查”和“上锁”一气呵成,避免了并发、异步导致的问题。存在的问题:不满足“让权等待”原则,会发生“忙等”(不让权等待)

2.14.3信号量机制——记录型信号量

        整型信号量的缺陷是存在“忙等”问题,因此人们又提出了“记录型信号量”,即用记录型数据结构表示的信号量。

/*记录型信号量的定义*/
typedef struct{
  int value;         //剩余资源数
  struct process *L; //等待队列
}semaphore;
/*某进程需要使用资源时,通过wait原语申请*/
void wait(semaphore S){
  S.value--;
  if(S.value<0){
    block(S.L);
  }

如果剩余资源数不够,使用block原语使进程从运行态进入阻塞态
并挂到信号量S的等待队列(即阻塞队列)中。
/*进程使用完资源后,通过signal原语释放*/
viod signal(semaphore S){
  S.value++;
  if(S.value<=0){
  wakeup(S.L);
  }
}

释放资源后,若还有别的进程在等待这种资源,则使用wakeup原语唤醒等待队列中的一个进程
该进程从阻塞态变为就绪态

      在考研题目中wait(S)、signal(S)也可以记为P(S)、V(S),这对原语可用于实现系统资源的“申请”和“释放”S.value的初值表示系统中某种资源的数目

      对信号量S的一次P操作意味着进程请求一个单位的该类资源,因此需要执行S.value--,表示资源数减1,当S.value<0时表示该类资源已分配完毕,因此进程应调用block原语进行自我阻塞(当前运行的进程从运行态—>阻塞态),主动放弃处理机,并插入该类资源的等待队列S.L中。可见,该机制遵循了“让权等待”原则,不会出现“忙等”现象。

      对信号量S的一次V操作意味着进程释放一个单位的该类资源,因此需要执行S.value++,表示资源数加1,若加1后仍是S.value<=0,表示依然由进程在等待该类资源,因此调用wakeup原语唤醒等待队列中的第一个进程(被唤醒进程从阻塞态—>就绪态)。

知识回顾

 注:若考试中出现P(S)、V(S)的操作,除非特别说明,否则默认S为记录型信号量。

2.15用信号量机制实现进程互斥、同步、前驱关系

Tips:不要一头钻到代码里,要注意理解信号背后的含义,一个信号量对应一种资源

           信号量的值=这种资源的剩余数量(信号量的值如果小于0,说明此时有进程在等待这种资源) 

           P(S)——申请一个资源S,如果资源不够就阻塞等待。

           V(S)——释放一个资源S,如果有进程在等待该资源,则唤醒一个进程。

知识总览

2.15.1信号量机制实现进程互斥

信号量机制实现进程互斥:

       1.分析并发进程的关键活动,划定临界区(如:对临界资源打印机的访问就应放在临界区)。

       2.设置互斥信号量mutex,初值为1//理解:信号量mutex表示“进入临界区的名额”

       3.在进入区P(mutex)——申请资源

       4.在退出区V(mutex)——释放资源

/*信号量机制实现互斥*/
semaphore mutex=1;//初试化信号量

P1(){
  …
  P(mutex);   //使用临界资源前需要加锁
  临界区代码段…
  V(mutex);   //使用临界资源后需要解锁
  …
}


P2(){
  …
  P(mutex);   //使用临界资源前需要加锁
  临界区代码段…
  V(mutex);   //使用临界资源后需要解锁
  …
}

       注意:1.对不同的临界资源需要设置不同的互斥信号量(比如:打印机这种临界资源的互斥信号量可以设置为mutex1;摄像头这种临界资源的互斥信号量可以设置为mutex2)

                  2.缺少P(mutex)就不能保证临界资源的互斥访问。缺少V(mutex)会导致资源永不被释放,等待进程永不被唤醒。

2.15.2信号量机制实现进程同步

      进程同步:要让各并发进程按要求有序地推进。

      比如:

      P1(){                  P2(){

         代码1;                  代码4;

         代码2;                  代码5;

         代码3;                  代码6;

     }                               }

       P1、P2并发执行,由于存在异步性,因此二者交替推进的次序是不确定的。若P2的“代码4”要基于P1的“代码1”和“代码2”的运行结果才能执行,那么我们就必须保证“代码4”一定是在“代码2”之后才会执行。

       这就是进程同步问题,让本来异步并发的进程互相配合,有序推进

信号量机制实现进程同步:

       1.分析什么地方需要实现“同步关系”,即必须保证“一前一后”执行的两个操作(或两句代码)。

       2.设置同步信号量S,初始为0

       3.在“前操作”之后执行V(S)。

       4.在“后操作”之前执行P(S)。(技巧口诀:前V后P)

例:

       /*信号量机制实现同步*/

       semaphore S=0;//初始化同步信号量,初始化为0。

       P1(){                  P2(){

              代码1;                  P(S);

              代码2;                  代码4;

              V(S);                  代码5;

              代码3;                  代码6;

        }                               }

        若先执行到V(S)操作,则S++后S=1。之后当执行到P(S)操作时,由于S=1,表示有可用资源,会执行S--,S的值会变回0,P2进程不会执行block原语,而是继续往下执行代码4。

        若先执行到P(S)操作,由于S=0,S--后S=-1,表示此时没有可用资源,因此P操作中会执行block原语,主动请求阻塞。之后当执行完代码2,继而执行V(S)操作,S++,使S变回0,由于此时有进程在该信号量对应的阻塞队列中,因此会在V操作中执行wakeup原语,唤醒P2进程。这样P2就可以继续执行代码4了。

        保证了代码4一定是在代码2之后执行。

2.15.3信号量机制实现前驱关系

        进程P1中有句代码S1,P2中有句代码S2,P3中有句代码S3…P6中有句代码S6。这些代码要求按如下前驱图所示的顺序来执行:

         其实每一对前驱关系都是一个进程同步问题(需要保证一前一后的操作)因此,

         1.要为每一对前驱关系各设置一个同步信号量。

         2.在“前操作”之后对相应的同步信号量执行V操作。

         3.在“后操作”之前对相应的同步信号量执行P操作。

知识回顾

注:1.互斥问题,信号量初值为1

       2.同步问题,信号量初值为2

       3.前驱问题,本质上就是多级同步问题

       4.除了互斥、同步问题外,还会考察有多个资源的问题,有多少资源就把信号量初值设为多少。申请资源时进行P操作,释放资源时进行V操作即可

2.16经典同步问题

        PV操作题目分析步骤:

        1.关系分析。找出题目中描述的各个进程,分析它们之间的同步、互斥关系。

        2.整理思路。根据各进程的操作流程确定P、V操作的大致顺序。

        3.设置信号量。并根据题目条件确定信号量初值。(互斥信号量初值一般为1,同步信号量初值要看对应资源的初始值为多少)

2.16.1生产者—消费者问题

       系统中有一组生产者进程和一组消费者进程。生产者进程每次生产一个产品放入缓冲区,消费者进程每次从缓冲区中取出一个进程并使用,那么他们之间具有这样一层关系:
       1.生产者、消费者共享一个初始为空、大小为n的缓冲区。
       2.只有缓冲区没满时,生产者才能把产品放入缓冲区。否则必须等待 //缓冲区没满->生产者生产)
       3.只有缓冲区不空时,消费者才能从中取出产品,否则必须等待// 缓冲区不空->消费者消费
       4.缓冲区是临界资源,各进程访问时要求互斥 //互斥访问

在这里插入图片描述

       由上面的描述可知:①要执行生产者生产操作的前提条件是缓冲区没满;②而执行消费者消费的前提条件是缓冲区不空。实际上是两对同步问题。应在前操作之后对相应的同步信息量执行V操作;在后操作之前对相应的同步信息量执行P操作。设置两个信号量full、empty。
       所以他们之间是这样的关系:

在这里插入图片描述

 定义信号量:

semaphore mutex = 1; //互斥信号量,实现对缓冲区的互斥访问
semaphore empty = n; //同步信号量,表示空闲缓冲区的数量
semaphore full = 0; //同步信号量,表示产品的数量,也即非空缓冲区的数量

生产者生产:

producer (){
   while(1){
    生产一个产品;
    P(empty);
    P(mutex);
    把产品放入缓冲区;
    V(mutex);
    V(full);
   } 
}

       P(mutex)和V(mutex)实现进程的互斥访问,P(empty)实现让缓冲区没满作为生产者生产的前操作,即当empty==0时即无空闲缓冲区数量时,让消费者消费作为生产者生产的前操作。
       V(full)实现让缓冲区不空作为消费者消费的前操作,即当full==0时即缓冲区全为空时,让生产者生产作为消费者消费的前操作。
消费者消费:

consumer (){
   while(1){
     P(full);
     P(mutex);
     从缓冲区取出一个产品;
     V(mutex);
     V(empty);
     使用产品; 
  }
}

       P(mutex)和V(mutex)实现进程的互斥访问,V(empty)实现让缓冲区没满作为生产者生产的前操作,即当empty==0时即无空闲缓冲区数量时,让消费者消费作为生产者生产的前操作。
       P(full)实现让缓冲区不空作为消费者消费的前操作,即当full==0时即缓冲区全为空时,让生产者生产作为消费者消费的前操作。

       思考:能否改变相邻P、V操作的顺序?
如果将上述的代码改为这样会产生怎样的影响呢?

producer (){
   while(1){
    生产一个产品;
    P(mutex);// ①
    P(empty);// ② 把产品放入缓冲区;
    把产品放入缓冲区;
    V(mutex);
    V(full);
   } 
}
consumer (){
   while(1){
     P(mutex);;// ③
     P(full);// ④ 从缓冲区取出一个产品;
     从缓冲区取出一个产品;
     V(mutex);
     V(empty);
     使用产品; 
  }
}

       若此时缓冲区内已经放满产品,则empty=0,full=n。则生产者在执行①操作时使mutex变为0,再执行②,由于已经没有空闲缓冲区了所以生产者会被堵塞,并且在消费者进程执行时,执行到③操作时,由于mutex为0,也会被堵塞。这就导致了生产者在等待消费者是否空闲缓冲区,而消费者等待生产者释放临界资源的情况,生产者和消费者就这样循环等待被对方唤醒,出现了“死锁”的情况。同样的,在缓冲区全为空的情况下,即empty=n,full=0时,按③④①的顺序执行也会发生“死锁”。因此,实现进程互斥的P操作要在实现进程同步的P操作之后
       V操作不会导致进程堵塞,因此V操作的顺序可以调换
知识回顾
      1、生产者、消费者共享一个初始值为空,大小为n的缓冲区 刚开始空闲缓冲区的数量为n,非空闲缓冲区的数量为0。
      2、只有缓冲区没满时,生产者才能把产品放入缓冲区。否则必须等待同步关系,缓冲区满时,生产者要等待消费者取走产品。
      3、只有缓冲区不空时,消费者才能从中取出产品,否则必须等待同步关系,缓冲区为空时,消费者要等待生产者生产。
      4、缓冲区是临界资源,各进程访问时要求互斥。

2.16.2读者—写者问题

       有读者和写者两组并发进程,共享一个文件,当两个或两个以上的读进程同时访问共享数据时不会产生副作用,但若某个写进程和其他进程(读进程或写进程)同时访问共享数据时则可能导致数据不一致的错误。因此要求:①允许多个读者可以同时对文件执行读操作;②只允许一个写者往文件中写信息;③任一写者在完成写操作之前不允许其他读者或写者工作;④写者执行写操作前,应让已有的读者和写者全部退出。

分析:
       写进程和写进程访问共享数据时互斥;写进程和读进程访问共享数据时互斥。

定义信号量:

semaphore rw=1; //用于实现对共享文件的互斥访问
int count = 0; //记录当前有几个读进程在访问文件
semaphore mutex = 1;//用于保证对count变量的互斥访问

写者:

writer (){
  while(1){
    P(rw); //写之前“加锁”
    写文件…
    V(rw); //写完了“解锁” 
  } 
}

读者:

reader (){
   while(1){
     P(mutex); //各读进程互斥访问count
     if(count==0) //由第一个读进程负责
     P(rw); //读之前“加锁”
     count++; //访问文件的读进程数+1
     V(mutex);
     读文件…
     P(mutex); //各读进程互斥访问count
     count--; //访问文件的读进程数-1
     if(count==0) //由最后一个读进程负责
     V(rw); //读完了“解锁”
     V(mutex);
   }
}

思考:这种算法是否有缺陷呢?
       问题1:若两个读进程并发执行,则count = 0时两个进程也许都能满足if条件 都会执行p(rw),从而使第二个读进程阻塞的情况。那这种问题又该如何解决呢?
       首先我们应当分析出现上述问题的主要原因在于对count变量的检查和赋值无法一气呵成,因此可以设置一个互斥信号量来确保对count变量的检查和赋值操作是互斥的。

       问题二:读文件可以同时进行,并且会使临界资源的访问互斥,如果有读进程源源不断的进行读文件,那写进程就有可能出现“饿死”的情况,那我们该如何解决这种情况呢?
       使算法里面读进程优先即可。

改进算法:

定义:
semaphore rw=1; //用于实现对共享文件的互斥访问
int count = 0; //记录当前有几个读进程在访问文件
semaphore mutex = 1; //用于保证对count变量的互斥访问
semaphore w = 1; //用于实现“写优先”
读者:
writer (){
  while(1){
    P(w);
    P(rw);
    写文件…
    V(rw);
    V(w);
  } 
}
写者:
reader (){
   while(1){
     P(w);
     P(mutex);
     if(count==0)
     P(rw);
     count++;
     V(mutex);
     V(w);
     读文件…
     P(mutex);
     count--;
     if(count==0)
     V(rw);
     V(mutex);
   }
}

       这种算法中,连续进入的多个读者可以同时读文件,写者和其他进程不能同时访问文件;写者不会饥饿,但也不是真正的“写优先”而是相对公平的先来先服务

知识回顾

       读者-写者问题的核心思想在于设置一个计数器count用来记录当前正在访问的读进程数,我们可以用count来判断当前进程是否为第一个或者最后一个读进,从而做出不同的处理。
       另外,对count变量的检查和赋值不能一气呵成导致了一些错误,如果需要实现“一气呵成”自然要想到互斥信号量

2.16.3哲学家进餐问题

       一张圆桌上坐着5名哲学家,每两个哲学家之间的桌上摆一根筷子,桌子的中间是一碗米饭。哲学家们倾注毕生的精力用于思考和进餐,哲学家在思考时,并不影响他人。只有当哲学家饥饿时,才试图拿起左、右两根筷子(一根一根地拿起)。如果筷子已在他人手上,则需等待。饥饿的哲学家只有同时拿起两根筷子才可以开始进餐,当进餐完毕后,放下筷子继续思考。

在这里插入图片描述

      分析:

      1.关系分析。系统中有五个哲学家进程,五位哲学家与左右邻居对其中间筷子的访问是互斥关系。

      2.整理思路。这个问题中只有互斥关系,但与之前遇到的问题不同的是,每个哲学家进程需要同时持有两个临界资源才能开始吃饭。如何避免临界资源分配不当造成的死锁现象,是哲学家问题的精髓。

      3.信号量设置。定义互斥信号量数组chopstick[5]={1,1,1,1,1}用于实现对5各筷子的互斥访问。并对哲学家按0~4编号,哲学家i左边的筷子编号为i,右边的筷子编号为(i+1)%5。

      首先我们看一下如下代码:

semaphore chopstick[5]={1,1,1,1,1};
Pi (){ //i号哲学家的进程
   while(1){
     P(chopstick[i]); //拿左
     P(chopstick[(i+1)%5]); //拿右
     吃饭…
     V(chopstick[i]); //放左
     V(chopstick[(i+1)%5]); //放右
     思考… 
   } 
}

       思考:按照如上的代码,我们思考一下,如果5位哲学家并发的拿起自己左手边的筷子,此时每位哲学家都在循环等待右边的人使用完放下右边的筷子,从而陷入无限的循环之中,这种情况就称为“死锁”。
       如何防止死锁的发生呢?
       方案1:可以对哲学家进程施加一些限制条件,比如说是能有同时四个哲学家进餐,这样可以保证至少有一个哲学家可以拿到左右两个筷子。
       方案2:添加一些特定的要求,比如要求奇数号哲学家进餐是需要首先那左边的筷子,偶数号哲学家进餐时需要首先拿右边的筷子。用这种方式可以保证如果相邻的两个奇偶哲学家都想吃饭,那么只会有一个可以拿起第一个筷子,另一个会直接阻塞,这就避免了占有一只再等待另一只的情况。
       方案3:仅当一个哲学家左右两只筷子有可用时,才允许他抓起筷子。
       方案4:使拿筷子的行为互斥的进行,这就保证了有哲学家拿到筷子堵塞之后其他的哲学家无法尝试拿筷子,直到正在吃饭的哲学家放下筷子,堵塞的哲学家拿到两只筷子之后,其他哲学家才能再次尝试拿筷子,也可以解决死锁的问题。

       以第四种解决方法为例,代码如下:

semaphore chopstick[5]={1,1,1,1,1};
semaphore mutex = 1; //互斥地取筷子
Pi (){ //i号哲学家的进程
   while(1){
     P(mutex);
     P(chopstick[i]); //拿左
     P(chopstick[(i+1)%5]); //拿右
     V(mutex);
     吃饭…
     V(chopstick[i]); //放左
     V(chopstick[(i+1)%5]); //放右
     思考… 
   } 
}

 知识回顾

        哲学家进餐问题的关键在于解决进程死锁。这些进程之间只存在互斥关系,但是与之前接触到的互斥关系不同的是,每个进程都需要同时持有两个临界资源,因此就有“死锁”问题的隐患。

2.17管程

知识总览

2.17.1为什么要引入管程

       信号量作为高层抽象与P、V操作一起来解决了绝大部分的进程同步问题。但这种解决方法也有一定的缺陷:
       (1)程序易读性差,因为要了解对于信息量的操作是否正确需要读懂整个系统活着并发程序。
       (2)程序不利于修改或维护,因为程序的局部性很差,所以对任意一组变量一段代码的修改都有可能影响全局。
       (3)正确性难以保证,因为操作系统或并发程序通常很大,要保证这样一个复杂的系统,没有逻辑错误是很难的
       (4)容易发生死锁的情况,如果程序员不使用好P、V操作时,逻辑上发生错误,很有可能会导致死锁的发生。

       那么能不能设置一种机制,让程序员写程序时,不需要再关注复杂的PV操作,让写代码更轻松,而且不会轻易发生死锁呢?
       这就是我们接下来介绍的于1973年,Brinch Hansen 首次在程序设计语言(Pascal)中引入了“管程”成分——一种高级同步机制。

2.17.2管程的定义和基本特征

1.管程的定义

       一个管程是由一个过程、变量及数据结构组成的一个集合,他们组成一个特殊的模块或软件包。 进程可在任何需要的时候调用管程中的过程来访问、调用管程中的数据结构注意:过程只能是管程中声明的才行
       管程是一种特殊的软件模块,有这些部分组成:
       1.局部于管程的共享数据结构说明;
       2.对该数据结构进行操作的一组过程
       3.对局部于管程的共享数据结构设置初始值的语句;
       4.管程有一个名字。

2.管程的基本特征

      1.管程是一个基本的软件模块,可以被单独编译。
      2.管程中封装的数据以及对数据的操作,与面向对象编程语言的类相似。
      3.局部于管程的数据只能被局部于管程的过程所访问。
      4.一个进程只能通过调用管程内的过程才能进行管程访问共享数据。
      5.每次允许一个进程在管程内执行某个内部过程

知识回顾

2.18死锁的概念

知识总览

2.18.1什么是死锁

       死锁就是在并发环境下,各进程因竞争资源而造成的一种互相等待对方手里的资源,导致各进程都阻塞,都无法向前推进的现象。发生死锁后若无外力干涉,这些进程都将无法向前推进。

        举个例子,有一首歌的歌词:我爱你,你爱他,他爱她,她爱我……这世界每个人都爱别人……
        我们从资源占有的角度来分析,这段关系为什么看起来那么纠结。

       每个人都占有一个资源,同时又在等待另一个人手里的资源。发生“死锁”。

       哲学家进餐的问题就是最经典的死锁问题。

2.18.2死锁、饥饿、死循环的区别

       死锁:各进程相互等待对方手里的资源,导致各进程都阻塞,无法向前推进的现象。
       饥饿:由于长时间得不到想要的资源,某进程无法向前推进的现象,比如:短进程优先(SPF)算法中,若有源源不断的短进程到来,则长进程将一直得不到处理机,从而发生长进程“饥饿”。
       死循环:某进程执行过程中一直跳不出某个循环的现象。有时是因为程序逻辑出现Bug导致的,有时是程序员故意设计的。

2.18.3死锁产生的必要条件

       产生死锁必须同时满足以下四个条件,任何一个条件不存在,都无法产生死锁
      互斥条件:只有对必须互斥使用的资源的争抢才会导致死锁(如哲学家的筷子、打印机设备)。像内存、扬声器这样可以同时让多个进程使用的资源是不会导致死锁的(因为进程不会阻塞等待这种资源)。
       不剥夺条件:进程所获得的资源在未使用完之前,不能由其他进程强行夺走,只能主动释放。
       请求和保持条件:进程已经保持了至少一个资源,但又提出新的资源请求,而该资源又被其他进程占有,此时请求被阻塞,但又对自己已有的资源保持不放。
       循环等待条件:存在一种进程资源循环等待,链中的每一个进程以获得的资源同时被下一个进程请求
       注意!发生死锁时一定有循环等待,但是发生循环等待时未必死锁(循环等待是死锁的必要不充分条件)
       如果同类资源数大于1,则即使使用循环等待,也未必发生死锁,但如果系统中每类资源只有一个,那循环等待就是死锁的充分必要条件了。

2.18.4什么时候会发生死锁

       1.对系统资源的竞争。各进程对不可剥夺的资源(如打印机)的竞争可能引起死锁,对可剥夺的资源(CPU)的竞争是不会引起死锁的。
       2.进程推进顺序非法。请求和释放资源的顺序不当,也同时会导致死锁。例如,并发执行的进程P1、P2分别申请并占有了资源R1、R2,之后进程P1又紧接着申请资源R2,而进程P2又申请资源R1,两者会因为申请资源被对方占有而阻塞,从而发生死锁
       3.信息量的使用不当也会造成死锁。如生产者-消费者问题中,如果实现互斥的P操作在实现同步的P操作之前,就有可能导致死锁。
       总之,对不可剥夺资源的不合理分配,可能导致死锁。

2.18.5死锁的处理策略

      1.预防死锁。破坏死锁产生的四个必要条件中的一个或几个。
      2.避免死锁。用某种方法防止系统进入不安全状态,从而避免死锁(银行家算法)。
      3.死锁的检测和解除。允许死锁的发生,不过操作系统会负责检测出死锁的发生,然后采取某些措施解除死锁。

知识回顾

2.19死锁的处理策略

2.19.1死锁的处理策略——预防死锁

知识总览

1.破坏互斥条件

       互斥条件:只有对必须互斥使用的资源的争抢才会导致死锁。

       如果把只能互斥使用的资源改造为允许共享使用,则系统不会进入死锁状态。比如: SPOOLing技术。操作系统可以采用 SPOOLing 技术把独占设备在逻辑上改造成共享设备。比如,用SPOOLing技术将打印机改造为共享设备…

       该策略的缺点:并不是所有的资源都可以改造成可共享使用的资源。并且为了系统安全,很多地方还必须保护这种互斥性。因此,很多时候都无法破坏互斥条件。

2.破坏不剥夺条件

       不剥夺条件:进程所获得的资源在未使用完之前,不能由其他进程强行夺走,只能主动释放。

       破坏不剥夺条件:

       方案一:当某个进程请求新的资源得不到满足时,它必须立即释放保持的所有资源,待以后需要时再重新申请。也就是说,即使某些资源尚未使用完,也需要主动释放,从而破坏了不可剥夺条件。

       方案二:当某个进程需要的资源被其他进程所占有的时候,可以由操作系统协助,将想要的资源强行剥夺。这种方式一般需要考虑各进程的优先级(比如:剥夺调度方式,就是将处理机资源强行剥夺给优先级更高的进程使用)

       该策略的缺点:

       ①、实现起来比较复杂。

       ②、释放已获得的资源可能造成前一阶段工作的失效。因此这种方法一般只适用于易保存和恢复状态的资源,如CPU。

       ③、反复地申请和释放资源会增加系统开销,降低系统吞吐量。

       ④、若采用方案一,意味着只要暂时得不到某个资源,之前获得的那些资源就都需要放弃,以后再重新申请。如果一直发生这样的情况,就会导致进程饥饿。


3.破坏请求和保持条件

       请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源又被其他进程占有,此时请求进程被阻塞,但又对自己已有的资源保持不放。

       可以采用静态分配方法,即进程在运行前一次申请完它所需要的全部资源,在它的资源未满足前,不让它投入运行。一旦投入运行后,这些资源就一直归它所有,该进程就不会再请求别的任何资源了。

       该策略实现起来简单,但也有明显的缺点:

       有些资源可能只需要用很短的时间,因此如果进程的整个运行期间都一直保持着所有资源,就会造成严重的资源浪费,资源利用率极低。另外,该策略也有可能导致某些进程饥饿。

4.破坏循环等待条件

       循环等待条件:存在一种进程资源的循环等待链,链中的每一个进程已获得的资源同时被下一个进程所请求。

       可采用顺序资源分配法。首先给系统中的资源编号,规定每个进程必须按编号递增的顺序请求资源,同类资源(即编号相同的资源)一次申请完。

       原理分析:一个进程只有已占有小编号的资源时,才有资格申请更大编号的资源。按此规则,已持有大编号资源的进程不可能逆向地回来申请小编号的资源,从而就不会产生循环等待的现象。

       该策略的缺点:

       ①、不方便增加新的设备,因为可能需要重新分配所有的编号;

       ②、进程实际使用资源的顺序可能和编号递增顺序不一致,会导致资源浪费;

       ③、必须按规定次序申请资源,用户编程麻烦。

知识总览

2.19.2死锁的处理策略——避免死锁

知识总览

1. 什么是安全序列

2.安全序列、不安全状态、死锁的联系

       所谓安全序列,就是指如果系统按照这种序列分配资源,则每个进程都能顺利完成。只要能找出一个安全序列,系统就是安全状态。当然,安全序列可能有多个

       如果分配了资源之后,系统中找不出任何一个安全序列,系统就进入了不安全状态。这就意味着之后可能所有进程都无法顺利的执行下去。当然,如果有进程提前归还了一些资源,那系统也有可能重新回到安全状态,不过我们在分配资源之前总是要考虑到最坏的情况。【比如A 先归还了10亿,那么就有安全序列T→B → A】

        如果系统处于安全状态,就一定不会发生死锁。如果系统进入不安全状态,就可能发生死锁(处于不安全状态未必就是发生了死锁,但发生死锁时一定是在不安全状态)

       因此可以在资源分配之前预先判断这次分配是否会导致系统进入不安全状态,以此决定是否答应资源分配请求。这也是“银行家算法”的核心思想。

3.银行家算法

       银行家算法是荷兰学者 Dijkstra 为银行系统设计的,以确保银行在发放现金贷款时,不会发生不能满足所有客户需要的情况。后来该算法被用在操作系统中,用于避免死锁

       核心思想:在进程提出资源申请时,先预判此次分配是否会导致系统进入不安全状态。如果会进入不安全状态,就暂时不答应这次请求,让该进程先阻塞等待。

实现步骤:

       以此类推,共五次循环检查即可将5个进程都加入安全序列中,最终可得一个安全序列。该算法称为安全性算法。可以很方便地用代码实现以上流程,每一轮检查都从编号较小的进程开始检查。实际做题时可以更快速的得到安全序列。

银行家算法示例(手算):

手算(找到安全系列)

手算(找不到安全系列) 

代码实现

      假设系统中有 n 个进程,m 种资源

      每个进程在运行前先声明对各种资源的最大需求数,则可用一个 n*m 的矩阵(可用二维数组实现)表示所有进程对各种资源的最大需求数。不妨称为最大需求矩阵 Max,Max[i, j]=K 表示进程 Pi 最多需要 K 个资源Rj。同理,系统可以用一个 n*m 的分配矩阵 Allocation表示对所有进程的资源分配情况。Max – Allocation =Need 矩阵,表示各进程最多还需要多少各类资源。

      另外,还要用一个长度为m的一维数组 Available 表示当前系统中还有多少可用资源。

      某进程Pi向系统申请资源,可用一个长度为m的一维数组 Requesti表示本次申请的各种资源量。

知识回顾

       数据结构:

       ①、长度为 m 的一维数组 Available 表示还有多少可用资源

       ②、n*m 矩阵 Max 表示各进程对资源的最大需求数

       ③、n*m 矩阵 Allocation 表示已经给各进程分配了多少资源

       ④、Max – Allocation = Need 矩阵表示各进程最多还需要多少资源

       ⑤、用长度为 m 的一位数组 Request 表示进程此次申请的各种资源数

       银行家算法步骤:

       ①、检查此次申请是否超过了之前声明的最大需求数

       ②、检查此时系统剩余的可用资源是否还能满足这次请求

       ③、试探着分配,更改各数据结构

       ④、用安全性算法检查此次分配是否会导致系统进入不安全状态

      安全性算法步骤:

      ①、检查当前的剩余可用资源是否能满足某个进程的最大需求,如果可以,就把该进程加入安全序列,并把该进程持有的资源全部回收。

      ②、不断重复上述过程,看最终是否能让所有进程都加入安全序列。

      系统处于不安全状态未必死锁,但死锁时一定处于不安全状态。系统处于安全状态一定不会死锁

2.19.3死锁的处理策略——检测和解除

知识总览

       如果系统中既不采取预防死锁的措施,也不采取避免死锁的措施,系统就很可能发生死锁。在这种情况下,系统应当提供两个算法:

       ①死锁检测算法:用于检测系统状态,以确定系统中是否发生了死锁。

       ②死锁解除算法:当认定系统中已经发生了死锁,利用该算法可将系统从死锁状态中解脱出来。

1.死锁的检测

       为了能对系统是否已发生了死锁进行检测,必须:

       ①用某种数据结构来保存资源的请求和分配信息;

       ②提供一种算法,利用上述信息来检测系统是否已进入死锁状态。

       如果系统中剩余的可用资源数足够满足进程的需求,那么这个进程暂时是不会阻塞的,可以顺利地执行下去。

       如果这个进程执行结束了把资源归还系统,就可能使某些正在等待资源的进程被激活,并顺利地执行下去。相应的,这些被激活的进程执行完了之后又会归还一些资源,这样可能又会激活另外一些阻塞的进程…

       如果按上述过程分析,最终能消除所有边,就称这个图是可完全简化的。此时一定没有发生死锁(相当于能找到一个安全序列) 

       如果最终不能消除所有边,那么此时就是发生了死锁

       最终还连着边的那些进程就是处于死锁状态的进程。

2.死锁的解除

       一旦检测出死锁的发生,就应该立即解除死锁。

       补充:并不是系统中所有的进程都是死锁状态,用死锁检测算法化简资源分配图后,还连着边的那些进程就是死锁进程

      解除死锁的主要方法有:

      ①、 资源剥夺法 。挂起(暂时放到外存上)某些死锁进程,并抢占它的资源,将这些资源分配给其他的死锁进程。但是应防止被挂起的进程长时间得不到资源而饥饿。

      ②、 撤销进程法(或称终止进程法) 。强制撤销部分、甚至全部死锁进程,并剥夺这些进程的资源。这种方式的优点是实现简单,但所付出的代价可能会很大。因为有些进程可能已经运行了很长时间,已经接近结束了,一旦被终止可谓功亏一篑,以后还得从头再来。

      ③、 进程回退法 。让一个或多个死锁进程回退到足以避免死锁的地步。这就要求系统要记录进程的历史信息,设置还原点。

知识回顾

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

rooof鑫鑫

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

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

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

打赏作者

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

抵扣说明:

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

余额充值