进程管理(进程的同步与互斥一)

在操作系统中引入进程实现并发后,虽然提高了资源的利用率和系统的吞吐量,但由于进程的异步性,也会给系统造成混乱,尤其是在进程之间争用临界资源的时候。例如:多个进程去争用一台打印机,有可能使多个进程的输出结果交织在一起,难以区分;而多个进程去争用共享变量、表格、链表时,有可能导致数据处理出错。

进程同步和互斥的主要任务是对多个相关进程在执行次序上进行协调,以使并发执行的个进程发之间能有效地共享资源和相互合作,从而使程序的执行具有可再现性。

  • 进程间的制约关系

在多道程序环境下,系统中有许多并发的进程,在这些进程之间存在以下两种关系。

(1)间接制约关系

多个进程之间彼此无关,它们并不知道其他进程的存在,但这些进程既然是同处于一个系统中,也就必然存在资源共享的关系,如共享CPU和I/O设备等。此时,系统的主要任务是保证各个进程能互斥地访问临界资源。为此,系统中的资源应不允许用户进程直接使用,而应由系统统一分配。例如:在仅有一台打印机的系统中,有两个进程A和B,如果在A进程提出打印请求时,系统已将打印机分配给进程B,则系统让A进程等待,直至B将打印机用完并释放后,系统才将打印机分配给进程A。如果不采取任务红纸措施,则会出现打印机输出结果错误。

由于各进程要求共享资源,而有些资源需要互斥使用,因此各个进程间竞争使用这些资源,进程间的这种关系称为间接制约,操作系统必须采用某种方法来保证存在间接制约关系的进程间并发执行的正确性,这就是互斥机制

(2)直接制约关系

多个进程间知道对方的存在,表现出来的是一种相互合作的关系。此时要保证相互合作的各个进程在执行次序上的协调,不会出现与时间有关的差错。例如:有一计算进程A将计算结果通过单缓冲向打印进程B提供数据打印。当该缓冲区空时,打印进程B因不能获得所需数据而等待。当计算进程A把数据送入缓冲区时,便应向进程B发送一个信号将它唤醒;反之,当缓冲区满时,计算进程A因无法再向缓冲区中投放数据而等待,只有当打印进程B将缓冲区数据取走时,再向A发信号将A唤醒。如果不控制进程间的执行顺序则会出现错误,例如在B没有取走数据时A向缓冲区中放数据,就会造成数据丢失。

系统中多个进程中发生的事件存在某种时序关系,各自的执行结果互为对方的执行条件,需要相互合作,共同完成一项任务。进程间的这种关系称为直接制约,操作系统也应该采用某种方法保证存在直接制约关系的进程间并发执行的正确性,这就是同步机制

综上所述,虽然互斥与同步在概念上有差别,解决问题的方法也不相同。但从某种意义上来说,互斥也是一种同步,因为它也是进程之间如何使用共享资源的一种协调。

  • 进程互斥

(1)临界资源与临界区

系统中某些资源一次只允许一个进程使用,如打印机、共享数据等。这样的资源称为临界资源或互斥资源,多个进程必须互斥地对它进行访问。把在每个进程中访问临界资源的那段代码称为临界区。

显然,若能保证各个进程互斥地进入自己的临界区,便可实现他们对临界资源的互斥访问。为此,每个进程在进入临界区之前应先对欲访问的临界资源进行检查,看他是否正被访问。如果刺客临界资源未被访问,该进程便可进入临界区,对该资源进行访问,并设置它正被访问的标志;如果刺客该临界资源正在被某进程访问,则进程不能进入临界区。因此,必须在临界区前面增加一段用于进行上述检查的代码,把这段代码称为进入区。相应地,在临界区后面也要加上一段称为退出区的代码,用于将临界区正被访问的标志回复为未被访问标志。进程中的除上述进入区、临界区以及退出区之外的其他部分代码,称为剩余区。

(2)互斥

可以把互斥定义为:一组并发进程中的一个或多个程序段,因共享某一共有资源而导致它们必须以一个不允许交叉执行的单位执行。也就是说,不允许两个以上的共享该资源的并发进程同时进入临界区称为互斥。

为实现进程互斥,可在系统中设置专门的机构来协调各个进程间的运行,这些机制应遵循下列四条准则。

空闲让进。当无进程处于临界区时,相应的临界资源处于空闲状态,因而可允许一个请求进入临界区的进程立即进入自己的临界区,以有效地利用临界资源。

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

有限等待。对要求访问临界资源的进程,应保证该进程能在有效时间内进入自己的临界区,以免陷入“死等”状态,包括饥饿(若一个进程无线等待)或死锁(两个以上进程相互无线等待)。

让权等待。当进程不能进入自己的临界区时,应立即释放处理机,以免进程陷入“忙等”。

知道了并发进程互斥执行时所必须遵守的准则后,怎样实现并发进程的互斥呢?可能会有人认为只需把并发进程的临界区按不同时间排列调度就行,但事实上这是不可能的。因为这需要该组并发进程中的每个进程事先知道其他并发进程与系统的动作,由进程并发执行的随机性可知,这是不可能的。

(3)互斥的软件实现

