目录
信号量机制
前言:
- 在双标志先检查法中,进入区的“检查”、“上锁”操作无法一气呵成,从而导致了两个进程可能同时进入临界区的情况
- 进程互斥的所有实现方式都不可以实现让权等待
- 1965年,荷兰学者DiJkstra提出了一种卓有成效的实现进程互斥、同步的方法——信号量机制
信号量含义
信号量:信号量实际上就是一个变量(可以是一个整数,也可以是更复杂的记录型变量)可以用一个信号量来表示系统中某种资源的数量,比如:系统中只有一台打印机,就可以设置一个初值为1的信号量
注意:
- 用户进程可以通过使用操作系统提供的一对原语来对信号量进行操作,从而方便的实现了进程的互斥,进程的同步
- 一对原语:wait(S)原语和signal(S)原语,可以把原语理解为我们自己写的函数,函数名分别为wait和signal,括号里的信号量S实际上就是函数调用时传入的一个参数(我们可以用系统提供的一对原语来对信号量进行操作)
- wait、signal原语简称为P、V操作。因此常常把wait(S)和signal(S)两个操作写为P(S)、V(S);对信号量S进行一次P操作意味着进程请求一个单位的该类资源,对信号量S进行一次V操作意味着进程释放一个单位的该类资源
整形信号量
含义:用一个整形信号量用来表示系统中某种资源的数量
eg:一台计算机有一台打印机
理解:一个线程执行时先执行wait(S) ,此时若另一个线程进来则由于S<=0为真,则会一直循环;直到刚才的进程执行完singal(S)释放信号量
注意:若一个进程(P1)暂时进不了临界区,系统资源不够的话会一直占用处理机,一直循环检查从而导致忙等,不满足让权等待
记录型信号量
前言:为了解决整形信号量的忙等问题
含义:用记录型数据结构表示的信号量
理解:一个线程执行时先执行wait(S) ,此时若另一个线程进来,若剩余资源数<0则会使进程从运行态进入阻塞态(阻塞前占一个信号量),并将其挂到信号量S的等待队列L中;直到刚才的进程执行完singal(S)释放信号量才会被唤醒进程。
信号量实现进程互斥
具体过程
- 分析并发进程的关键活动,划定临界区
- 设置互斥信号量mutex,初值为1(因为临界区内只能有一个资源进行访问)
- 在临界区之前执行P(mutex)
- 在临界区之后执行V(mutex)
注意:
- 对于不同的临界资源需要设置不同的信号量;
- P、V操作必须同时出现;缺少P就不能保证临界资源的互斥访问,缺少V就会导致资源永不被释放,等待进程用不被唤醒
信号量机制实现进程同步
进程同步:要让各并发进程按要求有序的推进
实现过程
- 分析什么地方需要实现“同步关系”,即必须保证一前一后地两个操作
- 设置同步信号量S,初值为0
- 在前操作之后执行V(S)
- 在后操作之前执行P(S)
理解:要想让代码2在代码4前执行,因为初始信号量为0,首先执行V操作将信号量改为1,再执行下一段代码前执行P操作将信号量改为0(因为S=0,所以不可能先执行P操作;若P先执行则会直接阻塞,一直等到V操作执行后被唤醒)
信号量实现前驱关系
实现过程
- 要为每一对前驱关系各设置一个同步变量
- 在“前操作”之后相应的同步变量执行V操作
- 在“后操作”之前相应的同步变量执行P操作
理解:就相当于多层的同步关系。
生产者消费者问题
具体操作
前言:系统中有一组生产者进程和一组消费者进程,生产者进程每次生产一个产品放入缓冲区,消费者进程每次从缓冲区取出一个产品并使用(注意:这里的产品理解为某种数据)生产者、消费者共享使用一个初始为空、大小为n的缓冲区
注意:
- 只有缓冲区没满时,生产者才能把产品放入缓冲区,否则必须等待(同步关系)
- 缓冲区不空时,消费者才能从中取出商品,否则必须等待(同步关系)
- 缓冲区是临界资源,各进程必须互斥的访问(互斥关系)
改变empty、mutexPV操作顺序
理解:若此时缓冲区已经放满产品,empty=0,full=n;则生产者生产产品,1锁死,后发现2缓冲区没有了进不了缓冲区就会一直阻塞;来到消费者,因为P已经锁死还未释放,直接进不去(生产者需要V(empty),消费者需要P(mutex)——互相等待对方资源,死锁)
结论:实现互斥的P操作一定要在实现同步的P操作之后,这样才不会发生死锁
多生产者-多消费者问题
例子:桌子上有一个盘儿,每次只能放一个果儿;爸爸放苹果给女儿吃;妈妈放橘子给儿子吃,只有盘儿为空,才可以放水果;仅当盘中有自己需要的水果儿子女儿才消费
分析
互斥关系(mutex=1):对缓冲区的访问要互斥进行
同步关系(一前一后)
- 父亲放苹果,女儿取
- 妈妈放橘子,儿子取
- 只有盘为空,父亲母亲才能放水果
执行
注意:这里的P(mutex)、V(mutex)可以省略(因为盘子一共才一个,父亲消耗了,母亲自然没法消耗[省略P(mutex)操作];而父亲和女儿,母亲和儿子又是同步关系[省略V(mutex)操作])
吸烟者问题
假设有一个系统有三个抽烟者进程和一个供应者进程,每个抽烟者不停的卷烟并将他抽掉,但卷起并抽掉一支烟需要三种材料:烟草、纸、胶水。三个抽烟者中,第一个拥有烟草、第二个拥有纸、第三个拥有胶水。供应者进程无限的提供三种材料,供应者每次将两种材料放在桌子上,拥有剩下那种材料的抽烟者卷一根烟并抽掉它,并给供应者进程一个信号告诉它完成了,供应者就会放另外两个材料到桌子上,这个过程一直重复(供应者目的:吸烟者轮流吸烟)
三种组合
- 组合一:纸+胶水
- 组合二:烟草+胶水
- 组合三:烟草+纸
同步关系
- 桌上有组合1:第一个抽烟者取走
- 桌上有组合2:第二个吸烟者取走
- 桌上有组合3:第三个吸烟者取走
- 发出完成信号:供应者将下一个组合放到桌上
读写者问题
前言:有读者和写者两组并发进程,共享一个文件,当两个或两个以上的读进程同时访问共享数据时不会产生副作用,但若某个写进程和其他进程同时访问共享数据时可能导致数据不一致的情况
因此要求
- 允许多个读者可以同时对文件执行读操作
- 只允许一个写者往文件中写信息
- 任一写者在完成写操作之前不允许其他读者或写者工作
- 写者执行写操作前,应让已有的读者和写者全部退出
潜在问题:只要读进程还在读,写进程就得一直阻塞等待,可能“饿死” 。因此,这种算法中读进程是优先的
理解:当一个读者进程读文件的时候有一个新的写者进程到达,由于第一个读者进程已经执行了V(w),所以写者进程执行P(w)的操作时不会被阻塞;在执行P(rw)操作时由于第一个读进程已经对P(rw)执行了P操作,所以写者进程会被阻塞在该位置;此时若有第二个读者进程到达的话,由于之前写者进程P(w),没有执行V(w),所以读者进程会被阻塞在P(w)
核心思想:设置一个计数器count用来记录当前正在访问共享文件的读进程数;可以用count值来判断当前进入的进程是否是第一个/最后一个读进程,而做出不同处理
哲学家进餐问题
一张圆桌上坐着五位哲学家,每两位哲学家之间的桌子上摆着一根筷子,桌子中间是一碗米饭。哲学家们倾注毕生精力用于思考和进餐,哲学家思考时并不影响其他人。只有当哲学家饥饿时,才试图拿起左右两根筷子(一根根的拿)若筷子已经在他人手上,则需要等待。饥饿的哲学家只有拿起两根筷子才可以开始进餐,当进餐完毕后,放下筷子继续思考
前言:信号量设置,定义互斥信号量数组chopstick[5] ={1,1,1,1,1}用于实现对5个筷子的互斥访问,并对哲学家按0-4编号,哲学家i左边的筷子编号为i,右边的筷子编号为(i+1)%5
关键问题:解决死锁,添加互斥信号量mutex
管程
为什么要引入管程
信号量机制存在的问题:编写程序困难,易出错
管程的定义和基本特征
管程是一种特殊的软件模块,由这些部分组成
- 局部于管程的共享数据结构
- 对该数据结构操作的一组过程
- 对局部于管程的共享数据设置初值的语句
- 管程要有一个名字
管程的基本特征
- 局部于管程的数据只能被局部于管程的过程所访问
- 一个进程只有通过调用管程内的过程才能进入管程访问共享数据
- 每次仅允许一个进程在管程内执行某个内部过程
理解:若两个消费者先执行,生产者进程后执行;那么第一个消费者进程在执行的时候会调用管程的remove方法,首先判断此时缓冲区里是否有可用的产品,没有则会等待在empty这个条件变量相关的队列中;同样的,第二个消费者进程开始执行remove的时候也会发现count的值为0,同样等待empty的队列队尾中;之后有一个生产者进程开始执行insert函数,其会将自己的产品放入缓冲区中,并且会检查自己放入的产品是不是缓冲区的第一个产品,若是第一个产品就意味着可能有别的消费者进程正在等待我的产品所以就会唤醒empty队列中的进程,由于第一个进程被唤醒之后就开始执行count--,然后检查自己取走产品前缓冲区是否满了,若缓冲区满了就意味着可能有生产者进程需要被唤醒signal(full),然后返回产品指针取出产品;
注意:
- 各进程必须互斥访问管程的特性是由编译器实现的
- 可在管程中设置条件变量及等待/唤醒操作以解决同步问题
死锁
含义:两个线程互相抱着对方需要的资源,互相等待对方的执行结果,形成僵持,并且两个线程均不会释放各自的资源
饥饿:由于长时间得不到想要的资源,某进程无法向前推进的情况
死锁的必要条件
- 互斥条件:只有对必须互斥使用的资源的争抢才会导致死锁
- 请求和保持条件:一个进程因请求资源而阻塞,对已获得的资源保持不放
- 不可剥夺条件:进程已获得资源,在未使用完之前不可被剥夺
- 环路等待条件:若干进程之间形成了一种头尾相接的环路等待资源关系
注意:
- 发生死锁时一定有循环等待,但是发生循环等待时未必死锁
- 对不可剥夺资源的不合理分配就会导致死锁
具体案例
public class DeadLock {
public static void main(String[] args) {
Makeup g1 = new Makeup(0, "灰姑娘");
Makeup g2 = new Makeup(1, "白雪公主");
g1.start();
g2.start();
}
}
class Lipstick{}
class Mirror{}
class Makeup extends Thread{
//需要的资源只有一份
static Lipstick lipstick=new Lipstick();
static Mirror mirror=new Mirror();
int choice;
String girlName;
Makeup(int choice,String girlName){
this.choice=choice;
this.girlName=girlName;
}
@Override
public void run() {
//化妆
try {
makeup();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void makeup() throws InterruptedException {
if (choice==0){
synchronized (lipstick){
System.out.println(this.girlName+"获得口红的锁");
Thread.sleep(1000);
synchronized (mirror){
System.out.println(this.girlName+"获得镜子的锁");
}
}
}else {
synchronized (mirror){
System.out.println(this.girlName+"获得镜子的锁");
synchronized (lipstick){
System.out.println(this.girlName+"获得口红的锁");
}
}
}
}
}
死锁的处理策略
- 预防死锁,破坏死锁产生的四个必要条件中的一个或几个
- 避免死锁,用某种方法防止系统进入不安全的状态,从而避免死锁
- 死锁的检测和解除,允许死锁的发生,不过操作系统会负责检测出死锁的发生,然后采取某种措施解除死锁
预防死锁
破坏互斥条件
互斥条件:只有对必须互斥使用的资源的争抢才会导致死锁
若把只能互斥使用的资源改造为允许共享使用,则系统不会进入死锁状态。
比如SPOOLing技术,操作系统可以采用SPOOLing技术把独占设备在逻辑上改造成共享设备,如下例子
过程:各个进程对打印机发出的请求会首先被输出进程接收,当他们的请求都被接收后,这些进程就可以顺利的往下执行别的事情了;之后输出进程就会根据各个进程的请求,依次放到打印机上打印输出
缺点:并不是所有的资源都可以改造成共享使用的资源。并且为了系统安全,很多地方还必须保护这种互斥性,因此,很多时候都无法破坏互斥条件
破坏不可剥夺条件
不可剥夺条件:进程所获得的资源在未使用完之前,不能由其他进程强行夺走,只能主动释放
破坏不可剥夺条件方案
- 方案1:当某个进程请求新的资源得不到满足时,他(该进程)必须立即释放保持的所有资源,待以后需要时再申请。也就是说,即使某些资源尚未使用完,也需要主动释放,从而破坏了不可剥夺条件
- 方案2:当某个进程需要的资源被其他进程占有的时候,可以由操作系统协助,将想要的资源强行剥夺。这种方式一般需要考虑各个进程的优先级(比如剥夺调度方式,就是将处理机资源强行剥夺给优先级更高的进程使用)
缺点
- 实现起来比较复杂
- 释放以获得的资源可能造成前一阶段的工作失效,因此这种方法一般只适用于易保存和恢复状态的资源,如CPU
- 反复的申请和释放资源会增加系统开销,减低系统吞吐量
- 若采用方案一,意味着只要暂时得不到某个资源,之前获得的那些资源都需要放弃,以后再重新申请。若一直发生这样的情况,就会导致进程饥饿
破坏请求和保持条件
请求和保持条件:一个进程因请求资源而阻塞,对已获得的资源保持不放
可以采用静态分配法,即进程在运行前一次申请他所需要的全部资源,在他资源未满足前,不让他投入运行。一旦投入运行后,这些资源就一直归他所有,该进程就不会请求别的任何资源了
缺点:有些资源可能只需要很短的时间,因此若进程整个期间都一直保持着所有的资源,(有些资源使用的频率并不高)就会造成严重的资源浪费,资源利用率极低。另外,该策略也可能导致某些进程饥饿
破坏循环等待条件
循环等待条件:存在一种进程资源的循环等待链,链中的每一个进程已获得的资源同时被下一个进程所请求
可以采用顺序资源分配法。首先给系统中的资源编号,规定进程必须按照编号递增的顺序请求资源,同类资源(编号相同的资源)一次申请完
原理:一个进程只有已经占有小编号的资源的同时才有资格申请大编号的资源。按此规则,已持有大编号的资源进程不可能逆向的回来申请小编号的资源,从而不会产生循环等待
避免死锁
安全序列:若系统按照这种序列分配资源,则每个进程都能完成,只要能找出一个安全序列,系统就是安全状态。当然,安全序列可能有多个
银行家算法的核心思想:在资源分配之前预先判断这次分配是否会导致系统进入不安全状态,以此决定是否答应资源的分配请求(若会进入不安全状态,就暂时不答应这次请求,让该进程先阻塞等待)
例子:系统中有5个进程P0-P4,3种资源R0-R2,初始数量为(10,5,7)则某一时刻的情况可表示
注意:
- 此时总共分配了(7,2,5)还剩余(3,3,2)
- 依次检查剩余可用资源(3,3,2)是否满足各进程的需求
- 将剩余可用资源分配给能完成任务的进程,当该进程执行结束后会释放资源,进而可以使系统处于较为安全状态(尽量找需要借的少能完成任务后回馈资源多的进程)
- 安全序列分配:{P1,P3,P0,P2,P4}(只是其中一个,还可能有很多)
银行家算法的步骤
- 检查此次申请是否超过了之前声明的最大需求数
- 检查此时系统剩余的可用资源是否还能满足这次请求
- 试着分配,更改数据结构
- 用安全性算法检查此次分配是否会导致系统进入不安全状态
安全性算法步骤
- 检查当前剩余可用资源是否满足某个进程的最大需求,若可以,就把该进程加入到安全序列并把该进程持有的资源全部回收
- 不断重复该过程,看最终能否让所有的进程都加入到安全序列
死锁的检测和解除
死锁检测和解除算法
- 死锁检测算法:用于检测系统状态,以确定系统中是否发生死锁
- 死锁解除算法:当认定系统中已经发生死锁,利用该算法可将系统从死锁状态中解脱出来
为了对系统是否已经发生了死锁进行检测
- 用某种数据结构来保存资源的请求和分配信息
- 提供一种算法,利用上述信息来检测系统是否已经进入了死锁状态
死锁的检测
进程请求完需要的资源后会释放所有资源并且不会申请资源,若根据这种方法最终能消除所有的边,就称这个图是可完全简化的。此时一定没有发生死锁(相当于找到一个安全序列);若最终不能消除所有的边,那么此时就是发生死锁(最终还连着边的进程处于死锁)
解除死锁的方法
- 资源剥夺法:挂起(暂时放到外存上)某些死锁进程,并抢占他的资源,将这些资源分配给其他死锁进程。但是应该防止被挂起的进程长时间得不到资源而饥饿
- 撤销进程法:强制撤销部分、甚至全部死锁进程,并剥夺这些进程的资源。这种方式的优点是实现简单,但所付出的代价可能会很大。因为有些进程可能已经运行了很长时间,已经接近结束了,一旦被中止可谓功亏一篑,还需从头再来
- 进程回退法:让一个或多个死锁进程回退到足以避免死锁的地步,这就要求系统要记录进程的历史信息,设置还原点