第三章——进程之间的并发控制和死锁

第三章——进程之间的并发控制和死锁

3.1 并发进程的关系

3.2 进程之间的低级通信:互斥、同步、信号量和P/V操作、经典IPC问题

3.3 管程

3.4 进程的高级通信:消息传递,共享内存

3.5 死锁:多进程竞争有限资源

3.1 并发进程的特点

(1)对资源的共享引起的互斥关系
进程之间本来是相互独立的,但由于共享资源而产生了关系。间接制约关系,互斥关系。

(2)协作完成同一个任务引起的同步关系
一组协作进程要在某些同步点上相互等待发信息后才能继续运行。直接制约关系,同步关系。

(3)进程之间的前序关系
由于进程之间的互斥同步关系,使得进程之间具有了前序关系,这些关系决定了各个进程创建和终止的时间。

理解什么是(资源共享)互斥,什么是(协作)同步很关键

3.2 进程之间的低级通信

  • 进程间通信:是指进程之间交换信息。
  • 进程间的低级通信:通过信号量实现进程之间的互斥和同步关系。
  • 进程间通信问题——IPC问题( InterProcess Communication, IPC )

3.2.1 进程之间的互斥

  • 共享资源:(1)慢速的硬设备,如打印机;2()软件资源,如共享变量共享文件和各种队列等。
  • 临界资源:一次仅允许一个进程使用的系统中共享资源。
  • 临界区(critical section):并发进程访问临界资源的那段必须互斥执行的程序

注意什么是临界资源,什么是临界区

并发进程进入临界区需要遵循的四个准则
  • 不能同时有两个进程在临界区内执行
  • 等待进入临界区的进程,应释放处理机后阻塞等待
  • 在临界区外运行的进程不可阻止其他进程进入临界区
  • 不应使要进入临界区的进程无限期等待在临界区之外

互斥使用;让权等待;有空让进;有限等待

解决进程之间互斥的方法
软件实现方法
  • 算法1:
设有两个进程Pi和Pj,共享变量
int  turn;
当turn==i时,进程Pi在临界区内执行 。
执行顺序:Pi, Pj, Pi, Pj…
  • 算法2:
设有两个进程Pi和Pj,共享变量
  boolean flag[2];
初值,flag[i]=flag[j]=false 
如果flag[i]==true,则表示Pi准备进入临界区。
  • 算法3:
设有两个进程Pi和Pj,共享两个变量:
     boolean flag[2];
     int turn;
初值,flag[i]=flag[j]=false
turn为i或j都行。
硬件实现方法

(1) 关中断 最简单的方法。在进程刚进入临界区后,立即禁止所有中断;在进程要离开之前再打开中断。因为CPU只有在发生时钟中断或其它中断时才会进行进程切换

  • 优点:简单。
  • 缺点:限制了处理机交叉执行程序的能力
    • 把禁止中断的权力交给用户进程是不明智的。(若用户进程禁止中断之后不再打开中断,其结果将会如何?整个系统可能会因此终止)。
    • 若是多处理机系统,则禁止中断仅仅对执行本指令的那个CPU有效。其他CPU仍将继续运行,并可以访问临界资源。

(2) 使用测试和设置硬件指令

锁位变量W :为每个临界资源设置一个,以指示其当前状态。W=0,表示资源空闲可用;W=1,表示资源已被占用。

3.2.2 进程之间的同步

  • 同步的原因:一组进程要合作完成一项任务。
  • 由于计算进程与打印进程访问缓冲区的速度不匹配,需要进行同步处理
  • 为了使进程同步,需要引入信号量机制。

3.2.3 信号量和P、V操作

  • 1965年,荷兰学者Dijkstra提出的一种同步机制
  • 基本原理:两个或多个进程通过简单的信号进行合作,一个进程被迫在某一位置停止,直到它接收到一个特定的信号。为了发信号,需要使用一个称作信号量的特殊变量。
typedef struct{ 	//信号量的类型描述
 int value; //表示该类资源的可用数量
 struct process *list; //等待使用该类资源的进程排成队列的队列头指针。
}semaphore, sem;
  • 对信号量S的操作只允许执行P、V原语操作
