操作系统--进程同步与死锁

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Xw_Classmate/article/details/50547664

5 进程同步

5.1 进程同步的基本概念

5.1.1 并发性

    进程的并发性是操作系统的基本特征,并发可以改善操作系统资源的利用率,提高系统的吞吐量。所谓并发性,是指一组进程执行在时间点上相互交替,在时间段上互相重叠。

5.1.2 与时间相关的错误

    在多进程并发的情况下,进程共享某些变量或硬件资源,由于进程的执行具有不确定性,如果不对进程的执行加以制约,其执行结果往往是错误的。


    从以上的分析可以看出,相同的程序由于指令的交错执行,最终的结果也不尽相同。这就要求使用进程同步及互斥机制,实现对共享资源的互斥访问,保证程序执行的正确性。

5.1.3  进程的同步与互斥

1. 进程的同步

    进程同步,是指当进程运行到某一点时,若其他进程已完成了某种操作,使进程满足了继续运行的条件,进程才能够继续运行,否则必须停下来等待。通常将进程等待的那一点称为“同步点”,而将等待运行的条件称为“同步条件”。

    相互协作的进程间经常存在数据或变量等共享资源,进程受到特定条件的限制,各进程需要严格按照固定的顺序执行,否则将导致程序的执行错误。

2. 进程的互斥

    对系统中的某些进程来说,为保证程序的正确执行,必须相互协调共享资源的使用顺序。通常共享资源可分为互斥共享资源和可同时访问共享资源两类。互斥共享资源是指在某段时间内,只能有一个进程对该资源进行访问,其他进程若想访问该资源则必须停下来等待,直到该共享资源被前一个进程释放。可同时访问共享资源是指在某段时间内,可以有多进程同时对该资源进行访问,因而也不会存在进程互斥的问题。

5.1.4 临界资源和临界区

1. 临界资源

    将只允许一个进程访问的共享资源称为临界资源,许多物理设备都属于你临界资源,如打印机、绘图仪等。另外,有很多变量,数据能由若干进程共享,这些共享变量及数据也属于临界资源。

2. 临界区

    将程序中对临界资源访问的代码部分称为临界区。

    临界区访问准则:

1)空闲让进:没有进程在临界区时,想进入临界区的进程可进入

2)忙则等待:任何时候,处于临界区内的进程不可多于一个,当已有进程在临界区时,其他欲进入的进程必须等待

3)有限等待:进入临界区的进程要在有限时间内完成并退出临界区,不能让其他进程无限等待

4)让权等待:如果进程不能进入自己的临界区,则应停止运行,让出处理器,避免进程出现“忙等”现象

5.2 互斥实现方法

5.2.1 硬件方法

1. 禁止中断

    由于处理器只能在发生中断引起进程切换,因此关闭中断就能保证当前运行的进程将临界区代码执行完,从而保证了对临界区资源的互斥访问。

    这种方法的优点是简单、方便,缺点是不适用于多处理器;不适用于用户程序;如果对临界区的访问时间较长,关中断的时间就会较长,从而限制了处理器并发能力。

2. TS(Test-and-Set)指令

    S指令的功能是检查指定标志后把该标志置位,可以将TS指令看作一个不可中断的函数,该函数以一个测试标志为参数。当测试标志位时函数返回0,表示资源被占用,否则函数返回1,表示资源可被占用,同时将测试标志位置位。可描述为如下形式:

TS(key){
	if(key==1){
		return 0;
	}else{
		key=1;
		return 1;
	}
}

    可使用如下TS指令实现临界区互斥:

while(!TS(key));	//测试标志位并置位,加锁
临界区;
key=0;	//清标志位,解锁

    进程在执行时首先检查标志位是否被置位,若未被置位则进入临界区,否则将循环进行测试。在进程访问完临界资源后,会将标志位清除,以保证其他进程可以进入临界区。

3.  Swap指令

    Swap对换指令的功能是交换两个字节的内容,该指令可用函数描述为如下形式。