软件实现的方法有很多种算法,基本原理是:在进入区设置和检查一些标志来标明是否有进程在临界区使用临界资源,如果已有进程在临界区,则在进入区通过循环检查进行等待,进程离开临界区后则在退出区修改标志。

下面介绍一种对临界区加锁实现互斥的方法。

当某个进程进入临界区之后,它将锁上临界资源,直到退出临界区为止,直到退出临界区为止。并发进程在申请进入临界区时,首先测试该临界区中的临界资源是否是上锁的。如果该临界资源已被锁住,则该进程要等到开锁之后才有可能获得临界资源。假设临界资源名为S,为了保证每一个临界资源只被一个临界区执行,再假设一个锁Key[S]。Key[S]表示该锁属于名为S的临界资源。加锁后的临界区程序描述如下:

lock(Key[S])

<临界区>

unlock(Key[S])

设Key[S]=1时表示名为S的临界资源可用,Key[S]=0时表示名为S的临界资源不可用。则unlock(Key[S])只用一条语句就可以实现,即Key[S]<-1(将1指向Key[S])。不过,由于lock(Key[S])必须满足Key[S]=0时,不允许任何进程进入临界区,而Key[S]=1时仅允许一个进程进入临界区的准则,因而实现起来比较困难。

一种简便的实现方法是:

lock(x) = begin local v

        repeat

                v<-x

        untilv=1

        x<-0

    end

不过,这种实现方式是不能保证并发进程互斥执行所需要的准则2的。因为当有几个进程并发调用lock(Key[S])时,在x<-0语句执行之前,可能已有两个以上的多个进程由于Key[S]=1而进入临界区。为了解决这个问题,有些机器在硬件中设置了“测试与设置(test and set)”指令,从而保证第一步和第二步执行的不可分离性。

注:在系统实现时,锁Key[S]总是设置在临界资源所对应的数据结构中。

(4)互斥的硬件指令实现

TestAndSet指令:这条指令是原子操作,级执行该代码时不允许被中断。其功能是读出指定标志后把该标志设置为真。指令的功能描述如下:

boolean TestAndSet(Boolean *lock){
            boolean old;
            old = *lock;
            *lock = true;
            return old;
        }

可以为每个临界资源设置一个共享布尔变量lock,表示资源的两种状态:true表示正被占用;初值为false。在进程访问临界资源之前,利用TestAndSet检查和修改标志lock;若有进程在临界区,则重复检查,直到进程退出。利用该指令实现进程互斥的算法描述如下:

while TestAndSet(&lock);

进程的临界区代码;

lock = false;

进程的其他代码;

硬件方法的有点:适用于任意数目的进程,不管是单处理器还是多处理器;简单、容易验证其正确性。可以支持进程内有多个临界资源,只需为每个临界资源设置一个布尔变量。

硬件方法的缺点:进程等待进入临界区时要耗费处理器时间,不能实现让权等待。

  • 进程同步

对于具有直接制约关系进程间的并发执行,保证进程正确执行的最为简单和直观的方法是直接制约的进程互相给对方进程发送执行条件已经具备的信号。这样,被制约进程即可省去对执行条件的测试,只要收到了制约进程发来的信号便开始执行,而在未收到制约进程发来的信号时便进入等待状态。

把异步环境下的一组并发进程,因直接制约而需要互相发送消息进行互相合作、互相等待,使得各进程按一定的速度执行的过程称为进程间的同步。

具有同步关系的一组并发进程称为合作进程,合作进程间互相发送的信号称为消息或事件。如果对一个消息或事件赋以唯一的消息名,则可用过程wait(消息名)表示进程等待合作进程发来的消息,而用过程signal(消息名)表示进程向合作进程发送消息。

例:利用过程wait和signal实现计算进程Pc和打印进程Pp的同步关系:

设消息名Bufempty表示Buf空,消息名Buffull表示Buf中装满了数据。

初始化Bufempty=true,Buffull=false。

Pc:

        A:wait(Bufempty)

              计算

              Buf<-计算结果

              Bufempty<-false

              signal(Buffull)

              Goto A

Pp:

         B:wait(Buffull)

              打印Buf中数据

              清楚Buf中数据

              Buffull<-false

              signal(Bufempty)

              Goto B

  • 信号量机制

(1)信号量

尽管用加锁的方法可以实现进程之间的互斥,但这种方法仍然存在一些影响系统可靠性和执行效率的问题。例如,循环测试锁将耗损较多的CPU计算时间。如果一组并发进程的进程数较多,且由于每个进程在申请进入临界区时都得对锁进行测试,则这种开销是很大的。另外,使用加锁法实现进程间互斥,由于进程执行的随机性, 会出现某个进程一直得不到临界资源的饥饿现象。

怎样解决上述问题呢?首先,必须找到产生上述问题的原因。显然,在用加锁法解决进程互斥的问题时,一个进程能否进入临界区是依靠进程自己调用lock过程去测试相应的锁定位。也就是说,每个进程能否进入临界区时依靠自己的测试判断。这样,没有获得执行机会的进程当然无法判断,从而出现不公平现象。而获得了测试机会的进程又因需要测试而损失一定的CPU时间。