P操作原语: //wait(s) ;  
void P (sem &s)
{  s.value = s.value-1;   //表示申请一个资源(或通过信号量s接收消息)
    if (s.value < 0)
         { add this process to s.list;
            block( );
         } //资源用完,调用阻塞原语。“让权等待”
}
V操作原语:// signal(s);  
Void V (sem &s) {  
     s.value = s.value+1;
      //释放一个资源(或通过信号量s发消息) 
  if (s.value <= 0) { 
      remove a process P from s.list;  
      wakeup( );
     }//表示在信号链表中,仍有等待该资源的进程被阻塞。 调用唤醒原语。
}
  • 显然,P、V操作的引入,克服了**加锁testset(w)**操作的忙等现象,提高了系统的效率。
  • 阻塞,唤醒
  • 根据用途不同,可以把信号量分为公用信号量和私用信号量。
  • 设置一个互斥信号量mutex,初值为1,表示该临界资源空闲。
  • 调用P(mutex)申请临界资源。
  • 调用V(mutex)释放临界资源。
  • 只需把临界区代码置于P(mutex)和 V(mutex)之间,就可实现临界资源的互斥使用了。
int  mutex=1;
P1:
  …
  	P(mutex);
 critical section V(mutex);
  …
P2:
  …
  P(mutex);
  临界区
  V(mutex);
  • 用信号量可以方便地解决n个进程互斥地执行临界区代码的问题。
  • 信号量的取值范围:+1~ -(n-1)
  • 信号量值为负时,说明有一个进程正在临界区执行,其它的正排在信号量等待队列中等待,等待的进程数等于信号量值的绝对值。
利用信号量实现进程之间的互斥
生产者和消费者问题
  • 生产者和消费者是相互合作进程关系的一种抽象
  • 生产者:当进程释放一个资源时,可把它看成是该资源的生产者,
  • 消费者:当进程申请使用一个资源时,可把它看成该资源的消费者。

假定有一组生产者和消费者进程,通过一个有界环形缓冲区(有k个缓冲区)发生联系。生产者向缓冲区放产品,消费者从中取产品。
当缓冲区满时,生产者要等消费者取走产品后才能向缓冲区放下一个产品;当缓冲区空时,消费者要等生产者放一个产品入缓冲区后才能从缓冲区取一个产品。
这个环形缓冲区是一个临界资源。互斥使用

  • empty:表示空缓冲区的个数,初值为k
  • full:有数据的缓冲区个数,初值为0
  • mutex:互斥访问临界区的信号量,初值为1
注意P操作的次序
  • 若生产者进程中的两个P操作的次序交换。
    • 当缓冲区满时,生产者将在P(empty)上等待,但不释放对缓冲区的互斥使用权。
    • 此后,消费者欲取产品时,由于申请使用缓冲区不成功,它将在P(mutex)上等待。
    • 相互等待就会造成系统发生死锁现象。
读者和写者问题

读/写问题:有一个多进程共享的数据区,这个数据区可以是一个文件或者主存的一块空间。有一些只读取这个数据区的进程(reader)和一些只往数据区中写数据的进程(writer)。此外还必须满足以下条件:

  • 多个读进程可以同时读这个数据区;
  • 一次只有一个写进程可以往数据区中写;
  • 若一个写进程正在写,禁止任何进程读。
信号量的设置
  • 写互斥信号量wmutex:实现读写互斥和写写互斥地访问共享文件,初值为1。
  • 计数器readcount:记录同时读的读者数,初值为0。
  • 读互斥信号量rmutex:使读者互斥地访问共享变量readcount,初值为1。
int rmutex=1,wmutex=1,readcount=0; 
Reader:
        P(rmutex); //互斥访问readcount
        if readcount=0 then P(wmutex);
          readcount++;
        V(rmutex); 
         读文件;  
        P(rmutex);
        readcount= readcount-1; 
        if readcount=0 then V(wmutex);
        V(rmutex);
 Writer: …
        P(wmutex);
        写文件;
        V(wmutex);
        …
读者/写者问题:优先策略
  • 读者优先策略
    • 只要有读者正在读状态,后来的读者都能直接进入
    • 如读者持续不断进入,则写者就处于饥饿
  • 写者优先策略
    • 只要有写者就绪,写者应尽快执行写操作
    • 如写者持续不断就绪,则读者就处于饥饿
  • 读者写者公平策略
    • 没有写者出现时,只要有读者正在读状态,后来的读者都能直接进入
    • 如有写者出现,读者和写着公平竞争进入
理发师问题

一个理发师和n把供等候理发的椅子。如果没有顾客,则理发师坐在椅子上睡觉,当有一个顾客到来时,就唤醒理发师,请求理发;如果理发师正在理发,又有顾客到来时,只要有空椅子,他就坐下来等待理发。请为理发师和顾客各编写一段程序来描述他们的同步问题。