Swap(a,b){
	temp=a;
	a=b;
	b=a;
}

    可使用如下Swap指令实现临界区互斥:

x=1;
while(x!=0) swap(&key,&x);	//加锁
临界区;
key=0;

    标志位key的初值被置为0,表示临界资源未被使用。进程在进入临界区时将使用Swap指令将key与x的值互换,若x值变为0,则表示临界资源可被占用,进程可进入临界区,否则将循环进行测试。在进程访问完临界资源后,通过将key置为0来释放其所占用的资源。

    使用硬件方法管理临界区优点:

1)适用范围广。

2)方法简单。

3)支持多个临界区。

   缺点:

1)易出现忙等待

2)可能产生进程饥渴现象

5.2.2 软件方法

    算法1:利用共享标志位来表示哪个并发进程可以进入临界区。

    对并发进程A与B,设置标志变量turn。若变量turn为0则允许进程A进入临界区访问,若变量turn为1则允许进程B进入临界区访问。算法的实现代码如下:

int turn =0;

进程A:
while(turn!=0);
临界区;
turn=1;

进程B:
while(turn!=1);
临界区;
turn=0;

    标志变量turn的初值被设为0,最初只允许A进入临界区。这样即使进程B先到达临界区的入口,它也会在while中循环忙等,只有在进程A从临界区中退出后,将标志位变量turn设置为1,进程B才有机会进入临界区。

    该算法虽然保证了并发进程对临界区的互斥访问,但是在无进程占用临界区时,进程B仍需等待,因此违反了“空闲让进”原则。

    算法2:利用双标志法判断进程是否进入临界区

    算法通过使用一个flag数组来表示进程是否希望进入临界区。对两个并发进程A与B,如flag[0]=1则表示进程A期望进入临界区,若flag[1]=1则表示进程B期望进入临界区。进程A与B在真正进入临界区之前先查看下对方的flag标志,如果对方正在进入临界区则进行等待。另外,为了避免并发执行时的错误还需要通过一个变量turn来避免两个进程都无法进入临界区。算法的实现代码如下:

int flag[2]={0,0};  
  
进程A:  
flag[0]=1;  
turn=1;  
while(flag[1]&&turn==1);  
临界区;  
flag[0]=0;  
  
进程B:  
flag[1]=1;  
turn=0;  
while(flag[0]&&turn==0);  
临界区;  
flag[1]=0; 



     进程在进入临界区之前会先通过对方的flag标志来判断对方进程是否期望进入临界区。变量turn则是为了避免并发进程语句在交错执行时,只判断对方的flag标志而导致两个进程都无法进入临界区,例如进程A执行完flag[0]=1,进程B也执行完flag[1]=1,如果他们在while条件中只对对方的flag进行判断,那么进程A与B将永远在while语句循环等待。

5.3 信号量

    信号量(Semaphore),类似于交通管理中的信号灯,通过信号量的状态来决定并发进程对临界资源的访问顺序。在信号量同步机制中包含“检测和“归还”两个操作。检测操作称为P操作,用来发出检测信号量的操作,查看是否可访问临界资源,若检测不通过,则进行等待,直到临界资源被归还后才能进入临界区访问。归还操作称为V操作,用来通知等待进程临界资源已经被释放。P操作与V操作都是原子操作,其中的每个步骤是不可分割的,也就是通常所说的“要么都做,要么都不做”。

    信号量的定义:

struct semaphore{
	int value;
	queueType queue;
}

    value表示用来传递信息的整数值,queue表示一个阻塞队列。

    P操作:

P操作:
p(x){
	x.value--;
	if(x.value<0){
		该进程状态置为阻塞状态;
		并将该进程插入相应的等待队列x.queue末尾;
		重新调度;
	}
}

    V操作:

V操作:
v(x){
	x.value++;
	if(x.value<=0){
		唤醒相应等待队列x.queue中等待的一个进程;
		改变其状态为就绪态,并将其插入就绪队列;
	}
}

    使用PV操作解决进程间互斥问题

