=====B102第二章进程管理==== (2)https://developer.aliyun.com/article/1415742
2.3.4 管程机制
虽然信号量机制是一种既方便、又有效的进程同步机制,但每个要访问临界资源的进程都必须自备同步操作 wait(S)和 signal(S)。这就使大量的同步操作分散在各个进程中。这不仅给系统的管理带来了麻烦,而且还会因同步操作的使用不当而导致系统死锁。这样, 在解决上述问题的过程中,便产生了一种新的进程同步工具——管程(Monitors)。
管程的定义
利用共享数据结构抽象地表示系统中的共享资源,而把对该共享数据结构实施的操作定义为一组过程,如资源的请求和释放过程 request 和 release。进程对共享资源的申请、释 放和其它操作,都是通过这组过程对共享数据结构的操作来实现的,这组过程还可以根据资源的情况,或接受或阻塞进程的访问,确保每次仅有一个进程使用共享资源,这样就可以统一管理对共享资源的所有访问,实现进程互斥。
代表共享资源的数据结构,以及由对该共享数据结构实施操作的一组过程所组成的资源管理程序,共同构成了一个操作系统的资源管理模块,我们称之为管程。管程被请求和释放资源的进程所调用。Hansan 为管程所下的定义是:“一个管程定义了一个数据结构和能为并发进程所执行(在该数据结构上)的一组操作,这组操作能同步进程和改变管程中的数据”。
由上述的定义可知,管程由四部分组成:
① 管程的名称;
② 局部于管程内部的共享数据结构说明;
③ 对该数据结构进行操作的一组过程;
④ 对局部于管程内部的共享数据设置初始值的语句。
图 2-13 是一个管程的示意图。
管程的语法描述如下:
需要指出的是,局部于管程内部的数据结构,仅能被局部于管程内部的过程所访问, 任何管程外的过程都不能访问它;反之,局部于管程内部的过程也仅能访问管程内的数据结构。由此可见,管程相当于围墙,它把共享变量和对它进行操作的若干过程围了起来, 所有进程要访问临界资源时,都必须经过管程(相当于通过围墙的门)才能进入,而管程每次 只准许一个进程进入管程,从而实现了进程互斥。
管程是一种程序设计语言结构成分,它和信号量有同等的表达能力,从语言的角度看, 管程主要有以下特性:
(1) 模块化。管程是一个基本程序单位,可以单独编译。
(2) 抽象数据类型。管程中不仅有数据,而且有对数据的操作。
(3) 信息掩蔽。管程中的数据结构只能被管程中的过程访问,这些过程也是在管程内部
定义的,供管程外的进程调用,而管程中的数据结构以及过程(函数)的具体实现外部不可见。
管程和进程不同,主要体现在以下几个方面:
(1) 虽然二者都定义了数据结构,但进程定义的是私有数据结构 PCB,管程定义的是公共数据结构,如消息队列等;
(2) 二者都存在对各自数据结构上的操作,但进程是由顺序程序执行有关的操作,而管程主要是进行同步操作和初始化操作;
(3) 设置进程的目的在于实现系统的并发性,而管程的设置则是解决共享资源的互斥使 用问题;
(4) 进程通过调用管程中的过程对共享数据结构实行操作,该过程就如通常的子程序一样被调用,因而管程为被动工作方式,进程则为主动工作方式;
(5) 进程之间能并发执行,而管程则不能与其调用者并发;
(6) 进程具有动态性,由“创建”而诞生,由“撤销”而消亡,而管程则是操作系统中的一个资源管理模块,供进程调用。
条件变量
在利用管程实现进程同步时,必须设置同步工具,如两个同步操作原语 wait 和 signal。当某进程通过管程请求获得临界资源而未能满足时,管程便调用 wait 原语使该进程等待, 并将其排在等待队列上,如图 2-13 所示。仅当另一进程访问完成并释放该资源之后,管程 才又调用 signal 原语,唤醒等待队列中的队首进程。
但是仅仅有上述的同步工具是不够的。考虑一种情况:当一个进程调用了管程,在管程中时被阻塞或挂起,直到阻塞或挂起的原因解除,而在此期间,如果该进程不释放管程,则其它进程无法进入管程,被迫长时间地等待。为了解决这个问题,引入了条件变量 condition。通常,一个进程被阻塞或挂起的条件(原因)可有多个,因此在管程中设置了多个条件变量,对这些条件变量的访问,只能在管程中进行。
管程中对每个条件变量都须予以说明,其形式为:Var x,y:condition。对条件变量的操作仅仅是 wait 和 signal,因此条件变量也是一种抽象数据类型,每个条件变量保存了一个链表,用于记录因该条件变量而阻塞的所有进程,同时提供的两个操作即可表示为 x.wait和 x.signal,其含义为:
① x.wait:正在调用管程的进程因 x 条件需要被阻塞或挂起,则调用 x.wait 将自己插入到 x 条件的等待队列上,并释放管程,直到 x 条件变化。此时其它进程可以使用该管程。
② x.signal:正在调用管程的进程发现 x 条件发生了变化,则调用x.signal,重新启动一个因 x 条件而阻塞或挂起的进程。如果存在多个这样的进程,则选择其中的一个,如果没有,则继续执行原进程,而不产生任何结果。这与信号量机制中的 signal 操作不同,因为 后者总是要执行 s:=s+1 操作,因而总会改变信号量的状态。
如果有进程 Q 因 x 条件处于阻塞状态,当正在调用管程的进程 P 执行了 x.signal 操作后,进程 Q 被重新启动,此时两个进程 P 和 Q,如何确定哪个执行,哪个等待,可采用下述两种方式之一进行处理:
(1) P 等待,直至 Q 离开管程或等待另一条件。
(2) Q 等待,直至 P 离开管程或等待另一条件。
采用哪种处理方式,当然是各执一词。Hoare 采用了第一种处理方式,而 Hansan 选择了两者的折衷,他规定管程中的过程所执行的 signal 操作是过程体的最后一个操作,于是,进程 P 执行 signal 操作后立即退出管程,因而进程 Q 马上被恢复执行。
2.4 经典进程的同步问题
2.4.1 生产者——消费者问题
前面我们已经对生产者—消费者问题做了一些描述,但未考虑进程的互斥与同步问题,因而造成了数据(Counter)的不定性。由于生产者—消费者问题是相互合作的进程关系的一种抽象,例如,在输入时,输入进程是生产者,计算进程是消费者;而在输出时,计算进程是生产者,而打印进程是消费者。因此,该问题有很大的代表性及实用价值。本小节将利用信号量机制来解决生产者—消费者问题。
利用记录型信号量解决 生产者——消费者问题
假定在生产者和消费者之间的公用缓冲池中,具有 n 个缓冲区,这时可利用互斥信号 量 mutex 实现诸进程对缓冲池的互斥使用。利用信号量 empty 和 full 分别表示缓冲池中空缓冲区和满缓冲区的数量。又假定这些生产者和消费者相互等效,只要缓冲池未满,生产者便可将消息送入缓冲池;只要缓冲池未空,消费者便可从缓冲池中取走一个消息。对生产者—消费者问题可描述如下:
在生产者—消费者问题中应注意:首先,在每个程序中用于实现互斥的 wait(mutex)和 signal(mutex)必须成对地出现;其次,对资源信号量 empty 和 full 的 wait 和 signal 操作,同样需要成对地出现,但它们分别处于不同的程序中。例如,wait(empty)在计算进程中,而 signal(empty)则在打印进程中,计算进程若因执行 wait(empty)而阻塞,则以后将由打印进程将它唤醒;最后,在每个程序中的多个 wait 操作顺序不能颠倒,应先执行对资源信号量的 wait 操作,然后再执行对互斥信号量的 wait 操作,否则可能引起进程死锁。
利用AND信号量解决生产者——消费者问题
对于生产者—消费者问题,也可利用 AND 信号量来解决,即用 Swait(empty,mutex)
来代替 wait(empty)和 wait(mutex);用 Ssignal(mutex,full)来代替 signal(mutex)和 signal(full); 用 Swait(full,mutex)来代替 wait(full)和 wait(mutex),以及用 Ssignal(mutex,empty)代替 Signal(mutex)和 Signal(empty)。利用 AND 信号量来解决生产者—消费者问题的算法描述
如下:
利用管程来解决 生产者——消费者
在利用管程方法来解决生产者—消费者问题时,首先便是为它们建立一个管程,并命 名为 ProclucerConsumer,或简称为 PC。其中包括两个过程:
(1) put(item)过程。生产者利用该过程将自己生产的产品投放到缓冲池中,并用整型变 量 count 来表示在缓冲池中已有的产品数目,当 count≥n 时,表示缓冲池已满,生产者须等待。
(2) get(item)过程。消费者利用该过程从缓冲池中取出一个产品,当 count≤0 时,表示缓冲池中已无可取用的产品,消费者应等待。
PC 管程可描述如下:
再利用管程解决 生产者——消费者问题时,其中的生产者和消费者可描述为:
2.4.2 哲学家进餐问题
由 Dijkstra 提出并解决的哲学家进餐问题是典型的同步问题。该问题是描述有五个哲学家共用一张圆桌,分别坐在周围的五张椅子上,在圆桌 上有五个碗和五只筷子,他们的生活方式是交替地进行思考和进餐。平时,一个哲学家进行思考,饥饿时便试图取用其左右最靠近他的筷子,只有在他拿到两只筷子时才能进餐。 进餐完毕,放下筷子继续思考。
利用记录型信号量解决哲学家进餐问题
经分析可知,放在桌子上的筷子是临界资源,在一段时间内只允许一位哲学家使用。为了实现对筷子的互斥使用,可以用一个信号量表示一只筷子,由这五个信号量构成信号 量数组。其描述如下:Var chopstick: array[0,…,4] of semaphore;
所有信号量均被初始化为1,第 i 位哲学家的活动可描述为:
在以上描述中,当哲学家饥饿时,总是先去拿他左边的筷子,即执行 wait(chopstick[i]); 成功后,再去拿他右边的筷子,即执行 wait(chopstick[(i+1)mod 5]);又成功后便可进餐。进餐完毕,又先放下他左边的筷子,然后再放右边的筷子。虽然,上述解法可保证不会有两个相邻的哲学家同时进餐,但有可能引起死锁。假如五位哲学家同时饥饿而各自拿起左边的筷子时,就会使五个信号量 chopstick 均为 0; 当他们再试图去拿右边的筷子时,都将因 无筷子可拿而无限期地等待。对于这样的死锁问题,可采取以下几种解决方法:
(1) 至多只允许有四位哲学家同时去拿左边的筷子,最终能保证至少有一位哲学家能够 进餐,并在用毕时能释放出他用过的两只筷子,从而使更多的哲学家能够进餐。
(2) 仅当哲学家的左、右两只筷子均可用时,才允许他拿起筷子进餐。 (3) 规定奇数号哲学家先拿他左边的筷子,然后再去拿右边的筷子,而偶数号哲学家则相反。按此规定,将是 1、2 号哲学家竞争 1 号筷子;3、4 号哲学家竞争 3 号筷子。即五位哲学家都先竞争奇数号筷子,获得后,再去竞争偶数号筷子,最后总会有一位哲学家能获 得两只筷子而进餐。
利用 AND 信号量机制解决哲学家进餐问题
在哲学家进餐问题中,要求每个哲学家先获得两个临界资源(筷子)后方能进餐,这在本质上就是前面所介绍的 AND 同步问题,故用 AND 信号量机制可获得最简洁的解法。描述
2.4.3 读者——写者问题
一个数据文件或记录,可被多个进程共享,我们把只要求读该文件的进程称为“Reader进程”,其他进程则称为“Writer 进程”。允许多个进程同时读一个共享对象,因为读操作不会使数据文件混乱。但不允许一个 Writer 进程和其他 Reader 进程或 Writer 进程同时访问共 享对象,因为这种访问将会引起混乱。所谓“读者—写者问题(Reader-Writer Problem)”是 指保证一个 Writer 进程必须与其他进程互斥地访问共享对象的同步问题。读者—写者问题常被用来测试新同步原语。
利用信号量解决读者——写者问题
为实现 Reader 与 Writer 进程间在读或写时的互斥而设置了一个互斥信号量 Wmutex。 另外,再设置一个整型变量 Readcount 表示正在读的进程数目。由于只要有一个 Reader 进程在读,便不允许 Writer 进程去写。因此,仅当Readcount=0,表示尚无 Reader 进程在读时,Reader 进程才需要执行 Wait(Wmutex)操作。若 Wait(Wmutex)操作成功,Reader 进程便 可去读,相应地,做 Readcount+1 操作。同理,仅当 Reader 进程在执行了 Readcount 减 1操作后其值为 0 时,才须执行 signal(Wmutex)操作,以便让 Writer 进程写。又因为 Readcount是一个可被多个 Reader 进程访问的临界资源,因此,也应该为它设置一个互斥信号量 rmutex。读者—写者问题可描述如下:
利用信号量集机制解决读者——写者问题
这里的读者—写者问题与前面的略有不同,它增加了一个限制,即最多只允许 RN 个读 者同时读。为此,又引入了一个信号量 L,并赋予其初值为 RN,通过执行 wait(L,1,1)操作,来控制读者的数目。每当有一个读者进入时,就要先执行 wait(L,1,1)操作,使 L 的值减 1。当有 RN 个读者进入读后,L 便减为 0,第 RN+1 个读者要进入读时,必然会因 wait(L,1,1)操作失败而阻塞。对利用信号量集来解决读者—写者问题的描述如下:
其中,Swait(mx,1,0)语句起着开关的作用。只要无 writer 进程进入写,mx=1,reader 进 程就都可以进入读。但只要一旦有 writer 进程进入写时,其 mx=0,则任何 reader 进程就都无法进入读。Swait(mx,1,1;L,RN,0)语句表示仅当既无 writer 进程在写(mx=1),又无 reader 进程在读(L=RN)时,writer 进程才能进入临界区写。
=====B102第二章进程管理==== (4)https://developer.aliyun.com/article/1415757?spm=a2c6h.13148508.setting.14.188b4f0evtcvhF