操作系统进程互斥、同步、死锁

文章目录

      • 同步、互斥、临界区*
      • 进程互斥的实现方式
      • 信号量机制*
      • 经典同步问题*
      • 管程*
      • 死锁*

带*是重点


同步、互斥、临界区*

临界区: 一个时间段内只允许一个进程使用的资源被称为临界资源,而对临界资源进行访问的那段代码被称为临界区。

同步: 直接制约关系,协调进程的工作次序.

互斥: 间接制约关系,多个进程在同一时刻只有一个进程能进入临界区。

进程同步是为了解决进程特征中的异步性。


进程互斥的实现方式

需要遵循的规则

  • 空闲让进:临界区空闲时,应允许一个进程访问
  • 忙则等待:临界区正在被访问时,其他要访问的进程需要等待
  • 有限等待:要在有限时间内进入临界区,保证不会饥饿
  • 让权等待:进不了临界区的进程,要释放处理机,防止忙等

1. 软件实现

  • 单标志法:两个进程在访问完临界区后会吧使用临界区的权限转交给另一个进程。
    违背:空闲让进
  • 双标志先检查法:设置一个布尔型数组 flag[],标记各进程想进入临界区的意愿,每个进程在进入前都需要检查有没有别的进程要进入。
    违背:忙则等待
  • 双标志后检查法:即将检查和上锁调换位置。
    违背:空闲让进、有限等待
  • Peterson 法:结合单标志法和双标志法,单标志用于表示优先让那个进程进入临界区(双方竞争则主动让出)。
    违背:让权等待

2. 硬件实现

  • 中断屏蔽法:使用开关中断原语,简单高效,不适用于多处理机。
    操作系统进程互斥、同步、死锁_第1张图片
  • TestAndSet 指令:简称 TSL 指令,可用于多处理机,但不满足让权等待。
    操作系统进程互斥、同步、死锁_第2张图片
  • Swap 指令:功能和逻辑类似于 TSL 指令。
    操作系统进程互斥、同步、死锁_第3张图片

信号量机制*

前面互斥实现的方式都无法实现让权等待原则,而信号量机制则符合所有原则。

概念
信号量是一个变量(整数/记录型),和单标志法一样用于表示系统中某种资源的数量,可以用操作系统提供的一对原语来对信号量进行操作,从而实现进程的互斥同步。

原语

  • wait(S):简称为 P 操作,S 为信号量
  • signal(S):简称为 V 操作,S 为信号量

互斥实现
1.整形

		int S = 1;//一台打印机
		void wait(int S){
			while(S<=0);
			S=S-1;
		}
		void signal(int S){
			S=S+1;
		}
		//进程
		...
		wait(S);//P操作
		//使用打印机
		signal(S);//V操作

2.记录型

			typedef struct {
				int value;//剩余资源数
				Struct process *L;//等待队列
			} semaphore;
			void wait(semaphore S){
				S.value--;
				if(S.value<0)
					block(S.L);//block原语阻塞队列,由于进入阻塞态,所以遵循了 让权等待
			}
			void signal(semaphore S){
				S.value++;
				if(S.value<=0){
					wakup(S,L);//唤醒等待队列中的第一个进程
				}
			}

3.总结

		semaphore mutex = 1;//初始化信号量(简写形式)
		semaphore mutex2 = 2;//不同的临界资源需要设置不同的互斥信号量
		void P1(){
			...
			P(mutex);
			//临界区...
			V(mutex);
			...
		}
		void P2(){
			...
			P(mutex);
			//临界区...
			V(mutex);
			...
		}

同步实现
同步即要让各并发进程按要求有序地进行。

		semaphore S = 0;
		void P1(){
			...
			...
			V(mutex);//S++;
			...
		}
		void P2(){
			P(mutex);//S--,由于初始 S=0,所以必须 P1 先执行完,调度到P2时才会执行
			...
			...
		}

经典同步问题*

生产者-消费者模型
问题:使用一个缓冲区来保存物品,只有缓冲区没有满,生产者才可以放入物品;只有缓冲区不为空,消费者才可以拿走物品。

		semaphore mutex = 1;//互斥量
		semaphore empty = n;//缓冲区
		semaphore full = 0;//产品
		void producer(){
			while(true){
				//生产一个产品
				P(empty);//消耗一个缓冲区空间
				P(mutex);//互斥锁
				//放入产品
				V(mutex);
				V(full);//增加一个产品
			}
		}
		void consumer(){
			while(true){
				P(full);
				P(mutex);
				//拿走产品
				V(mutex);
				V(empty);
			}
		}

注:互斥的P操作一定要在同步P操作之后,不然会死锁;V操作的顺序则可以任意调换。