1)分析并发进程的关键活动,划定临界区

2)设置信号量mutex,初值为1

3)在临界区前实施P(mutex)

4)在临界区后实施V(mutex)

5.3 经典的进程同步问题

5.3.1 生产者-消费者问题

    生产者-消费者问题是最著名的进程同步问题。一组生产者进程向一组消费者进程提供产品,它们共享一个环形缓冲池。缓冲池中的每个缓冲区可以存放一个产品,生产者进程不断生产产品并放入缓冲池中,消费者进程不断从缓冲池内取出产品并消费。

     生产者与消费者具有的同步关系有:

1)当缓冲池满时生产者进程需等待

2)当缓冲池空时消费者进程需等待

3)各个进程应互斥使用缓冲池,也就是说不论是生产者与生产者、生产者与消费者还是消费者与消费者来说,缓冲池都是临界资源。

    利用信号量来解决生产者-消费者问题。设置如下3个信号量:

1)full。表示放有产品的缓冲区数,初值为0.

2)empty。表示可供使用的缓冲区数,初值为N。

3)mutex。互斥信号量,初值为1,使各进程互斥进入临界区,保证任何时候只有一个进程使用缓冲区。

    算法描述如下:

//生产者进程:
while(1){
	生产产品;
	P(&empty)
	P(&mutex);
	产品送往缓冲区;
	V(&mutex);
	V(&full);
}

//消费者进程:
while(1){
	P(&full);
	P(&mutex);
	从缓冲区取出产品;
	V(&mutex);
	V(&empty);
	消费产品;
}


    生产者进程利用信号量empty保证在具有空闲的缓冲区时才将产品放入缓冲池,消费者进程利用信号量full保证只有正在缓冲池中存在产品时采取出,信号量mutex保证了生产者和消费者进程对缓冲池的互斥访问。使用信号量mutex实现互斥时,P操作和V操作是在同一个线程中的,但是使用信号量full、empty实现同步时,P操作和V操作是在不同的线程中的。

    注意:无论是在生产者还是在消费者进程中,P操作的次序都不能颠倒,否则将可能造成死锁。

5.3.2 读者-写者问题

    读者-写者问题也是一个著名的进程同步问题。多个进程共享一个数据区,这些进程可分为两组,一组是读者进程,另一组是写者进程。读者进程只能读取数据区中的数据,写者进程只往数据区中写数据。

    读者和写者具有的同步关系有:

1)允许多个读者向数据区同时执行读操作

2)不允许多个写者向数据区同时执行写操作

3)不允许读者、写者同时向数据区操作

    用一句话总结,对于读者线程与写者线程来说数据区是临界资源,对于写者线程与写者线程来说数据区也是临界资源,但是对于读者线程与读者线程来说却不是。

    读者-写者问题可根据写者到来后是否仍允许新读者进入而分为两类:读者优先和写者优先。读者优先是指当写者准备向数据区写数据时,仍允许新读者进入;写者优先是指当写者准备向数据区写数据时,不允许新读者进入。

    下面用信号量来解决读者优先的读者-写者问题。这里需要设置一个信号量和一个共享变量。

1)mutex。信号量,作用是使读进程与写进程以及写进程与写进程之间的对数据区操作的互斥,其初始值为1。

2)readcount。共享变量,用于记录当前的读者进程数目,初值为0。

//读者进程:
//由于允许多个读者进程同时执行读操作,
//所以需要第一个读者执行P操作,最后一个读者执行V操作
while(1){
	readcount++;//读者进程进入
	if(readcount==1){
		P(mutex);
	}
	从数据区读数据;
	readcount--;//读者进程离开
	if(readcount==0){
		V(mutex);
	}
}

//写者进程:
while(1){
	P(mutex);
	向数据区写数据;
	V(mutex);
}

     上面的算法看似解决了问题,实际上也引入了一个新的问题,因为可以有多个读者对数据区进行读操作,并且对共享变量readcount进行操作,这时候readcount也变成了一个临界资源,所以必须增加一个信号量rmutex(初值为1),用于给使用readcount这个临界资源的临界区进行保护。最终的代码如下:

