第二章 进程管理(3)——进程同步

2.4、进程同步

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

进程同步是指系统中多个进程发生的事件存在某种时序关系(即表示多个进程之间的执行是有先后顺序的),需要相互协作,共同完成一个任务。

1、两种形式的制约关系

       在多道程序环境下,当程序并发执行时,由于资源共享和进程合作,使同处于一个系统中的诸进程之间可能存在着以下两种形式的制约关系

(1) 间接相互制约关系

       同处于一个系统中的进程,通常都共享着某种系统资源,如共享CPU、共享I/O 设备等。所谓间接相互制约即源于这种资源共享,例如,有两个进程AB,如果在A进程提出打印请求时,系统已将惟一的一台打印机分配给了进程B,则此时进程A只能阻塞;一旦进程B将打印机释放,则A进程才能由阻塞改为就绪状态

(2) 直接相互制约关系

       这种制约主要源于进程间的合作。例如,有一输入进程A通过单缓冲向进程B 提供数据。当该缓冲空时,计算进程因不能获得所需数据而阻塞,而当进程A把数据输入缓冲区后,便将进程B唤醒;反之,当缓冲区已满时,进程A因不能再向缓冲区投放数据而阻塞,当进程B将缓冲区数据取走后便可唤醒A。

2、临界资源

      许多硬件资源如打印机、磁带机等,都属于临界资源(Critical Resource),诸进程间应采取互斥方式,实现对这种资源的共享

      生产者-消费者(producer-consumer)问题是一个著名的进程同步问题它具体是一个什么样的问题呢?该问题是:生产者进程会出现缓冲区满的情况下继续向缓存区写入数据,消费者进程会出现在缓冲区为空的情况下向缓冲区读取数据。所以,生产者-消费者问题的解决方案关键需要处理的地方是:要保证生产者在缓冲区满的情况下不在向缓冲区写入数据,消费者在缓冲区空的情况不向缓冲区读取数据。

它描述的是:有一群生产者进程在生产产品,并将这些产品提供给消费者进程去消费。为使生产者进程与消费者进程能并发执行,在两者之间设置了一个具有n个缓冲区的缓冲池,生产者进程将它所生产的产品放入一个缓冲区中;消费者进程可从一个缓冲区中取走产品去消费。尽管所有的生产者进程和消费者进程都是以异步方式运行的,但它们之间必须保持同步,即不允许消费者进程到一个空缓冲区去取产品,也不允许生产者进程向一个已装满产品且尚未被取走的缓冲区中投放产品。

      我们可利用一个数组来表示上述的具有n 个(0,1,…,n-1)缓冲区的缓冲池。用输入指针in 来指示下一个可投放产品的缓冲区,每当生产者进程生产并投放一个产品后,输入指针加1;用一个输出指针out来指示下一个可从中获取产品的缓冲区,每当消费者进程取走一个产品后,输出指针加1。由于这里的缓冲池是组织成循环缓冲的,故应把输入指针加1 表示成 in:= (in+1)mod n;输出指针加1 表示成out:= (out+1) mod n。当 (in+1) mod n=out时表示缓冲池满;而in=out则表示缓冲池空。此外,还引入了一个整型变量counter,其初始值为0。每当生产者进程向缓冲池中投放一个产品后,使counter 加1;反之,每当消费者进程从中取走一个产品时,使counter 减1。

       指针in和out初始化为1。在生产者和消费者进程的描述中,noop是一条空操作指令,whilecondition do no-op语句表示重复的测试条件(condication),重复测试应进行到该条件变为false(假),即到该条件不成立时为止。在生产者进程中使用一局部变量nextp,用于暂时存放每次刚生产出来的产品;而在消费者进程中,则使用一个局部变量nextc,用于存放每次要消费的产品。

producer: repeat

    …

   produce an item in nextp;

   …

   while counter = n do no-op;

           buffer[in]:= nextp;

           in := in+1 mod n;

          counter := counter+1;

until false;

consumer: repeat

    while counter = 0 do no-op;

       nextc := buffer[out];

      out := (out+1) mod n;

      counter := counter-1;

       consumer the item in nextc;

