并发进程详稿12学时.docx
- 文档编号:24573785
- 上传时间:2023-05-29
- 格式:DOCX
- 页数:64
- 大小:479.02KB
并发进程详稿12学时.docx
《并发进程详稿12学时.docx》由会员分享,可在线阅读,更多相关《并发进程详稿12学时.docx(64页珍藏版)》请在冰豆网上搜索。
并发进程详稿12学时
引言:
本章第一节首先介绍并发的概念和进程并发执行带来的问题,同时指出并发进程之间的关系(无关的并发进程和相关的并发进程)和判断进程是否相关的Berstein条件。
特别对于相关的并发进程,可用互斥的方法解决进程间的竞争关系;用同步的方法解决进程间的合作关系。
第二节中介绍实现互斥的软件方法和硬件机制。
实现互斥的软件方法有Dekker算法和Pertenson算法,也可通过禁止中断或采用特殊的硬件指令等硬件机制来支持互斥。
第三节介绍用信号量和PV原语来实现进程的互斥和同步。
在这一节中我们还将介绍一些进程同步和互斥的经典问题。
第四节简单介绍管程的概念以及用管程来实现进程的同步和互斥。
最后一节中我们将讨论在并发处理中通常需要解决的两个问题死锁和饥饿,并分析处理死锁的三种常用方法:
预防、检测和避免。
本章是操作系统课程的精华,也是学习的最大难点。
4.1并发的基本原理
一、再论进程的并发性
1.顺序程序设计
传统的程序设计方法是顺序程序设计,即把一个程序设计成一个顺序执行的程序模块,不同程序也是按序执行的。
程序执行不仅具有内部顺序性,也具有外部顺序性。
首先程序中包含了用来实现某个算法的若干操作,当程序在处理器上执行时,只有前一个操作结束,才能开始后继操作,称为程序内部的顺序性。
如果需要若干不同的程序来完成某个任务,则这些不同程序也将按调用次序严格有序执行,称为程序外部的顺序性。
2.顺序程序设计的特性
(1)执行的顺序性
一个程序在处理器上执行是严格有序的,即每个操作必须在下一个操作开始之前结束。
(2)环境的封闭性
由于程序是一个个顺序执行的,所以每个程序在执行时独占系统的全部资源,不会受到其他程序和外界因素的干扰。
(3)执行结果的确定性
虽然程序执行过程中允许出现中断,但是中断不会引发程序的切换,所以对程序的最终
结果没有影响,换言之,程序的执行结果与它的执行速率无关。
(4)计算过程的可再现性
一个程序针对同一个数据集一次执行的结果,在下一次执行时会重现,即重复执行程序
会获得相同的执行结果。
程序的顺序执行给程序的编制和调试带来很大方便,缺点是计算机系统的效率不高。
3.进程的并发执行
操作系统中引入并发程序设计技术后,程序的执行不再是顺序的,一个程序未执行完而
另一个程序便已开始执行,程序外部的顺序性消失,程序与计算不再一一对应,所以在操作系统中引入进程这一概念来描述这种变化。
(1)进程的并发性
进程的并发性是指一组进程的执行在时间上是重叠的,即一个进程执行的第一条指令是在另一个进程执行的最后一条指令完成前开始的。
例如有两个进程A和B,它们分别执行操作a1,a2,a3和b1,b2,b3。
这两个进程在单处理器上可以交叉执行,如执行序列为a1,b1,a2,b2,a3,b3或a1,b1,a2,b2,b3,a3等,称A和B两个进程的执行是并发的。
值得注意的是进程内部的顺序性并未消失,例如不可能出现a1,b1,a3,b2,a2,b3这样的执行序列。
从宏观上看,并发性反映出一个时间段中有几个进程都处于运行还未运行结束的状态,且进程都在同一处理器上运行;从微观上看,任一时刻最多只有一个进程在处理器上执行。
(2)并发程序设计的优势
可将一个程序分成若干个可同时执行的程序模块的方法称并发程序设计,每个程序模块和它在执行时所处理的数据就组成一个进程。
采用并发程序设计可以充分发挥硬件的并行性,消除处理器和I/O设备的互等现象,提高系统效率。
例如有一个程序不断地从输入设备读取一个字符数据(调用input过程),再进行处理(执行process过程),然后将结果写到磁带上(调用output过程),可表示为:
while(true)
{
input();
process();
output();
}
如果程序按照输入—处理—输出顺序执行,系统的效率是相当低的。
如果把这个求解问题的程序分成三部分:
while(true){input();send();}
while(true){receive();process();send();}
while(true){receive();output();}
每一部分称为一个程序(子)模块,功能是:
模块1:
循环执行,读入字符,将字符送缓冲区1;
模块2:
循环执行,处理缓冲区1中的字符,将计算结果送缓冲区2;
模块3:
循环执行,取出缓冲区2中的计算结果并写到磁带上。
其中send和receive操作是程序模块之间的某种通信机制。
从图中不难看出这三个程序模块能同时执行,在t3时刻输入i3、处理p2和输出o1可以并行工作,同样在t4时刻输入i4、处理p3和输出o2也可以并行工作。
由于中断和通道等硬件技术的出现的确可以使处理器和外部设备能并行工作,但是这些部件能并行工作仅仅意味着计算机有提高效率的可能性,只有通过采用并发程序设计技术才能充分发挥和利用机器部件的并行工作能力。
4.并发程序设计的特性
(1)并发性
进程的执行在时间上可以重叠,单处理器系统中可以并发执行;在多处理器环境中可以并行执行。
(2)共享性
进程之间可以共享某些变量,通过引用这些共享变量就能互相交换信号,从而程序的运行环境不再是封闭的。
(3)制约性
进程并发执行或协作完成同一任务时,会产生相互制约关系,必须对它们并发执行的次序(即相对执行速率)加以协调。
(4)交互性
由于进程共享一些变量,所以,一个程序的执行可能会影响其他程序的执行结果。
因此,虽然程序自身能正确运行,但由于程序运行环境不再是封闭的,程序结果仍可能是不确定的,计算过程具有不可再现性。
二、进程并发执行带来的问题
虽然并发程序设计或者说进程的并发执行可提高系统的工作效率,但是进程的并发执行会引发一系列的问题,从而给操作系统的设计和管理带来困难。
第一章在分析操作系统的异步性的时候曾通过飞机订票问题来说明并发执行的进程可能会导致与时间有关的错误。
这里再举一些由于进程并发执行而出现错误的例子:
1.一个简单的例子
下面过程显示了字符回显程序的基本步骤:
voidecho()
{
chin=getchar();
chout=chin;
putchar(chout);
}
每当用户单击一下键,从键盘获得的每个输入字符就保存在变量chin中,然后传送给变量chout,并回送显示器。
任何程序可以重复调用该过程,接受用户输入,并在用户的屏幕上显示输入的字符。
现有进程P1和P2共享过程echo,chin作为全局变量,而chout是每个进程的局部变量。
考虑下面的顺序:
(1)进程P1调用echo过程,并在getchar返回输入字符(假设字符x)并存于chin后立即
被中断,此时变量chin中保存输入字符x。
(2)进程P2被激活并执行echo过程,输入的字符y覆盖变量chin中的值x,然后在屏幕上显示单个字符y。
(3)进程P1被恢复。
此时chin中的值x被写覆盖,已经丢失,而chin中的值y被传递给chout并显示出来。
因此,第一个字符丢失,第二个字符被显示了两次,问题的本质在于中断的产生可能会导致进程切换,而全局变量的共享又充满了危险。
在多处理器系统中,不仅在单个处理器上会出现上述问题,而且当两个进程同时执行并且试图访问同一个全局变量时,也会引发问题。
(1)进程P1和P2分别在一个单独的处理器上执行,它们都调用了echo过程,chin仍然是共享变量。
(2)下面事件的发生(同行事件表示同时发生的):
结果是输入到P1的字符在显示前被丢失,输入到P2的字符被显示在P1和P2中。
2.竞争条件
竞争条件发生在多个进程在读写数据时,其最终的结果依赖于多个进程的指令执行顺序。
考虑这样一个简单的例子:
两个进程P3和P4共享全局变量b和c,并且初始值b=1,c=2。
在某一执行时刻,p3执行赋值语句b=b+c,在另一执行时刻,p4执行赋值语句c=b+c。
两个变量的最终值依赖于两个进程执行赋值语句的顺序。
如果p3先执行,最终值b=3,c=5。
如果p4先执行,那么最终值b=4,c=3。
三、无关的并发进程
1.并发进程之间的关系
一组并发执行的进程可能是无关的,也可能是交互的。
(1)无关并发进程
无关的并发进程是指它们分别在不同的变量集合上操作。
所以,一个进程执行与其他并发进程的进展无关,即一个并发进程不会改变另一个并发进程的变量值。
(2)交互并发进程
交互的并发进程,由于共享某些变量,所以一个进程的执行可能会影响其他进程执行的结果,可以认为交互的并发进程之间具有制约关系。
2.Bernstein条件
并发进程的无关性是指进程的执行与时间无关的一个充分条件。
该条件在1966年首先由Bernstein提出,又称之为Bernstein条件。
假设R(Pi)={a1,a2,…,an}表示程序Pi在执行期间引用的变量集;
W(Pi)={b1,b2,…,bn}表示程序Pi在执行期间改变的变量集。
只要两个进程各自的程序P1和P2满足Berstein条件——引用变量集与改变变量集之交集为空,R(P1)∩W(P2)=∮R(P2)∩W(P1)=∮W(P1)∩W(P2)=∮
则并发进程的执行与时间无关。
【思考】下面的进程P1和P2中的语句是否可以并发执行(对各自结果不会造成影响)?
x=y=1;
进程P1进程P2
y=y+2;x=y+1;
z=y+1;x=z+2;
四、进程的交互
基于进程相互之间知道对方是否存在的程度,可将进程的交互方式划分为竞争和合作(共享合作和通信合作)。
1.竞争进程
这是一些独立的进程,它们不会一起工作,彼此间也不知道对方的存在。
尽管如此,由于这些进程共享了一套计算机系统资源,因此必然会发生多个进程竞争资源的问题。
操作系统要协调好竞争进程对共享资源(尤其是临界资源)的争用。
竞争进程间没有任何信息交换,但是一个进程的执行可能会影响到同其竞争资源的其他进程。
特别如果两个进程都期望访问同一个资源,操作系统把这个资源分配给了一个进程,另一个就必须等待,被拒绝访问的进程的速度就会变慢。
一种极端情况是,被阻塞进程永远不能访问这个资源,因此一直不能成功地终止。
2.竞争进程面临的控制问题
(1)互斥的要求
系统中的某些资源如打印机、磁带机、卡片机,虽然它们可提供给多个进程使用,但在同一时间段内却只允许一个进程访问这些资源,即要求互相排斥地使用这些资源。
当一个程序还在使用该资源时,其他欲访问该资源的进程必须等待,仅当占有者访问完毕并释放资源后,才允许另一个进程对该资源进行访问。
一次仅允许一个进程使用的资源称临界资源,进程中访问临界资源的那部分代码称为程序的临界区。
一次只允许有一个程序在执行临界区,这一点非常重要。
例如在打印机的例子中,我们希望任何一个进程在打印整个文件时都拥有打印机的控制权,否则在打印结果总就会穿插着来自竞争进程的打印内容。
只能采用互斥的方法来控制竞争进程对临界资源的使用,但互斥的实施又产生了两个额外的控制问题:
死锁和饥饿。
(2)死锁(deadlock)
死锁是指这样一种情况:
一组进程中,如果每个进程都获得了部分资源,还想要得到其他进程所占有的资源,最终所有的进程都无法继续执行。
例如有两个进程P1和P2,以及两个资源R1和R2;假设每个进程为执行部分功能都需要访问这两个资源,那么就有可能出现下列情况:
操作系统把R1分配给P2,把R2分配给P1,每个进程都在等待另一个资源,并且在获得其他资源并完成所需功能之前,谁都不会释放自己已经拥有的资源。
这样,两个进程就发生了死锁。
(3)饥饿(starvation)
饥饿是指一个可运行的进程尽管能继续执行,但被调度器无限期地忽视,而不能被调度执行的情况。
假设有三个进程(P1、P2、P3),每个进程都周期性地访问临界资源R。
考虑这种情况:
进程P1拥有资源R,进程P2和P3被迫等待这个资源。
当P1退出临界区时,P2和P3都被允许访问R,假设操作系统把访问权授予P3,并且在P3完成临界区之前P1又访问了临界资源,如果在P3结束后操作系统又把访问权授予P1,接下来资源的访问权轮流在P1和P3中传递,那么即便没有死锁,P2也可能无限期地被拒绝访问资源。
再看一个例子,为了让尽量多的文件打印出来,操作系统规定了一种打印机分配策略:
总是把打印机分配给打印文件最短的进程。
假设有一个进程生成了一个很大的文件需要打印,如果不断有新进程加入系统,并且它们都只打印小文件,那么,要打印大文件的进程总是得不到打印机。
尽管这里没有产生死锁,但却出现饥饿,要打印大文件的进程被无限期推迟执行。
饥饿的产生很有可能与操作系统所采取的资源分配策略有关,要解决饥饿问题,最简单的策略是FCFS资源分配策略,在这种机制下,等待最久的进程将是下一个被调度的进程,随着时间的推进,每个进程终会成为最“老”的进程,因而可获得资源并完成任务。
由于操作系统负责分配资源,竞争的控制不可避免地涉及到操作系统。
操作系统需要保证诸进程能互斥地访问临界资源,既要解决饥饿问题,又要解决死锁问题。
3.进程互斥
进程互斥(mutualexclusion)解决进程间竞争关系(间接制约关系)的手段,进程互斥是指若干个进程要使用同一共享资源时,任何时刻最多允许一个进程去使用,其他要使用该资源的进程必须等待,直到占有资源的进程释放该资源。
4.合作进程
(1)进程间通过共享的合作
通过共享合作的进程,在相互间并不确切知道对方的情况下进行交互,它们在共享同一个对象时表现出合作行为。
例如,多个进程可能访问同一共享变量、共享文件或数据库,进程可能使用并修改共享变量而并不涉及到其他进程,但却知道其他进程也可能访问同一个数据,为了确保共享数据得到正确的管理(例如确保共享数据的完整性),这些进程必须合作。
由于数据保存在资源中(设备、存储器),因此再次涉及到有关互斥、死锁和饥饿等控制问题。
唯一的区别是可以使用两种不同的模式(读和写)访问数据,并且只有写操作必须保证互斥。
除了上述问题外还有一个新的要求:
数据的一致性。
考虑有一个计帐的应用程序,在这个程序中可能会修改各种数据项。
假设两个数据项a和b保持着相等关系a=b,为保持这个关系,任何一个程序如果修改其中一个变量,也就必须修改另一个。
现有两个进程:
P1:
a=a+1;
b=b+1;
P2:
b=2*b;
a=2*a;
如果最初状态是一致的,则单独执行每个进程都会使共享数据保持一致状态。
但是考虑下面的并发执行,两个进程在访问每个数据项时都考虑了互斥:
a=a+1;
b=2*b;
b=b+1;
a=2*a;
按照这个执行顺序,结果不再保持条件a=b。
例如,开始时有a=b=1,在这个执行序列结束时有a=4和b=3。
(2)进程间的通信合作
进程之间相互通信,合作完成某些活动。
通信可由各种类型的消息组成。
发送消息和接受消息的原语或者由程序设计语言提供,或者由操作系统的内核提供。
由于合作的每个进程都是以独立地不可预知的速度推进,这就需要相互协作的进程在某些协调点上协调各自的工作,当到达协调点后需要等待来自伙伴进程发送的消息或者信号。
由于在传递消息的过程中进程间没有共享任何对象,因而这类合作不需要互斥,但是仍然存在死锁和饥饿的问题。
例如有两个进程可能都被阻塞,每个都在等待来自对方的通信,这时发生死锁。
作为饥饿的例子,考虑三个进程P1、P2和P3,它们都有如下特性。
P1不断地试图与P2或P3通信。
P2和P3都试图与P1通信,如果P1和P2不断地交换信息,而P3一直被阻塞,等待与P1通信,由于P1一直是活跃的,因此虽不存在死锁,但P3处于饥饿状态。
5.进程同步
进程同步(synchronization)是实现进程合作关系(直接制约关系)的重要方法。
进程同步是指两个以上进程基于某个条件来协调它们的活动。
一个进程的执行依赖于另一个合作进程的消息或信号,如果没有得到来自另一个进程的消息或信号时,则该进程需要等待,直到消息或信号到达才被唤醒。
不难看出,进程互斥是一种特殊的进程同步关系。
通过上一节的介绍,我们发现支持并发进程的基本需求是加强互斥的。
有多种方法可以满足互斥的要求。
第一种方法是让希望并发执行的进程担负这个责任,不需要程序设计语言或操作系统提供任何支持以实施互斥。
我们把这类方法称为软件方法。
尽管这类方法已经被证明会增加处理开销和缺陷,但通过分析这类方法,可以更好地理解并发处理的复杂性。
第二种方法涉及到使用专门的机器指令,这种方法的优点是可以减少开销,但却很难成为一种通用的解决方案。
第三种方法是在操作系统或程序设计语言中提供某种级别的支持。
本节介绍实现互斥的软件方法和硬件机制。
4.2实现互斥的软件方法和硬件方法
一、互斥的要求
1.必须强制实施互斥:
多个共享同一对象或资源的进程,一次只允许一个进程进入临界区。
2.一个在非临界区停止的进程必须不干涉其他进程。
3.决不允许出现一个需要访问临界区的进程被无限延迟的情况,即不会饥饿或死锁。
4.当没有进程在临界区中时,任何需要进入临界区的进程必须能够立即进入。
5.对相关进程的速度和处理器的数目没有任何要求和限制。
6.一个进程驻留在临界区中的时间必须是有限的。
可把上述互斥的要求总结成四句话:
“无空等待、有空让进、择一而入、算法可行”。
二、互斥:
软件方法
实现互斥的软件方法针对单处理器或共享主存的多处理器系统上执行的并发进程,这些方法通常基于在访问内存基本互斥条件的假设,即对主存中同一个单元的同时访问必定被内存仲裁器串行化了。
此外,不考虑硬件、操作系统或是编程语言的支持。
软件方法会带来高处理负载且很容易产生逻辑错误,然而讨论这些算法可以阐明在开发并发程序时的许多基本概念和潜在的问题。
1.互斥尝试
(1)算法一
turn为共享全局变量,进程P0或P1执行临界区前,先检查turn的值。
如果turn的值等于进程号,那么该进程可进入它的临界区;否则被强制等待。
等待进程需重复读取turn的值直到被允许进入临界区,这个过程被称为忙等待或自旋等待(会消耗处理器的时间)。
进程在获取访问临界区且访问结束后,必须将turn的值更新为另一个进程的进程号。
算法一有两个缺点:
●进程必须交替使用临界区,因此执行的步调由较慢的进程决定。
如果P0一小时使用一次临界区,而P1要以一小时一千次的速率执行临界区,则P1就需要适应P0的步调。
●如果一个进程失败(无论是临界区内失败还是之外失败),则另一个进程就会永久阻塞。
(2)算法二
算法二中,进程P0和P1共享全局变量flag:
typedefenum{false,true}boolean;
booleanflag[2]={false,false};
flag[0]和P0相联系,flag[1]和P1相联系,当进程在它的临界区内时对应的flag元素为true。
当一个进程要进入临界区时,它要周期性地检查另一个进程的flag,直到其值为false,表明对方不在临界区内。
检查进程立即将自己的flag设为true,进入临界区。
当离开临界区时,检查进程再将自己的flag设置为false以让对方进入其临界区。
和算法一相比,算法二中当一个进程在临界区外(包括修改flag的代码)调用失败,那么另一个进程不会阻塞。
但是如果一个进程在临界区内调用失败(已将自己的flag设为true),那么另一个进程就会永久阻塞。
仔细分析,不难发现算法二甚至连互斥都未能保证。
考虑下面的执行顺序:
P1执行while语句发现flag[0]设置为false。
P0执行while语句发现flag[1]设置为false。
P1将flag[0]设置为true并进入自己的临界区。
P0将flag[1]设置为true并进入自己的临界区。
(3)算法三
算法三对算法二作如下修正:
延迟检查对方进程的flag状态,先将自己的flag的值设为true以封锁对方进程。
下面从进程P0的角度检查算法三是否保证互斥。
P0先将flag[0]设置为true,接下来分两种情况:
一是P1尚未进入临界区,则由于flag[0]为true,所以P1只能执行while语句忙等待,直到P0离开临界区将flag[0]设为false为止;二是P1已经进入临界区,此时flag[1]必为true,于是P0会被while语句阻塞直到P1离开临界区将flag[1]设为false为止。
同理,从P1的角度看也是如此。
算法三保证了互斥,但很容易发现其缺陷——如果两个进程在执行任一个while语句之前都将flag设置为true,那么每个进程都认为另一个已经进入临界区,从而造成死锁。
(4)算法四
算法三中,一个进程在设置自己的状态时并不知道另一个进程的状态。
因为每个进程都坚持进入临界区的权利,所以造成死锁。
现用算法四对算法三进行改进:
每个进程设置flag表明它要进入临界区,但是为了“谦让”另一个进程,接着不停地尝试重设flag值。
算法四很接近正确算法,但仍有缺点,它和算法三类似能够保证互斥。
但是考察以下事件顺序:
P0设置flag[0]为true。
P1设置flag[1]为true。
P0检查flag[1]。
P1检查flag[0]。
P0设置flag[0]为false。
P1设置flag[1]为false。
P0设置flag[0]为true。
P1设置flag[1]为true。
如果这个顺序无限进行下去,无论哪个进程都进不了临界区。
严格来讲,这不是死锁,因为两个进程执行的相对速度的任何改变都会打破这种循环,从而允许其中一个进入临界区。
这种状态称为活锁(livelock)。
死锁指一组进程要进入临界区但没有一个进程会成功,而活锁有可能执行成功,但也有可能任一进程都无法进入临界区。
所以尽管上面的场景不会维持很长时间,但这仍然是一种可能的情形。
为此还是拒绝算法四。
2.正确的算法
(1)Dekker算法
Dijkstra在1965年提出了两个进程互斥的算法,由德国数学家Dekker实现。
除了使用全局布尔数组flag来表示每个进程的状态外,为了避免算法四中“互相谦让”的情况,还需引入算法一中的全局变量turn,用其值表明哪个进程更有权利进入临界区。
Dekker算法中进程P0和P1共享全局数组flag和全局变量turn:
booleanflag[2];
intturn;
为了并发执行程序P0和P1,再构造一个主程序main:
voidmain()
{
flag[0]=false;
flag[1]=false;
turn=1;
Parbegin(P0,P1);/*搁置主程序的执行,初始化并发执行程序P0和P1,当所有的程序P0、P1都结束后,重新开始主程序*/
}
当P0要进入它的临近区时,它先将自己的flag设为true,然后检查P1的flag,分两种情况:
如果flag[1]为false,说明P1定然不在临界区中(每个进程进入临界区前必然已将自己的flag设为true),那么P0就可以立即进入它的临界区;
如果flag[1]为true,尚无法判断P1是否在临界区中,于是P0就咨询turn,如果turn为0,P0知道它只要持续检查P1的flag[1]的状态就一定可以进入临界区。
此时分两种情况:
一是P1已在临界区中,当它退出临界区就将flag[1]设为false,P0就可进入临界区;二是P1还没进入临界区,它和P1一样也
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 并发 进程 12 学时