//读者进程:
//由于允许多个读者进程同时执行读操作,
//所以需要第一个读者执行P操作,最后一个读者执行V操作
while(1){
	P(rmutex);
	readcount++;//读者进程进入
	if(readcount==1){
		P(mutex);
	}
	V(rmutex);
	从数据区读数据;
	P(rmutex);
	readcount--;//读者进程离开
	if(readcount==0){
		V(mutex);
	}
	V(rmutex);
}

//写者进程:
while(1){
	P(mutex);
	向数据区写数据;
	V(mutex);
}

5.3.3 哲学家进餐问题

5.3.4 打瞌睡的理发师问题

5.4  管程

5.4.1 管程的介绍

    利用信号量机制实现进程同步问题时,需要设置很多信号量,并且对共享资源的管理分散在各个进程之中,因此很容易出现死锁等问题。为了解决这类问题,提出了一种高级同步机制—管程(Monitor)。管程是由共享资源的数据结构及在其上操作的一组过程组成,这组过程能够使得进程同步、改变管程中的数据。

    进程要想进入管程,必须调用管程中的过程。

monitor 管程名
	管程变量说明;
	procedure 过程名(...,形式参数表,...);
		begin
			过程体
		end;
	...
	procedure 过程名(...,形式参数表,...);
		begin
			过程体
		end;
	begin
		管程的局部数据初始化语句
	end;

    作为一种同步机制,管程需要解决什么问题呢?

1)互斥

    管程是互斥进入的,目的是为了保证管程中数据结构的数据完整性,管程的互斥性是由编译器负责保证的。

2)同步

     管程中引入了条件变量及其等待/唤醒两个操作原语来解决同步问题。可以让一个进程或线程在条件变量上等待(此时,应先释放管程的使用权),并且通过发送信号将以前等待在条件变量上的进程或线程唤醒。

    但是在使用管程中可能出现问题,试想下下面的场景:当一个进入管程的进程执行等待操作时,它应当释放管程的互斥权,当后面进入管程的进程执行唤醒操作时(例如P唤醒Q),管程中便存在两个同时处于活动状态的进程,这就不满足互斥性了,为了解决这种问题,有一下几种解决办法:P等待Q执行(Hoare管程)、Q等待P继续执行(MESA管程)、规定唤醒操作作为管程中最后一个可执行操作(Hansen)

5.4.2 管程的分类

1. Hoare管程

    管程管程是互斥进入的,在管程外部等待的进程通过一个叫做入口等待队列的链表进行维护。进入管程而被迫的等待的进程加入叫做紧急等待队列的链表,且紧急等待队列的优先级要高于入口等待队列。条件变量就是维护这两个链表的变量,在条件变量上可以执行wait()和signal()操作。


    定义条件变量c,则

    condition c;

wait(c):如果紧急等待队列非空,则唤醒第一个等待者;否则释放管程的互斥权,执行此操作的进程进入c链末尾

signal(c):如果c链为空,则相当于空操作,执行此操作的进程继续执行;否则唤醒入口等待队列的第一个等待者,执行此操作的进程进入紧急等待队列的末尾

    使用管程来解决生产者—消费者问题:

monitor ProducerConsumer
	condition full, empty;
	integer count;
	procedure insert (item: integer);
	begin
		if count == N then wait(full);
		insert_item(item); count++;
		if count ==1 then signal(empty);
	end;
	
	function remove: integer;
	begin
		if count ==0 then wait(empty);
		remove = remove_item; count--;
		if count==N-1 then signal(full);
	end;
	
	count:=0;
end monitor;

procedure producer;
begin
	while true do
	begin
		item = produce_item;
		ProducerConsumer.insert(item);
	end
end;

procedure consumer;
begin
	while true do
	begin
		item=ProducerConsumer.remove;
		consume_item(item);
	end
