Skip to content

Latest commit

 

History

History
612 lines (555 loc) · 26.4 KB

计算机操作系统(八)——并发程序设计.md

File metadata and controls

612 lines (555 loc) · 26.4 KB

[TOC]

并发程序设计

顺序程序设计

  1. 程序是实现算法的操作(指令)序列
  2. 每个程序在处理器上执行是严格有序的,称为程序执行的内部顺序性
  3. 程序设计的一般习惯是顺序程序设计
    1. 把一个具体问题的求解过程设计成一个程序或者若干严格顺序执行的程序序列,称为程序执行的外部顺序性
  4. 特性:
    1. 程序执行的顺序性:程序指令执行是严格按序的
    2. 计算环境的封闭性:程序运行时如同独占受操作系统保护的资源
    3. 计算结果的确定性:程序执行结果与执行速度和执行时段无关
    4. 计算过程的可再见性:程序对相同数据集的执行轨迹是确定的

进程的并发执行

  1. 多道程序设计让多个程序同时进入内存去竞争处理器以获得运行机会
  2. OS允许计算机系统在一个时间段内存在多个正在运行的进程,即允许多个进程并发执行
  3. OS保证按照“顺序程序设计”方法编程的程序在并发执行时不受影响,如同独占计算机
  4. 这些按照顺序程序设计思想编程的进程在OS中并发执行属于无关的并发进程

处理器利用率计算

  1. 顺序程序设计:input(78s)、process(52s)、output(20s),52/(78+52+20)=35%
  2. 并发程序设计:时间同上,while(1){input,send}; while(1){receive,process,send}; while(1){receive,output},(52n)/(78n+52+20)=67%,n趋近于无穷大

并发程序设计

把一个具体问题求解设计成若干个可同时执行的程序模块的方法

特性:

  1. 并发性:多个进程在多道程序系统中并发执行或者在多处理器系统中并行执行:提高了计算效率
  2. 共享性:多个进程共享软件资源
  3. 交往性:多个进程并发执行时存在制约:增加了程序设计的难度

无关与交往的并发进程

  1. 无关:一组并发进程分别在不同变量集合上运行,一个进程的执行与其他并发进程的进展无关
  2. 交往:一组并发进程共享某些变量,一个进程的执行可能影响其他并发进程的结果

与时间有关的错误

  1. 对于一组交往的并发进程,执行的相对速度无法相互控制
  2. 如果程序设计不当,可能出现各种“与时间有关的”错误
  3. 表现一:结果错误;表现二:永远等待

进程互斥与进程同步

  1. 交互的并发进程在执行时必须进行制约,才能保证得到合理的结果
  2. 进程互斥:并发进程之间因相互争夺独占性资源而产生的竞争制约关系
  3. 进程同步:并发进程之间为完成共同任务基于某个条件来协调执行先后关系而产生的协作制约关系

互斥与临界区

  1. 临界资源:互斥共享变量所代表的资源,即一次只能被一个进程使用的资源
  2. 临界区:并发进程中与互斥共享变量相关的程序段
    1. 确定临界资源 share
    2. 确定临界区 region do <statement_list>
    3. 两个进程的临界区有相同的临界资源,就是相关的临界区,必须互斥进入
    4. 两个临界区不相关,进入就没有限制
    5. 临界区管理的三个要求
      1. 一次至多允许一个进程停留在相关的临界区内
      2. 一个进程不能无限止地停留在临界区内
      3. 一个进程不能无限止地等待进入临界区
    6. 临界区管理
      1. 同时进入临界区
        process P1
          begin
          	while inside2 do [ ];
        inside1 := true;
        临界区;
        inside1 := false;
        end;
        
        process P2
          begin
          	while inside1 do [ ];
        inside2 := true;
        临界区;
        inside2 := false;
        end;
      2. 同时等待进入临界区
        process P1
          begin
            inside1 := true;
        while inside2 do [ ];
        临界区;
        inside1 := false;
        end;
        
        process P2
          begin
          	inside2 := true;
        while inside1 do [ ];
        临界区;
        inside2 := false;
        end;
    7. 解决思路
      // 测试锁
      TS(x) {
      	if (x==false) {x=true;return true;
      	} else return false;
      }
      	Boolean lock;
      	lock=false;               // 临界区可用
      process Pi {
      	Boolean pi;
      	repeat pi=TS(lock) until pi; // 循环请求锁
      	临界区
      	lock=false;               // 解锁
      }
      // 交换锁
      swap(a, b){temp=a;a=b;b=temp;}
      	Boolean lock;
      	lock=false;              // 临界区可用
      process Pi {
      	Boolean pi;
      	pi=true;
      	repeat swap(lock, pi) until !pi; // 循环建立锁
      	临界区;
      	lock=false;              // 解锁
      }
    8. 实现临界区管理的硬件设施
      1. TS和swap指令均是忙式等待,效率低
      2. 简单的解决办法是在进出临界区时开关中断,这样临界区执行就不会中断了,执行就有原子性。关中断;临界区;开中断
      3. 操作系统原语就采用这种实现思路
      4. 但是,临界区的指令长度应该是短小精悍,这样才能保证系统效率
      5. 不建议用户程序使用,滥用是可怕的!
  3. 多个并发进程访问临界资源时,存在竞争制约关系。如果两个进程同时停留在相关的临界区内就会出现与时间相关的错误