多生产者-多消费者
问题:使用一个缓冲区来保存物品,多个生产者生产不同的产品,多个消费者使用不同的产品。
互斥:
生产者和消费者之间都是互斥的
同步:
消费者释放一个缓冲区->生产者消耗一个缓冲区
生产者生产一个苹果->对应生产者拿走一个苹果
生产者生产一个橘子->对应生产者拿走一个橘子

		semaphore mutex = 1;
		semaphore apple = 0;
		semaphore orrange = 0;
		semaphore plate = 1;
		void father(){
			//生产一个苹果
			P(plate);
			P(mutex);
			//将苹果放入盘子
			v(mutex);
			V(apple);
		}
		void mother(){
			//生产一个橘子
			P(plate);
			P(mutex);
			//将橘子放入盘子
			v(mutex);
			V(orrange);
		}
		void son(){
			P(apple);
			P(mutex);
			//取走苹果
			V(mutex);
			V(plate);
			//吃掉苹果
		}
		void dau(){
			P(orrange);
			P(mutex);
			//取走橘子
			V(mutex);
			V(plate);
			//吃掉橘子
		}

注:如果缓冲区大小为1,那么可能不需要设置互斥量就可以实现互斥访问缓冲区的功能。

读者-写者(不是所有互斥)
问题:允许多个进程同时对数据进行读操作,但是不允许读和写以及写和写操作同时发生。
互斥:
写进程-写进程、写进程-读进程
读进程之间是不互斥的,用一个计数器来实现。

		int count = 0;
		semaphore countMutex = 1;
		semaphore rdMutex = 1;
		void writer(){
			P(rdMutex);
			//写入
			V(rdMutex);
		}
		void reader(){
			P(countMutex);
			if(count==0)
				P(rdMutex);
			count++;
			V(countMutex);
			//读取
			P(countMutex);
			count--if(count==0)
				P(rdMutex);
			V(countMutex);
		}

注:这是一种读优先策略,如果一直有读者加入,则最终会造成写饥饿。

哲学家进餐(多临界资源)
操作系统进程互斥、同步、死锁_第4张图片
错误解法(死锁):

		semaphore chopstick[5]={1,1,1,1,1};
		void P(int i){//第i个哲学家
			while(true){
				think();
				P(chopstick[i]);//拿起左筷子
				P(chopstick[(i+1)%5]);//拿起右筷
				eat();
				V(chopstick[i]);
				V(chopstick[(i+1)%5]);
			}
		}

解决方案:
1.最多允许四个哲学家同时进餐,这样可以至少保证有一个哲学家拿到两只筷子。
实现:

		semaphore nums = 4;

2.奇数号哲学家先拿左边筷子,然后再拿右边筷子,而偶数号哲学家相反。
实现:增加奇偶判断,调整筷子顺序