理发师:    		顾客 : 
P(s1);查是否有顾客	P(s2); 申请空椅子
给顾客理发; 	         V(s1); 唤醒理发师
V(s2);                            坐椅子上等理发;
哲学家进餐问题
  • 这是一个典型的同步问题,是一大类并发控制问题的例子。

  • 假设有5个哲学家,花费一生的时光思考和吃饭。在桌子上放着5把叉子。一个哲学家要分两次去取其左边和右边的叉子。若得到两把叉子,就开始吃饭;吃完放下两把叉子。

int fork[0]=fork[1]==fork[4]=1;
第i个哲学家所执行的程序:
do{ 	
		P(mutex);
 		P(fork[i]);
 		P(fork[(i+1)mod5]);
 		V(mutex);
   		 吃饭
 		V(fork[i]);
 		V(fork[(i+1)mod5]);
	} while(1)

3.3 管程

  • 1973年,Hansan和Hoare提出了具有高级语言结构的管程。管程比信号量好控制。
  • 管程是关于共享资源的数据结构及一组针对该资源的操作过程所构成的软件模块。
  • 管程保证:一次只有一个进程执行管程中的代码。从而提供互斥机制,保证管程数据的一致性。

管程的组成:

Monitor  monitor-name {
	  
……   局部于该管程的共享变量的说明
condition …… 条件变量
     	  define ……;  本管程内定义的过程名 
     	  use ……; 	操作条件变量的同步原语
	  ……  本管程内定义的各过程(函数体)
}

用管程解决临界资源的互斥使用

Monitor  mutexshow { 
boolean busy=false; 	//临界资源是否可用标志
condition nonbusy; 	//等待队列的条件变量
define request, release; 	//管程中的过程说明
use wait, signal;  		//引用外部模块
} 

调用wait()的进程会阻塞在条件变量nonbusy的等待队列上。调用signal()会唤醒一个阻塞进程,若无阻塞进程则signal()不起作用。

procedure  request()    //申请临界资源的过程
{
   if busy then wait(nonbusy);   
   busy=true;      
}


procedure  release()      //释放临界资源的过程
{
   busy=false;           
   signal(nonbusy);  
}

用管程解决生产者和消费者问题

Monitor  prod_conshow
{   char  buffer[n] ;     	环形缓冲区
int  k=0;                    缓冲区中的产品个数
int nextempty=0, nextfull=0;/取产品的指针
condition nonempty,nonfull;    
define put, get;            管程中定义的过程说明
use wait(), signal(); 引用外部模块的过程说明
}
procedure put(product)  {     //向缓冲区送产品
	  if k=n then wait(nonfull); //缓冲区满等待
 buffer[nextempty] = product;
 k=k+1;
 nextempty=(nextempty+1)mod n;
 signal(nonempty);  //唤醒等待取产品消费者
}
procedure get(product) {         //从缓冲区取产品
      if k=0 then wait(nonempty);  //缓冲区空消费者等待
      product = buffer[nextfull];
      k=k-1;
      nextfull = (nextfull+1) mod n;
      signal(nonfull);  //唤醒等待送产品的生产者
  }
producer:   	
     {  char  item;
         produce an item;
         prod_conshow.put(item); 
     }

consumer:   	
     {  char  item;
         prod_conshow.get(item);
         consume an item;
     }

局限性

  • 管程是编程语言的组成部分,编译器知道它们的特殊性,因此对其操作做出互斥安排。
  • C、Pascal以及多数其他语言都没有管程。
  • Java使用管程机制。多线程互斥进入管程。
    • wait方法将挂起申请进入的线程并且暂时释放管程的所有权。
    • 而挂起的线程只能在其他线程使用notify方法时才能被唤醒。

3.4 进程的高级通信

  • 低级通信的优点:速度快。

  • 低级通信的缺点:

    • 传送信息量小且效率低。每次通信只能传递一个单位的信息量。
    • P、V操作使用不当时,易导致死锁。
    • 当程序非正常撤离时,查找只做了P操作而未做V操作的进程是很困难的。
  • 高级通信:是指进程采用操作系统提供的多种通信方式来实现通信。如消息缓冲信箱管道共享主存区等。

  • 发送进程和接收进程的消息通信方式:

    • 阻塞发送:发送进程阻塞,直到消息被接收。
    • 非阻塞发送:发送消息并继续运行。
    • 阻塞接收:接收进程阻塞,直到有消息可用。
    • 非阻塞接收:收到一个有效消息或空消息。