end;


2. Mesa管程

    在Hoare管程中,如果条件变量队列上有挂起的进程,当另一个进程为该条件变量产生signal时,条件变量队列中的一个进程必须立即执行,产生signal的进程必须挂在紧急队列中。如果产生signal的进程在管程内还没有结束,则需要做两次切换:挂起进程切换到唤醒的进程、当管程可用时恢复该进程又切换一次。

    为了解决这个问题,Lampson和Redell提出了notify,用notify代替signal原语,notify的含义是:当一个正在管程中的进程执行notify(x) 时,它使得x条件队列得到通知,发信号的进程继续执行。通知的结果使得位于条件队列头的进程在将来方便的时候、当处理器可用时被恢复。但是,由于不能保证在它之前没有其他进程进入管程, 因而这个进程必须重新检查条件。所以在判断条件使可以使用while代替if。

    引入broadcast

    broadcast:使所有在该条件上等待的进程都被释放并进入就绪队列。当一个进程不知道有多少进程将被激活时,这种方式是非常方便的;当一个进程难以准确判定将激活哪个进程时,也可以使用broadcast。

Mesa管程和Hoare管程比较:

1)Mesa管程优于Hoare管程之处在于Mesa管程错误比较少

2)在Mesa管程中,由于每个过程在收到信号后都重新检查管程变量,并且由于使用了while结构,一个进程不正确的broadcast广播或发信号notify,不会导致收到信号的程序出错

5.5 死锁

5.5.1 死锁的概念

    一组进程中,每个进程都无限等待被该组进程中 另一进程所占有的资源,因而永远无法得到的资源,这种现象称为进程死锁,这一组进程就称为死锁进程。

    参与死锁的所有进程都在等待资源;参与死锁的进程是当前系统中所有进程的子集。

    这儿区分下死锁、活锁、饥饿的区别

死锁:死锁发生在当一些进程请求其它进程占有的资源而被阻塞时。

活锁:活锁不会被阻塞,而是不停检测一个永远不可能为真的条件。除去进程本身持有的资源外,活锁状态的进程会持续耗费宝贵的CPU时间。

饥饿:资源分配策略决定,比如持续地有其它优先级更高的进程请求相同的资源。

1. 发生死锁的必要条件

    发生死锁的必要条件:

1)互斥条件:一个资源每次只能给一个进程使用

2)部分分配(占有且等待)条件:进程在申请新的资源的同时保持对原有资源的占有。也就是说,进程并不是一次性地得到所需要的所有资源,而是得到一部分资源后,还允许继续申请新的资源。

3)不可抢占(非剥夺)条件:资源申请者不能强行的从资源占有者手中夺取资源, 资源只能由占有者自愿释放。

4)循环等待条件:存在一个进程等待队列 {P1 , P2 , … , Pn}, 其中P1等待P2占有的资源,P2等待P3占有的资源, …,Pn等待P1占有的资源,形成一个进程等待环路。

2. 资源分配图

    用有向图描述系统资源和进程的状态。二元组G=( V, E),V: 结点的集合,分为P(进程), R(资源)两部分,P = {P1, P2, … , Pn},R = {R1, R2, … , Rm}。E: 有向边的集合,其元素为有序二元组 (Pi, Rj) 或 (Rj, Pi)。

    在资源分配图中,通常圆圈表示进程,用方框表示资源,方框中的黑点表示各个单元的资源。由资源实例指向进程的带箭头的线段称为分配边,由进程指向资源的带箭头的线段称为申请边。


     死锁定理:

    如果资源分配图中没有环路,则系统中没有死锁,如果图中存在环路则系统中可能存在死锁。

    如果每个资源类中只包含一个资源实例,则环路是死锁存在的充分必要条件。


    那么如何简单地通过资源分配图来看出是否有死锁存在呢?下面给出资源分配图的化简方法。

1)找一个非孤立、 且只有分配边的进程结点 去掉分配边,将其变为孤立结点。