3.仅当一个哲学家左右两只筷子都可用时,才允许拿起筷子。(较好解法
实现:

		semaphore chopstick[5]={1,1,1,1,1};
		semaphore mutex = 1;
		void P(int i){//第i个哲学家
			while(true){
				think();
				P(mutex);
				P(chopstick[i]);
				P(chopstick[(i+1)%5]);
				V(mutex);
				eat();
				V(chopstick[i]);
				V(chopstick[(i+1)%5]);
			}
		}

管程*

管程是为了解决信号量在临界区的 PV 操作上的配对的麻烦,把配对的 PV 操作集中在一起,生成的一种并发编程方法。其中使用了条件变量这种同步机制。

特征

  • 各外部进程/线程只能通过管程提供的特定“入口”才能访问共享数据
  • 每次仅允许一个进程在管程内执行某个内部过程

作用
解决信号量机制编程麻烦、易出错的问题。

互斥: 由编译器实现。
同步: 管程引入了 条件变量 以及相关的操作:wait() 和 signal() 来实现同步操作。对条件变量执行 wait() 操作会导致调用进程阻塞,把管程让出来给另一个进程持有。signal() 操作用于唤醒被阻塞的进程。

用管程解决生产者消费者问题
1.定义管程

		monitor PridycerCOnsumer
			condition full,empty;
			int count = 0;
			void insert(Item item){
				if(count == N)
					wait(full);
				count++;
				insert_item(item);
				if(count == 1)
					signal(empty);
			}
			Item remove(){
				if(count==0)
					wait(empty);
				count--;
				if(count == N-1)
					signal(full);
				
			}

2.生产者和消费者

		producer(){
			while(true){
				item = 生产一个产品;
				ProducerConsumer.insert(item);
			}
		}
		consumer(){
			while(true){
				item = ProducerConsumer.remove();
				消费一个产品
			}
		}

Java 中的管程
在Java虚拟机中,每个对象和类在逻辑上与管程(对象头在有锁时关联monitor)相关联。而为了实现管程的互斥能力,一个锁关联每个对象和类,Java 中的 Lock 互斥锁是一个二进制信号量,相当于操作系统中的信号量。同样在编码时会不方便

于是 JVM 自动为我们实现了 synchronized ,一旦代码被嵌入synchronized关键字,它就是一个管程区域。该锁在后台通过JVM自动实现。

我们知道每个对象/类都关联一个管程。我认为更好的说法应该是每个对象都有一个管程,因为每个对象可以有它自己的临界区,并能够监控线程顺序。

为了使不同的线程协作,JAVA为提供了wait()和notify()来挂起线程和唤醒另外一个等待的线程

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

		static class MyMonitor {
			private Item buffer[] = new Item[N];
			private int count = 0, lo = 0, hi = 0;// 计数器和索引
	 
			public synchronized void insert(Item item) {
				if (count == N)
					go_to_sleep();
				buffer[hi] = itme;
				hi = (hi + 1) % N;
				count++;
				if (count == 1)
					notify();
			}
	 
			public synchronized Item remove() {
				Itme val;
				if (count == 0)
					go_to_sleep();
				val = buffer[lo];// 从缓冲区中取出一个数据项
				lo = (lo + 1) % N;// 设置待取数据项的槽
				count--;
				if (count == N - 1)
					notify();
				return val;
			}
	 
			private void go_to_sleep() {
				try {wait();} catch (InterruptedException exc) {};
			}
		}

死锁*

概念
如果一个进程集合里面的每个进程都在等待只能由这个集合中的其他一个进程(包括他自身)才能引发的事件,这种情况就是死锁。

造成死锁的原因就是多个线程或进程对同一个资源的争抢或相互依赖。
资源可分为两种:可剥夺资源和不可剥夺资源。
一般来说对于由可剥夺资源引起的死锁可以由系统的重新分配资源来解决,所以一般说的死锁都是由于不可剥夺资源所引起的。

死锁的必要条件

  • 互斥:只有对必须互斥使用的资源的争抢才会导致死锁。
  • 不可占有:进程所获得的资源不能被其他进程强行夺走,只能主动释放。
  • 占有和等待:已经得到某个资源的进程可以再请求新的资源。
  • 循环等待:有两个或两个以上的进程,在相互等待下一进程所占有的资源,形成一条环路。

死锁的处理方法

  • 预防死锁: 破坏四个必要条件中的一个或多个。
  • 避免死锁: 防止系统进入不安全状态(银家算法)。
  • 死锁的检测和解除: 允许死锁的发生,操作系统会检测并采取措施。

预防死锁

  1. 破坏互斥条件
    将临界资源改造成可共享使用的资源,如假脱机打印机技术允许若干个进程同时输出,唯一真正请求物理打印机的进程是打印机守护进程。
    缺点:可行性不高,很多时候无法破坏互斥条件。
  2. 破坏不可占有条件
    允许进程强行从占有者那里夺取某些资源,例如在申请不到足够资源时立即释放拥有的所有资源。
    缺点:实现复杂,还可能会降低系统性能。
    3.破坏占有和等待条件
    运行前分配好所有需要的资源,之后一直保持。
    缺点:资源利用率低,可能会导致饥饿。
    4.破坏循环等待条件
    实行资源有序分配策略,即给资源编号,必须按编号从小到大的顺序申请资源。
    缺点:不方便增加新设备,导致资源浪费。

避免死锁
安全序列和安全状态:

  • 安全序列是指系统按照这种序列分配资源,则每个进程都能顺利完成,可以有多个。
  • 安全状态就是只要系统能找出一个安全序列,那么系统就是安全状态;如果找不出任何一个则进入不安全状态。
  • 系统处于安全状态就一定不会发生死锁,如果进入不安全状态就可能会发生死锁。

所以我们可以通过一种算法在资源分配之前预先判断这次分配是否会导致系统进入不安全状态,这种算法就是银行家算法

  • 用Max矩阵表示每个进程的每种资源的最大需求,Has矩阵表示每个进程所以拥有的每种资源的数量,Need矩阵表示每个进程最多还需要多少资源,Available数组表示每种资源的剩余数目,request数组表示某进程本次申请的资源数。
  • request小于Need且小于 Available,系统才会将资源分配给此进程,并修改相应的数据。
  • 操作系统执行安全性算法,即检查剩余资源是否能满足某个进程的最大需求,检查是否处于安全状态,是则继续上一步,如果不是则会恢复修改数据并让进程阻塞。

死锁的检测和解除
检测系统是否已经发生了死锁:

  • 定义一个数据结构:两种结点,进程结点和资源结点;两种边,进程结点->资源结点,资源结点->进程结点,如下:
    操作系统进程互斥、同步、死锁_第5张图片
  • 提供一种算法,如果系统中剩余资源数足够满足某个进程的需求,那么这个进程的分配和请求边就可以消除。最终如果能消除所有边即图可完全简化就一定不会发生死锁,而最终还连着边的进程就是处于死锁状态的进程。

解除死锁的方法:

  • 资源剥夺法:挂起某些死锁进程,并抢占其资源,将这些资源分配给其他死锁进程。
  • 终止进程法:强制撤销部分、甚至全部死锁进程。
  • 进程回退法:让一个或多个死锁进程会退到足以避免死锁的地步(设置还原点)。

你可能感兴趣的:(操作系统)