until false;

       虽然上面的生产者程序和消费者程序在分别看时都是正确的,而且两者在顺序执行时其结果也会是正确的,但若并发执行时就会出现差错,问题就在于这两个进程共享变量counter。生产者对它做加1操作,消费者对它做减1操作,这两个操作在用机器语言实现时。

       常可用下面的形式描述(这里需要特别注意的是变量加1和变量减1操作用机器语言表示时其形式,引入了一个中间寄存器变量,我们将变量的值赋给中间寄存器变量,然后对中间寄存器变量进行加1或减1操作,最后再把中间寄存器变量的值赋给该变量。我们不是直接对该变量进行加1或减1操作。也就是说一个变量加1或减1操作在操作系统中它是被分成了三步进行计算:第一步将该变量的值赋给中间寄存器变量;第二步对该中间寄存器变量进行加1或减1操作;第三步将中间寄存器变量赋给该变量。这一点需要特别注意,这对于进程同步时进行变量值的计算值非常重要,要记得将其拆分为三个步骤)

register1:=counter;             register2:=counter;

register1:=register1+1;       register2:=register2-1;

counter:=register1;             counter:=register2;

      假设counter 的当前值是5。如果生产者进程先执行左列的三条机器语言语句,然后消费者进程再执行右列的三条语句,则最后共享变量counter 的值仍为5;反之,如果让消费者进程先执行右列的三条语句,然后再让生产者进程执行左列的三条语句,则counter 值也还是5,但是,如果按下述顺序执行:

 

register1:=counter;      (register1=5)

register1:=register1+1; (register1=6)

register2:=counter;      (register2=5)

register2:=register2-1; (register2=4)

counter:=register1;      (counter=6)

counter:=register2;      (counter=4)

      正确的counter 值应当是5,但现在是4。读者可以自己试试,倘若再将两段程序中各语句交叉执行的顺序改变,将可看到又可能得到counter=6的答案,这表明程序的执行已经失去了再现性。为了预防产生这种错误,解决此问题的关键是应把变量counter作为临界资源处理,亦即,令生产者进程和消费者进程互斥地访问变量counter

3、临界区

      人们把在每个进程中访问临界资源的那段代码称为临界区(critical section)这里需特别注意:临界区是一段代码,它是进程中访问临界资源的那段代码。)。显然,若能保证诸进程互斥地进入自己的临界区,便可实现诸进程对临界资源的互斥访问。为此,每个进程在进入临界区之前,应先对欲访问的临界资源进行检查,看它是否正被访问。如果此刻该临界资源未被访问,进程便可进入临界区对该资源进行访问,并设置它正被访问的标志;如果此刻该临界资源正被某进程访问,则本进程不能进入临界区。因此,必须在临界区前面增加一段用于进行上述检查的代码,把这段代码称为进入区(entry section)。相应地,在临界区后面也要加上一段称为退出区(exit section)的代码,用于将临界区正被访问的标志恢复为未被访问的标志。进程中除上述进入区、临界区及退出区之外的其它部分的代码,在这里都称为剩余区。这样,可把一个访问临界资源的循环进程描述如下:

repeat

     entry section              (进入区)

     critical section;       (临界区)

     exit section                (退出区)

     remainder section;  (剩余区)

until false;

4、同步机制应遵循的规则

      为实现进程互斥地进入自已的临界区,可用软件方法,更多的是在系统中设置专门的同步机构来协调各进程间的运行。所有同步机制都应遵循下述四条准则

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

      (2) 忙则等待。当已有进程进入临界区时,表明临界资源正在被访问,因而其它试图进入临界区的进程必须等待,以保证对临界资源的互斥访问。

      (3) 有限等待。对要求访问临界资源的进程,应保证在有限时间内能进入自己的临界区,以免陷入“死等”状态。

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