2)再把相应的资源分配给一个等待该资源的进程 即将该进程的申请边变为分配边。

5.5.2 死锁的预防与避免

1. 死锁的预防

    死锁预防是在设计系统时,通过确定资源分配算法,排除发生死锁的可能性。具体的做法是:防止产生死锁的四个必要条件中任何一个条件发生。

1)破坏互斥条件

    使用资源转换技术,把独占资源变为共享资源。例如SPOOLing技术,它不允许任何进程直接占有打印机,而是设计一个“守护进程/线程”负责管理打印机, 进程需要打印时,将请求发给该daemon,由它完成打印任务。

2)破坏部分分配(占有且等待)条件

    实现方法1:要求每个进程在运行前必须一次性申请它所要求的所有资源,且仅当该进程所要资源均可满足时才给予一次性分配。

    实现方法2:在允许进程动态申请资源前提下规 定,一个进程在申请新的资源不能立即得到满足而变为等待状态之前,必须释放已占有的全部资源,若需要再重新申请。

    但是上面的两种方法会导致资源利用率低、“饥饿”现象等问题。

3)破坏不可抢占(非剥夺)条件

    当一个进程申请的资源被其他进程占用时,可以通过操作系统抢占这一资源(两个进程优先级不同)。这种办法常用于资源状态易于保存和恢复的环境中,如CPU寄存器和内存,但不能用于打印机或磁带机等资源。

4)破坏循环等待条件

    把系统中所有资源编号,进程在申请资源时必须严格按资源编号的递增次序进行,否则操作系统不予分配。

2. 死锁的避免

    定义:在系统运行过程中,对进程发出的每一个系统 能够满足的资源申请进行动态检查,并根据检查结果决定是否分配资源,若分配后系统发生死锁或可能发生死锁,则不予分配,否则予以分配。

    如果系统中存在一个由所有进程构成的安全序列(安全序列是指一个进程序列{P1,…,Pn}是安全的,即对于每一个进程Pi(1≤i≤n),它以后尚需要的资源量不超过系统当前剩余资源量与所有进程Pj (j < i )当前占有资源量之和)P1, …, Pn,则称系统处于安全状态。如果系统不存在这样一个安全序列,则系统是不安全的。不安全状态并不一定引起死锁。

    Dijkstra于1965年提出了一个经典的避免死锁的算法—银行家算法。其模型是在银行中,客户申请贷款的数量是有限的,每个客户在第一次申请贷款时要声明完成该项目所需的最大资金量,在满足所有贷款要求时,客户应及时归还。银行家在客户申请的贷款数量不超过自己拥有的最大值时,都应尽量满足客户的需要。在这样的描述中,银行家就好比操作系统,资金就是资源,客户就相当于要申请资源的进程。

     银行家算法:许进程动态地申请资源,但系统在进行资源分配之前,应先计算此次分配资源的安全性,若分配不会导致系统进入不安全状态,则分配,否则等待。

    为实现银行家算法,系统必须设置若干数据结构。

1)可利用资源向量Available:是个含有m个元素的数组,其中的每一个元素代表一类可利用的资源数目。如果Available[j]=K,则表示系统中现有Rj类资源K个。

2)最大需求矩阵Max:这是一个n×m的矩阵,它定义了系统中n个进程中的每一个进程对m类资源的最大需求。如果Max[i,j]=K,则表示进程i需要Rj类资源的最大数目为K。

3)分配矩阵Allocation:这也是一个n×m的矩阵,它定义了系统中每一类资源当前已分配给每一进程的资源数。如果Allocation[i,j]=K,则表示进程i当前已分得Rj类资源的 数目为K。

4)需求矩阵Need:这也是一个n×m的矩阵,它定义了系统中每一类资源当前已分配给每一进程的资源数。如果Allocation[i,j]=K,则表示进程i当前已分得Rj类资源的数目为K。

    可以看出:Need[i,j]=Max[i,j]-Allocation[i,j]。


   



展开阅读全文

没有更多推荐了,返回首页