3.4.1 消息缓冲通信

  • 实现方法:
    • 系统设置一个消息缓冲池,其中每个缓冲区可以存放一个消息。
    • 每当进程欲发送消息时,向系统申请一个缓冲区,将消息存入缓冲区,然后把该缓冲区链接到接收进程的消息队列上。
    • 消息队列通常放在接收进程的进程控制块中。
    • 属于直接通信方式。
1) 消息缓冲区的类型
 
struct  message_buffer{
       xx    sender;	/* 发送进程标识符*/
       xx    size; 		/* 消息长度*/
       xx    text;  		/*消息正文*/
     struct message_buffer *next;
          	/*指向下一个消息缓冲区的指针*/
}
2)PCB中有关通信的数据项描述
   
 struct  PCB {
        …
        mq;     	//消息队列队首指针
        mutex; 	//消息队列互斥信号量
        sm;      	//消息队列同步信号量}
消息队列通常放在进程控制块中。

(3) 发送、接收系统调用

  • send (接收者,被发送消息始址)

  • receive (发送者,接收区始址)

  • 发送者先在自己的地址空间形成一个消息发送区,将消息写入其中,然后调用发送原语。

  • 发送原语:从系统缓冲区申请一个 消息缓冲区,将消息从发送区传入其中, 然后挂到接收进程的消息队列上。

  • 接收原语:将消息接收到自己的接收区

 send(receiver, a){ //发送原语
   getbuf(a.size, i)//据a区消息长度来申请一系统缓冲区i
    i.sender=a.sender;     i.size=a.size;
    i.text=a.text;    i.next=0;
   getid(PCB set, receiver, j);
     //获得接收进程的内部标识符j
   P(j.mutex);  
   insert(j.mq, i); //将i挂在接收进程j的消息队列mq上(属于临界资源)。
   V(j.mutex);
   V(j.sm);   //消息队列同步信号量sm
}    
receive(b){   //接收原语
       j=get caller’s internal name;   //内部标识符
      P(j.sm);    //等消息
      P(j. mutex);
      remove(j.mq, i); //从自己的消息缓冲队列mq中摘下第一个消息缓冲区i。
      V(j.mutex);
      b.sender=i.sender;
      b.size=i.size;
      b.text=i.text;
}
信箱通信原语

发送进程将消息发送到中间媒介——信箱,接收进程从中取得消息。

间接通信方式

  • 发送原语:send(A, Msg),将一个消息Msg发送到信箱A
  • 接收原语:receive(A, Msg),从信箱A中接收一个消息Msg

3.4.2 其他通信机制

  • 管道通信
    • 是指用于连接一个读进程和一个写进程的共享文件,又称pipe文件。OS强制实施互斥。
    • 是通过操作系统管理的环形缓冲区。先进先出。
    • 命令方式的管道通信;程序方式的管道通信。
  • 共享存储区(最快捷)
    • 诸进程为了相互交换大量数据,申请创建一块共享存储区,并将共享存储区映射到各自的地址空间,通过读或写共享存储区中的数据来实现通信。

3.5 死 锁

在计算机系统中有很多独占性的资源,在任一时刻只能被一个进程使用。如打印机、磁带机。

3.5.1 死锁的定义和产生的必要条件

1. 资源的特性
  • 可抢占资源:当资源从占用进程剥夺走时,对进程不产生什么破坏性的影响。如主存、CPU。
  • 不可抢占资源(临界资源):一旦分配,不能强收回,只能由其自动释放。如打印机、磁带机。
  • 死锁涉及的是不可抢占资源。
  • 进程使用资源的顺序:
  • 请求资源,使用资源,释放资源。
2. 死锁的定义

一组进程是死锁的,是指这一组中的每个进程都正在等待该组中的其他进程所占用的资源时,可能引起的一种错误现象。

3. 死锁产生的必要条件
  • 互斥条件。独占性的资源。
  • 保持和等待条件。进程因请求资源而阻塞时,对已经获得的资源保持不放。
  • 不剥夺条件。已分配给进程的资源不能被剥夺,只能由进程自己释放。
  • 循环等待条件。存在一个进程循环链,链中每个进程都在等待链中的下一个进程所占用的资源。

要分辨死锁四种产生的必要条件,特别是保持和等待条件和循环等待条件

4. 死锁产生的原因
  • 系统资源配置不足,引起进程竞争资源。
  • 并发进程请求资源的随机性,包括所请求资源的类别和数量。
  • 各并发进程在系统中异步向前推进,造成进程推进顺序的不合理性。

产生死锁的根本原因:是对独占资源的共享,并发进程的同步关系不当。

3.5.2 解决死锁的方法

  • 鸵鸟算法。忽略死锁。
  • 死锁的预防。通过破坏产生死锁的四个必要条件中的一个或几个,来防止发生死锁。
  • 死锁的避免。是在资源的动态分配过程中,用某种方法去防止系统进入不安全状态,从而避免发生死锁。
  • 死锁的检测和恢复。允许死锁发生,通过设置检测机构,及时检测出死锁的发生,然后采取适当措施清除死锁。
1. 鸵鸟算法
  • 最简单的解决方法是鸵鸟算法:把头埋进沙子里,假装毫无问题。
  • 如果死锁平均每5年发生一次,而每个月系统都会因硬件故障、编译器错误或者操作系统故障而崩溃一次,那么大多数工程师不会花费代价去防止系统死锁。
  • UNIX,Linux,Windows都采用该策略。
2. 死锁的预防

因产生死锁需四个必要条件。若d能破坏其中的一个或几个条件,则不产生死锁。

(1) 破坏互斥条件

  • 资源的互斥使用条件是由资源本身性质决定的,不能破坏。
  • 如果采用spooling技术,借助磁盘空间,就可以将一台独享设备改造成多台设备,以满足多个进程的共享需求。如打印机。
  • 实际中,不是所有设备都能采用spooling技术的。即使采用了该技术,由于多个进程竞争磁盘空间,磁盘空间的不足,仍可能导致死锁。

简单来说就是在内存中形成缓冲区,在高级设备形成输出井和输入井,传递的时候,从低速设备传入缓冲区,再传到高速设备的输入井,再从高速设备的输出井,传到缓冲区,再传到低速设备。

(2) 破坏保持和请求条件

  • 让进程在开始运行前,就获得所需的全部资源。若系统不能满足,则该进程等待。
  • 属静态分配,资源利用率低。
  • 许多进程在开始运行之前,不能精确提出所用资源数量。

(3) 破坏非剥夺条件

  • 当一个进程已占有某些资源,又申请新的资源而得不到满足时,则在进入阻塞状态前强行使其释放已经占有的资源。以后运行时,再重新申请。
  • 手段暴力,显然也不行,因为保护进程放弃资源时的现场以及之后的恢复现场,系统要付出很高的代价。

(4) 破坏循环等待条件

  • 将系统全部资源按类进行全局编号排序。进程对资源的请求必须按照资源的序号递增顺序进行。这样,就不会出现进程循环等待资源,预防死锁。
  • 但是前提是要将资源排好序,但是资源利用还是不合理的。

静态分配(一次给足)——破坏保持与请求条件

资源顺序分配法——破坏循环等待

3. 死锁的避免

基本思想:允许进程动态地申请资源,一次申请一部分资源。系统在进行资源分配之前,先计算资源分配的安全性。若此次分配不会导致系统进入不安全状态,便将资源分配给进程;否则,进程等待。

(1) 进程–资源轨迹图

  • 下图中:
  • 有两个进程A、B和两个资源(打印机和绘图仪)
  • 水平坐标表示进程A执行的指令序列;垂直坐标表示进程B执行的指令序列。
  • 两个进程不能同时进入阴影区域

(2) 银行家算法

  • 最具代表性的避免死锁的算法是Dijkstra的银行家算法,是在1965年提出的。
  • 利用了上面图中介绍的避免进程进入不安全区的原理。
  • 安全状态:是指系统能按某种顺序,如P1,P2,P3,…,Pn (安全序列),来为每个进程分配其所需资源,直至最大需求,使每个进程都可顺利完成。
4. 死锁的检测和恢复

死锁的检测和恢复技术:是指定期启动一个软件检测系统的状态,若发现有死锁存在,则采取措施恢复之。

(1)死锁的检测——用进程资源分配图检测

检查由进程和资源构成的有向图是否包含一个或多个环路,若是,则可能存在死锁,否则不存在。

(2)死锁的恢复

  • 故障,终止一些进程

    • 故障,终止所有死锁进程。简单。
    • 一次终止一个死锁进程,直到死锁解除为止。
  • 资源剥夺

    • 夺走一个进程的资源,给另一个进程使用。
    • 将一死锁进程滚回到获得资源之前的执行点。为进程设置执行点是指将进程在该点的执行状态信息写到一个文件中,便于以后从该执行点启动进程执行。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值