举例来说就是某个学生想使用某个认人都可使用且不规定使用时间的教室,他必须先申请获得使用该教室的权利,然后再到教室看看该教室是否被锁上了。如果该教室被锁上了,那他只好下次再来查看,看该教室的门是否已被打开。这种反复将持续到他进入教室为止。针对这个问题,可以设置一个教室管理员。如果有学生申请使用教室而没能如愿时,教室管理员记下他的名字,等教室门开了就通知该学生进入。

在操作系统中,这个教室管理员就是信号量(semaphore)。信号量管理相应临界区的临界资源,它代表可用资源实体。

信号量是一个记录型数据结构,它有两个数据项,分别表示临界资源可用数目的整型值和用于链接等待使用该资源的链接指针。定义如下:

struct semaphore{
	int value;
	pointer_PCB queue;
}semaphore S

建立一个信号量必须经过说明所建信号量的意义、赋初值以及相应的数据结构以便指向那些等待使用该临界资源的进程。除初始化外,信号量仅能通过两个标准的原子操作P(S)和V(S)来访问。

(2)P、V原语

信号量的数值仅能由P、V原语操作改变,一次P原语操作使得信号量S减1,而一次V原语操作将使得信号量S加1。

这两个操作的定义为:

void P(S){
	S.value = S.value--;
	if(S.value<0)
		block(S.queue);		/*阻塞调用本操作的进程*/
}

一个进程q在给定信号量S上的P操作,首先将该信号量的值减1。若减1后其值小于0,则将调用P操作的进程q阻塞起来,将其PCB插入该信号量的等待队列并转进程调度;否则,操作结束,该进程继续执行并进入临界区。

void V(S){
	S.value = S.value++;
	if(S.value<=0)
		wakeup(S.queue);		/*唤醒等待队列一个进程,变其状态为就绪态*/
}

一个进程在给定信号量S上的V操作,首先将该信号量的值加1。若加1后其值不大于等于0,则将该信号量等待队列中的第1个进程唤醒,并将其状态变为就绪,否则,操作结束,该进程继续执行。

P、V操作的含义为:P(S)表示申请一个资源或等一个可用信号,S.value>0表示有资源可用,其值为资源数目;S.value=0表示无资源可用;S.value<0,则|S.value|表示S等待碎裂中的进程个数。V(S)表示释放一个资源或发一个可用信号,信号量的初值应该大于0。

一是执行P(S)操作出现进程阻塞时,被阻塞的进程应是调用P(S)的进程;而执行V(S)操作有进程被唤醒时,被唤醒的进程不是调用V(S)的进程,而是在S上阻塞的一个进程。二是P,V操作是原语操作,当一个进程正在修改某信号量时,不会有别的进程“同时”修改该信号量。

(3)信号量机制实现互斥

设信号量S是用于互斥的信号量,且其初值为1。

当一个进程想要进入临界区时,它必须先执行P原语操作以将信号量S减1。在一个进程完成对临界区的操作之后,它必须执行V原语操作以释放它所占用的临界资源。由于信号量初始值为1,所以,任一进程在执行P原语操作之后将S的值变为0,然后进入临界区执行。在该进程未执行V原语操作之前如有另一进程想进入临界区的话,它也应先执行P原语操作,从而使S的值变为-1,因此,第二个进程将被阻塞。知道第一个进程执行V原语操作之后,S的值变为0,唤醒第二个进程进入就绪队列,经调度后再进入临界区。在第二个进程执行完V原语操作之后,如果没有其他进程申请进入临界区的话,则S又恢复到初始值。

实现进程互斥的算法如下:

semaphore S=1;
P1(){
	P(S);
	临界区;
	V(S);
}
P2(){
	P(S);
	临界区;
	V(S);
}

(4)信号量机制实现同步

信号量机制也可以用来解决进程同步问题,把各进程之间发送的消息作为信号量来看待。与进程互斥时不同的是,这里的信号量只与制约进程及被制约进程有关,而不是与整组并发进程有关。因此,该信号量也可以叫做私用信号量。

一个进程Pi的私用信号量S是从制约进程发送来的进程Pi执行条件所需要的消息。与私用信号量对应,互斥时使用的信号量为公用信号量。

信号量机制实现进程同步的方法与利用wait和signal过程相同,也是分为三步。首先为各并发进程设置私用信号量,然后为私用信号量赋初值,最后利用P、V原语规定各进程的执行顺序。

设S为实现进程P1、P2同步的信号量,初值为0。进程P2中的语句y要使用进程P1中语句x的运行结果,所以只有当语句x执行完成之后进程P2才可以执行语句y。

实现进程同步的算法如下:

semaphore S=0;
P1(){
	...;
	x;
	V(S);
}
P2(){
	...;
	P(S);
	y;
}

可以发现,使用信号量机制解决进程同步互斥问题时,P、V操作必须成对出现,有一个P操作就一定有一个V操作。同步操作中,同一信号量的P、V操作处于不同进程中;互斥操作中,则处于同一进程中。

 

猜你喜欢

转载自blog.csdn.net/Dream_Ryoma/article/details/81628804