PV操作与进程互斥

问题

  1. TS或swap指令管理临界区,采用忙式轮询,效率低
  2. 关开中断管理临界区,不便交给用户程序使用

信号量的构思

  1. 一种可动态定义的软件资源:信号量
    1. 核心数据结构:等待进程队列
    2. 信号量声明:资源报到,建立队列
    3. 申请资源的原语:若申请不到,调用进程入队等待
    4. 归还资源的原语:若队列中有等待进程,需释放
    5. 信号量撤销:资源注销,撤销队列
  2. 记录型信号量:一种带数值的软资源
    typedef struct semaphore {
    	int value;          // 信号量值
    	struct pcb *list;   // 信号量等待进程队列指针
    }
    1. 每个信号量建立一个等待进程队列
    2. 每个信号量相关一个整数值
      1. 正值表示资源可复用次数
      2. 0值表示无资源且无进程等待
      3. 负值表示等待队列中进程个数
    3. P、V操作原语
      procedure P(semaphore:s) {
          s -= 1;          // 信号量-1
          if (s<0) W(s);   // 若信号量小于0,则调用进程被置成等待信号量s的状态
      }
      procedure V(semaphore:s) {
          s += 1;          // 信号量+1
          if (s<=0) R(s);  // 若信号量小于等于0,则释放一个等待信号量s的进程
      }
    4. PV操作解决进程互斥问题框架
      semaphore s;
      s=1;
      begin
        	process Pi {
        		......
        		P(s);
        		临界区;
        		V(s);
        		......
        	}
        end;
    5. PV操作解决进程同步问题
      1. 进程同步:并发进程为完成共同任务基于某个条件来协调执行先后关系而产生的协作制约关系
      2. 一个进程的执行等待来自于其他进程的消息
      3. 解决的基本思路:
        1. 定义一个信号量:其数值代表可用消息数
        2. 等待消息进程:执行P,无消息则等待
        3. 发出消息进程:执行V,有等待进程则释放
      4. 1生产者1消费者1缓冲区问题
        1. 生产者和消费者共享缓冲区
        2. 缓冲区有空位时,生产者可放入产品,否则等待
        3. 缓冲区有产品时,消费者可取出产品,否则等待
        Int B;
        process producer
        begin
        	L1:
        	produce a product;
        	B = product;
        	goto L1;
        end;
        
        process consumer
        begin
        	L2:
        	product = B;
        	consume a product;
        	goto L2;
        end;
        // 正确执行顺序:P→(同步关系1:等待产品)C→(同步关系2:等待缓冲)P→C→P→C...
        
        // 信号量仅仅解决信号传递数据传送需要共享缓冲区
        1. 解决思路:
          1. 同步关系1:消费者一开始在等待产品到来,考虑设置一个信号量(等待产品);一开始无产品,初值为0
          2. 同步关系2:消费者则在等待缓冲区中有空位,也可设置一个信号量(等待缓冲区);一开始缓冲区有空位,初值为1
          // PV解决1生产者1消费者1缓冲区问题
          Int B;               // 共享缓冲区
          Semaphore sput;      // 可以使用的空缓冲区
          Semaphore sget;      // 缓冲区内可以使用的产品数
          sput = 1;            // 缓冲区内允许放入一件产品
          sget = 0;            // 缓冲区内没有产品
          process producer {
          	L1:
          	produce a product;
          	P(sput);
          	B = product;
          	V(sget);
          	goto L1;
          }
          
          process consumer {
          	L2:
          	P(sget);
          	product = B;
          	V(sput);
          	consume a product;
          	goto L2;
          }
          
          // PV解决1生产者1消费者N缓冲区问题
          Int B[k];            // 共享缓冲区队列
          Semaphore sput;      // 可以使用的空缓冲区
          Semaphore sget;      // 缓冲区内可以使用的产品数
          sput = k;            // 缓冲区内允许放入k件产品
          sget = 0;            // 缓冲区内没有产品
          Int putptr, getptr;  // 循环队列指针
          putptr = 0; getptr = 0;
          process producer_i {
          	L1:
          	produce a product;
          	P(sput);
          	B[putptr] = product;
          	putptr = (putptr+1) mod k;
          	V(sget);
          	goto L1;
          }
          
          process consumer_j {
          	L2:
          	P(sget);
          	product = B[getptr];
          	getptr = (getptr+1) mod k;
          	V(sput);
          	consume a product;
          	goto L2;
          }
          
          // PV解决N生产者N消费者N缓冲区问题
          Int B[k];            // 共享缓冲区队列
          Semaphore sput;      // 可以使用的空缓冲区
          Semaphore sget;      // 缓冲区内可以使用的产品数
          sput = k;            // 缓冲区内允许放入k件产品
          sget = 0;            // 缓冲区内没有产品
          Int putptr, getptr;  // 循环队列指针
          putptr = 0; getptr = 0;
          s1, s2:semaphore;    // 互斥使用putptr,getptr
          s1 = 1;s2 = 1;
          process producer_i {
          	L1:
          	produce a product;
          	P(sput);
          	P(s1);
          	B[putptr] = product;
          	putptr = (putptr+1) mod k;
          	V(s1);
          	V(sget);
          	goto L1;
          }
          
          process consumer_j {
          	L2:
          	P(sget);
          	P(s2);
          	product = B[getptr];
          	getptr = (getptr+1) mod k;
          	V(s2);
          	V(sput);
          	consume a product;
          	goto L2;
          }
        // 苹果橘子问题
        Int plate;
        Semaphore sp;        // 盘子里可以放几个水果
        Semaphore sg1;       // 盘子里有桔子
        Semaphore sg2;       // 盘子里有苹果
        sp = 1;              // 盘子里允许放入一个水果
        sg1 = 0;             // 盘子里没有桔子
        sg2 = 0;             // 盘子里没有苹果
        process father {
        		L1: 削一个苹果;
        		P(sp);
        		把苹果放入plate;
        		V(sg2);
        		goto L1;
        }
        
        process mother {
        		L2: 剥一个桔子;
        		P(sp);
        		把桔子放入plate;
        		V(sg1);
        		goto L2;
        }
        
        process son {
        		L3: P(sg1);
        		从plate中取桔子;
        		V(sp);
        		吃桔子;
        		goto L3;
        }
        
        process daughter {
        		L4: P(sg2);
        		从plate中取苹果;
        		V(sp);
        		吃苹果;
        		goto L4;
        }