5、信号量机制(是一种进程同步工具

      1965 年,荷兰学者Dijkstra提出的信号量(Semaphores)机制是一种卓有成效的进程同步工具。信号的类型有以下几种:

(1)整型信号量(表示该信号量是一个整型变量

       最初由Dijkstra 把整型信号量定义为一个用于表示资源数目的整型量S(即也就是说它是一个整型变量,但又与一般的整型变量不同),它与一般整型量不同,除初始化外,仅能通过两个标准的原子操作(Atomic Operation)wait(S)signal(S)来访问。很长时间以来,这两个操作一直被分别称为PV操作。wait(S)和signal(S)操作可描述为:

wait(S):  while S <= 0 do no-op;

                   S := S - 1;

signal(S): S := S + 1;

       wait(S)signal(S)是两个原子操作,因此,它们在执行时是不可中断的。亦即,当一个进程在修改某信号量时,没有其他进程可同时对该信号量进行修改。此外,在wait操作中,对S值的测试和做S:=S-1操作时都不可中断

(2)记录型信号量(表示该信号量是一个记录型变量(也就是结构体型变量)

       在整型信号量机制中的wait操作,只要是信号量S≤0,就会不断地测试。因此,该机制并未遵循“让权等待”的准则,而是使进程处于“忙等”的状态。记录型信号量机制则是一种不存在“忙等”现象的进程同步机制。但在采取了“让权等待”的策略后,又会出现多个进程等待访问同一临界资源的情况。为此,在信号量机制中,除了需要一个用于代表资源数目的整型变量value外,还应增加一个进程链表指针L,用于链接上述的所有等待进程。记录型信号量是由于它采用了记录型的数据结构而得名的。它所包含的上述两个数据项可描述为:

type semaphore = record

     value: integer;

     L: list of process;

end

相应地,wait(S)和signal(S)操作可描述为:

procedure wait(S)

     var S:semaphore;

    begin

          S.value := S.value - 1;

          if  S.value <0 then block(S.L);

    end

procedure signal(S)

      var S: semaphore;

      begin

           S.value := S.value + 1;

           if  S.value <=0 then wakeup(S.L);

     end

       在记录型信号量机制中,S.value的初值表示系统中某类资源的数目,因而又称为资源信号量对它的每次wait操作,意味着进程请求一个单位的该类资源,使系统中可供分配的该类资源数减少一个,因此描述为S.value:=S.value-1;当S.value<0时,表示该类资源已分配完毕,因此进程应调用block原语,进行自我阻塞,放弃处理机,并插入到信号量链表S.L 中。可见,该机制遵循了“让权等待”准则。此时S.value的绝对值表示在该信号量链表中已阻塞进程的数目对信号量的每次signal操作,表示执行进程释放一个单位资源,使系统中可供分配的该类资源数增加一个,故S.value:=S.value+1操作表示资源数目加1。若加1后仍是S.value0,则表示在该信号量链表中,仍有等待该资源的进程被阻塞,故还应调用wakeup原语,将S.L链表中的第一个等待进程唤醒如果S.value的初值为1,表示只允许一个进程访问临界资源,此时的信号量转化为互斥信号量,用于进程互斥

(3)AND型信息量

      上述的进程互斥问题,是针对各进程之间只共享一个临界资源而言的。在有些应用场合,是一个进程需要先获得两个或更多的共享资源后方能执行其任务假定现有两个进程AB,他们都要求访问共享数据DE。当然,共享数据都应作为临界资源。为此,可为这两个数据分别设置用于互斥的信号量DmutexEmutex,并令它们的初值都是1。相应地,在两个进程中都要包含两个对Dmutex 和Emutex 的操作,即

process A:                     processB:

wait(Dmutex);           wait(Emutex);

wait(Emutex);            wait(Dmutex);

若进程A和B按下述次序交替执行wait操作:

process A: wait(Dmutex);于是Dmutex = 0

process B: wait(Emutex);于是Emutex = 0

process A: wait(Emutex);于是Emutex = -1     A阻塞

process B: wait(Dmutex);于是Dmutex = -1    B阻塞

      最后,进程A和B处于僵持状态。在无外力作用下,两者都将无法从僵持状态中解脱出来。我们称此时的进程AB已进入死锁状态。显然,当进程同时要求的共享资源愈多时,发生进程死锁的可能性也就愈大。

       AND 同步机制的基本思想是:将进程在整个运行过程中需要的所有资源,一次性全部地分配给进程,待进程使用完后再一起释放。只要尚有一个资源未能分配给进程,其它所有可能为之分配的资源也不分配给它。亦即,对若干个临界资源的分配,采取原子操作方式:要么把它所请求的资源全部分配到进程,要么一个也不分配。由死锁理论可知,这样就可避免上述死锁情况的发生。为此,在wait 操作中,增加了一个“AND”条件,故称为AND 同步,或称为同时wait 操作,即Swait(Simultaneous wait)定义如下:

Swait(S1,S2,…,Sn)

     if Si >= 1 and … and Sn >= 1 then

          for i := 1 to n do

              Si := Si - 1;

          endfor

      else

place the process in the waitingqueue associated with the first Si found                 withSi < 1,and set theprogram count of this process tothe beginning of Swait operation

     endif

Ssignal(S1,S2,…,Sn)

      for i := 1 to n do

            Si := Si + 1;

            Remove all the process waitingin the queue associated with Si into the ready queue.

      endfor;

(4)信号量集

        在记录型信号量机制中,wait(S)或signal(S)操作仅能对信号量施以加1 或减1 操作,意味着每次只能获得或释放一个单位的临界资源。而当一次需要N个某类临界资源时,便要进行Nwait(S)操作,显然这是低效的。此外,在有些情况下,当资源数量低于某一下限值时,便不予以分配。因而,在每次分配之前,都必须测试该资源的数量,看其是否大于其下限值。基于上述两点,可以对AND信号量机制加以扩充,形成一般化的“信号量集”机制。Swait操作可描述如下,其中S为信号量,d为需求值,而t为下限值

Swait(S1,t1,d1,…,Sn,tn,dn)

     if Si >= t1and … and Sn >= tn then

         for i := 1 to ndo

             Si := Si - di;

         endfor

    else

Place theexecuting process in the waiting queue of the first Si with Si<ti and setits program counter to the beginning of the Swait Operation.

    endif

Ssignal(S1,d1,…,Sn,dn)

     for i := 1 to n do

         Si := Si + di;

         Remove all the process waiting in the queueassociated with Si into the ready queue

     endfor;

下面我们讨论一般“信号量集”的几种特殊情况:

      1) Swait(S,d,d)。此时在信号量集中只有一个信号量S,但允许它每次申请d 个资源,当现有资源数少于d时,不予分配。

      2) Swait(S,1,1)。此时的信号量集已蜕化为一般的记录型信号量(S>1时)或互斥信号量(S=1时)。

      3) Swait(S,1,0)。这是一种很特殊且很有用的信号量操作。当S≥1 时,允许多个进程进入某特定区;当S 变为0 后,将阻止任何进程进入特定区。换言之,它相当于一个可控开关。

 