管程概念的提出

  1. 管程试图抽象相关并发进程对共享变量访问,以提供一个友善的并发程序设计开发环境
  2. 管程是由若干公共变量及其说明和所有访问这些变量的过程所组成
  3. 管程把分散在各个进程中互斥地访问公共变量的那些临界区集中起来管理,管程的局部变量只能由该管程的过程存取
  4. 进程只能互斥地调用管程中的过程
  5. 管程的条件变量
    1. 条件变量(condition variables):当调用管程过程的进程无法运行时,用于阻塞进程的信号量
    2. 同步原语wait:当一个管程过程发现无法继续时(如发现没有可用资源时),它在某些条件变量上执行wait,这个动作引起调用进程阻塞
    3. 同步原语signal:用于释放在条件变量上阻塞的进程
  6. 管程过程执行中signal的处理问题
    1. 当使用signal释放一个等待进程时,可能出现两个进程同时停留在管程内。解决办法:
      1. 执行signal的进程等待,直到被释放进程退出管程或等待另一个条件
      2. 被释放进程等待,直到执行signal的进程退出管程或等待另一个条件
    2. 霍尔采用了第一种办法
    3. 汉森选了二者的折中,规定管程过程所执行的signal操作是过程体的最后一个操作

霍尔管程的实现方法

  1. 使用signal释放一个等待进程时,霍尔管程让执行signal的进程等待,直到被释放进程退出管程或等待另一个条件
  2. 霍尔管程基于PV操作原语实现
    1. Wait和signal可以是程序过程
    2. 可以用语言机制实现霍尔管程
    // 互斥调用霍尔管程的信号量
    TYPE interf=RECORD
    	mutex: semaphore;    // 调用管程过程前使用的互斥信号量
    	next: semaphore;     // 发出signal的进程挂起自己的信号量
    	next_count: integer; // 在next上等待的进程数
    END;
    // 互斥调用霍尔管程的框架
    P(IM.mutex);
    	<过程体>
    if IM.next_cout > 0 then V(IM.next);
    										else V(Im.mutex);
    // 霍尔管程的条件变量
    x_sem: semaphore;      // 与资源相关的信号量
    x_count: integer;      // 在x_sem上等待的进程数
    // 霍尔管程的wait进程
    procedure wait(var x_sem: semaphore, var x_count: integer, var IM: interf);
    begin
    	x_count += 1;
    	if IM.next_count > 0 then V(IM.next);
    											 else V(IM.mutex);
    	P(x_sem);
    	x_count -= 1;
    end;
    // 霍尔管程的signal过程
    procedure signal(var x_sem: semaphore, var x_count: integer, var IM: interf);
    begin
    	if x_count > 0 then begin
    		IM.next_count += 1;
    		V(x_sem);
    		P(IM.next);   // 进入等待调用管程的队列
    		IM.next_count -= 1;
    	end;
    end;
    
    // 哲学家问题
    TYPE dining_philosophers = MONITOR
    	var state: array[0..4] of (thinking, hungry, eating);
    			s: array[0..4] of semaphore;
    			s_count: array[0..4] of integer;
    	define pickup, putdown;
    	use wait, signal;
    procedure test(k: 0..4) P
    	if state[(k-1) mod 5] <> eating and state[k]=hungry
    		and state[(k+1) mod 5] <> eating then
    			{state[k] := eating; signal(s[k],s_count[k],IM);}
    }
    
    procedure pickup(i:0..4) {
    	state[i] := hungry;
    	test(i);
    	if state[i] <> eating
    	then wait(s[i], s_count[i], IM);
    }
    
    procedure putdown(i:0..4 {
    	state{i]:=thinking;
    	test((i-1) mod 5);
    	test((i+1) mod 5);
    }
    
    begin
    	for i:=0 to 4 do
    		state[i] := thinking;
    end;
    
    begin
    	process philosopher_i() {
    		L:thinking();
    		P(IM.mutex);
    		dining_philosophers.pickup(i);
    		if IM.next_count > 0 then V(IM.next); else V(IM.mutex);
    		eating();
    		P(IM.mutex);
    		dining_phi;osophers.putdown(i);
    		if IM.next_count > 0 then V(IM.next); else V(IM.mutex);
    		goto L;
    	}
    end;
    
    // 读者写者问题
    TYPE read-writer = MONITOR
    	var Rc, Wc: integer; R, W:semaphore; rc, wc: integer;
    	define start_read, end_read, start_writer, end_writer;
    	use wait, signal, check, release;
    begin rc:=0;wc:=0;Rc:=0;Wc:=0;R:=0;W:=0; end;
    
    procedure start_read;
    begin
    	if wc > 0 then wait(R,Rc,IM);
    	rc += 1;
    	signal(R, Rc, IM);          // 连续释放读者
    end;
    
    procedure end_read;
    begin
    	rc += 1;
    	if rc = 0 then signal(W, Wc,IM);
    end;
    
    procedure start_write;
    begin
    	wc += 1;
    	if rc > 0 or wc > 1 then wait(W, Wc, IM);
    end;
    
    procedure end_write;
    begin
    	wc += 1;
    	if wc > 0 then signal(W,Wc,IM);
    						else signal(R,Rc,IM);
    end;

进程通信的概念

  1. 交往进程通过信号量操作实现进程互斥和同步,这是一种低级通信方式
  2. 进程有时还需交换更多的信息(如把数据传送给另一个进程),可以引进高级通信方式——进程通信机制,实现进程间用信件来交换信息
  3. 进程通信扩充了并发进程的数据共享
  4. 进程直接通信
    1. 发送或接收信件的进程指出信件发给谁或从谁那里接收信件
      1. send(P,信件):把信件发送给给进程P
      2. receive(Q,信件);从进程Q接收信件
  5. 进程间接通信
    1. 发送或接收信件通过一个信箱来进行,该信箱有唯一标识符
    2. 多个进程共享一个信箱
      1. send(A,信件):把信件传送到信箱A
      2. receive(A,信件):从信箱A接收信件
    3. 间接通信的信箱
      1. 存放信件的存储区域,每个信箱可以分成信箱特征和信箱体两部分
        1. 信箱特征指出信箱容量、信件格式、指针等
        2. 信箱体用来存放信件,信箱体分成若干个区,每个区可容纳一封信
  6. 发送信件原语的处理流程
    1. 若指定的信箱未满,则把信件送入信箱中指针所指示的位置,释放等待该信箱中信件的等待者;否则,发送信件者被置成等待信箱的状态
  7. 接收信件原语的处理流程
    1. 若指定信箱中有信件,则取出一封信件,释放等待信箱的等待者;否则,接收信件者被置成等待信箱中信件的状态

基于流的进程通信

  1. 多个进程使用一个共享的消息缓冲区(管道、多路转接器、套接字)
  2. 一些进程往消息缓冲区中写入字符流(send/write)
  3. 一些进程从消息缓冲区中读出字符流(receive/read)
  4. 信息交换单位基于字符流,长度任意

远程过程调用RPC

  1. 采用客户/服务器计算模式
  2. 服务器进程提供一系列过程/服务,供客户进程调用
  3. 客户进程通过调用服务器进程提供的过程/服务获得服务
  4. 考虑到客户计算机和服务器计算机的硬件异构型,外部数据表示XDR被引入来转换每台计算机的特殊数据格式为标准数据格式

死锁的产生

定义

  1. 允许多个进程并发执行共享系统资源时,系统必须要提供同步机制和进程通信机制。然而,对这种机制使用不当的话,可能会出现进程永久被阻塞的现象。例如,两个进程分别等待对方占有的一个资源,于是两者都不能执行而处于永远等待,这种现象称为“死锁”

死锁:每一个进程都在等待被另一个进程所占有的、不能抢占的资源

  1. 存在n个进程P1,P2,...,Pn
  2. 进程Pi因为申请不到资源Ri而处于等待状态
  3. 而Ri又被Pi+1占有,Rn被P1占有
  4. 显然,这n个进程的等待状态永远不能结束,这n个进程就处于死锁状态

产生

  1. 竞争资源产生死锁
  2. PV操作使用不当产生死锁
  3. 同类资源分配不当引起死锁
    1. 若系统中有m个资源被n个进程共享,当每个进程都要求K个资源,而$m< n\times(K-1)+1$时,如果分配不当就可能引起死锁
  4. 对临时性资源使用不加限制引起死锁
    1. 在进程通信时使用的信件可以看作是一种临时性资源,如果对信件的发送和接收不加限制的话,则可能引起死锁
  5. 产生的必要条件
    1. 互斥条件:进程应互斥使用资源,任一时刻一个资源仅为一个进程独占
    2. 占有和等待条件:一个进程请求资源得不到满足而等待时,不释放已占有的资源
    3. 不剥夺条件:任一进程不能从另一进程那里抢夺资源
    4. 循环等待条件:存在一个循环等待链,每一个进程分别等待它前一个进程所持有的资源

解决办法

产生死锁因素:

  1. 系统拥有的资源数量
  2. 资源分配的策略
  3. 进程对资源的使用要求以及并发进程的推进顺序

三个方面解决死锁问题

  1. 死锁防止
    1. 破坏四个必要条件之一
    2. 破坏第一个条件,把独占型资源改造成共享性资源,使资源可同时访问而不是互斥使用。但对许多资源无法做到
    3. 破坏第三个条件,采用剥夺式调度方法,但剥夺式调度方法目前只适用于对主存资源和处理器资源的分配,而不适用于所有资源
    4. 破坏第二个条件,采用静态分配
      1. 静态分配:一个进程必须在执行前就申请它所要的全部资源,并且直到它索要的资源都得到满足之后才开始执行
      2. 所有并发执行的进程要求的资源总和不超过系统拥有的资源数
      3. 进程在执行过程中不再申请资源,所以就破坏了第二个条件
    5. 破坏第四个条件,层次分配
      1. 层次分配将资源分成多个层次
      2. 一个进程得到某一层的一个资源后,它只能再申请在较高层的资源
      3. 当一个进程要释放某层的一个资源时,必须先释放所占用的较高层的资源
      4. 当一个进程获得了某一层的一个资源后,它想再申请该层中的另一个资源,那么必须先释放该层中的已占资源
  2. 死锁避免
    1. 当不能防止死锁的产生时,如果能掌握并发进程中与每个进程有关的资源申请情况,仍然可以避免死锁的发生
    2. 只需要在位申请者分配资源前先测试系统状态,若把资源分配给申请者会产生死锁的话,则拒绝分配,否则接收申请,为它分配资源
    3. 银行家算法:借钱给有偿还能力的客户
      1. 系统首先检查申请者对资源的最大需求量,如果现存的资源可以满足它的最大需求量时,就满足当前的申请
      2. 换言之,仅仅在申请者可能无条件地归还它所申请的全部资源时,才分配资源给它
      3. 例:
        1. 假设系统有三个进程P、Q、R,系统只有一类资源共10个。分配情况:
          1. P:占有4个,还需申请4个
          2. Q:占有2个,还需申请2个
          3. R:占有2个,还需申请7个
        2. 现在总共占用了8个资源,只能再分配个两个资源。所以先分配给Q,等Q完成之后,就有4个资源,继续分配给P,等P完成之后,就有8个资源,最后给R
  3. 死锁检测和恢复
    1. 对资源的分配不加限制,但系统定时运行一个“死锁检测”程序,判定系统内是否已出现死锁,若检测到死锁则设法加以解除
    2. 检测:
      1. 可设置两张表格来记录进程使用资源的情况

        1. 等待资源表记录每个被阻塞进程等待的资源
        2. 占用资源表记录每个进程占有的资源
      2. 进程申请资源时,先查该资源是否为其他进程所占用;若资源空闲,则把该资源分配给申请者且登入占用表;否则,则登入进程等待资源表

      3. 死锁检测程序定时检测这两张表,若有进程Pi等待资源rk,且rk被进程Pj占用,则说明Pi和Pj具有“等待占用关系”,记为W(Pi,Pj)

      4. 死锁检测程序反复检测这两张表,可以列出所有的“等待占用关系”

      5. 如果出现W(Pi,Pj),W(Pj,Pk),...,W(Pm,Pn),W(Pn,Pi)时,显然,系统中存在一组循环等待资源的进程:Pi,Pj,Pk,...,Pm,Pn,也就是说出现了死锁

      6. 两张表可以用一个矩阵A表示

        进程/$A[b_{ij}]$/进程 P1 P2 ... Pn
        P1 b11 b12 ... b1n
        P2 b21 b22 ... b2n
        ... ... ... ... ...
        Pn bn1 bn2 ... bnn

        其中b_ij=1表示当Pi等待被Pj占用的资源时,=0表示Pi与Pj不存在等待占用关系时

      7. 算法

          // Warshall的传递闭包算法检测是否有死锁发生
          // 即对矩阵A构造传递闭包A*[bij]
          for k:= 1 to n do
          	for i: 1 to n do
          		for j:= 1 to n do
          			bij:= bij∪(bijbkj)

死锁检测后的解决办法

  1. 采用重新启动进程执行的办法,恢复工作应包含重启动一个或全部进程,以及从哪一点开始重启动
  2. 全部卷入死锁从头开始启动,但这样的代价相当大
  3. 进程执行过程中定时设置校验点,从校验点开始重执行
  4. 中止一个卷入死锁的进程,以后重执行