注意点:信号量实现进程同步和实现进程互斥的区别???

1、信号量实现进程的同步

      利用信号量能方便的实现进程的同步。设S为两个并发进程P1和P2的公共信号量,初值为0。P1执行的程序中有一条语句S1,P2执行的程序中有一条语句S2。而且,只有当P1执行语句S1后,P2才能开始执行S2语句。对这种简单的同步问题,很容易使用信号量解决。其描述如下:

main()

{

       semaphore S = 0; // 信号量S置初值0

       cobegin

              P1();

              P2();

       coend

}

P1()

{

       ...

       S1;

       V(S);     // 这个进程执行V操作

       ...

}

P2()

{

       ...

       P(S);      // 这个进程执行P操作

       S2;

       ...

}

      同步的实现由一个进程P1对一个信号量进行P操作后,只能由另一个进程P2对同一个信号量进行V操作,使P1能继续前进,在这种情况下,P1要同步等待P2。若进程P2也要同步等待P1,则要设置另一个信号量。(实现进程的同步的信号量的P操作和V操作是在不同的进程中,这一点需要特别注意!!!)

2、信号量实现互斥

     利用信息量可以方便的实现进程互斥,S为两个进程P1P2实现互斥的信号量,由于每次只允许一个进程进入临界区,所以S的初值应为1(即可用资源个数为1)。只须把临界区置于P(S)V(S)之间,即可实现两个进程的互斥。互斥访问临界区的描述如下:

main()

{

      semaphoreS = 1; // 信号量S置初值0

      cobegin

             P1();

             P2();

      coend

}

P1()

{

      ...

      P(S)

      进程1的临界区

      V(S);    

      ...

}

P2()

{

      ...

      P(S);

      进程2的临界区

      V(S);

      ...

}

      两个或多个进程使用的初值为1的信号量可保证同时只有一个进程能够进入临界区。如果每个进程在进入临界区前都执行一个P操作,并在退出时执行一个V操作,则能实现互斥。

       互斥的实现是不同进程对同一个信号量进行PV操作,一个进程在成功的对信号量执行了P操作后进入临界区,并在退出临界区后,由该进程本身对该信号量执行V操作,表示当前没有进程进入临界区,可让其他进程进入(实现进程的互斥的信号量的P操作和V操作是在同一个的进程中,这一点需要特别注意!!!)

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值