同步与互斥
OS复习:同步与互斥
[TOC]
同步与互斥概念
在多道程序下,继承是并发执行,不同进程之间存在着不的相互制约关系;为协调进程之间的相互制约关系,引入进程同步概念,当我们计算1+2*3,加法进程一定是在乘法后的,但是OS具有异步性。
临界资源
我们将一次只能给一个进程服务的资源称为临界资源
分为四部分:进入区、临界区、退出区、剩余区
1 | while(true){ |
同步
同步也称直接制约关系,指为完成某种人物而建立的两个或多个进程,这些进程因为要协调它们的运行词语而等待、传递信息所产生的制约关系。同步关系源于进程之间的相互合作。
互斥
互斥是间接制约关系。当一个进程进入临界区使用临界资源,另一个进程必须等待;
在一台打印机系统中,两个进程A和B,当A需要打印时,系统已将打印机分配给进程B,则进程A必须阻塞。一旦进程B将打印机释放,系统便将A唤醒,将其由阻塞态变为就绪态
临界区互斥的准则
为禁止两个进程同时进入临界区,同步机制应该遵循以下原则:
- 空闲让进:临界区空闲,允许一个请求进入临界区
- 忙则等待:已有进程进入临界区,其他试图进入的进程必须等待
- 有限等待: 对请求访问的进程,应保证能在有限时间内进入临界区,防止进程无限等待
- 让权等待:当进程不能进入临界区时,应立即释放处理器,防止进程忙等待
实现临界区互斥的基本方法
软件实现方法
在进入区设置一些标志来标明是否由进程在临界区,若已有进程在临界区,则在进入区通过循环检查进行等待,进程离开临界区后再退出区修改标志
单标志法
1 | 进程P0; |
这样的问题是:当P0进入临界区并离开了,此时turn=1;但是P1不打算进入临界区了,那么之后P0就一直无法拿到临界资源。也就是说,两个进程必须交替进入临界区,很可能造成违背空闲让进的原则
双标志先检查法
1 | //我们用flag[2]数组,标记各个进程想进入临界区的意愿 |
优点:可以不交替进入了
缺点:当执行顺序是1324的时候,在检查对方标识和设置自己的标识前可能发生进程转换,违背了忙则等待
双标志后检查法
1 | //进程P0 |
这里我们先设置自己的标志后检查,但是1342的顺序会导致双方都竞争进入临界区,违背了空闲让进的原则,也会导致两个进程产生饥饿
Peterson算法
1 | //进程P0 |
我们用flag解决互斥访问,用turn解决饥饿问题;
当两个进程的flag都为true,此时turn会决定临界区分配给哪个进程:turn值为1时,P1拿到临界资源;当P1退出临界资源时,将flag[1]设置为flase,允许P0进入临界区;如果P1不想进入临界区了,flag[1]是false,P0也能拿到临界资源
可见Peterson满足了空闲让进、忙则等待、有限等待,但是没有满足让权等待
因为在while循环中使用了忙等待(busy waiting)机制。当进程i发现无法进入临界区时,它并不会主动释放CPU,而是不断循环检查条件。CPU时间被浪费在无效的循环检查上
硬件方法
中断屏蔽方法
1 | ... |
注意:关中断是关闭中断
之前的软件算法会违背了一些原则,是因为在程序运行时可能会发生进程切换,而进程切换会发生中断
那我们只需要在进程拿临界资源时关中断就好了,这样不会被进程切换扰乱
当然有些缺点:
- 限制CPU交替执行程序能力,系统效率降低
- 对于内核,若将关中断权限给用户是不安全的,若一个进程关中断后不再开中断,系统可能会终止
- 不适用多CPU系统
TestAndSet指令
1 | bool TestAndSet(boolean *lock){ |
以上是TS指令,它是原子操作。功能是读出标志后将该标志设置为真;
于是,我们可以用TS做互斥
1 | while TestAndSet(&lock); |
我们用TS指令管理临界区,为每个临界资源设置一个共享值lock,表欧式该资源两种状态,ture上锁,false为加锁
相比软件实现,TS将加锁和检查用硬件方式变成了原子操作,不会被打断;相比关中断,lock是共享的,使用多CPU
缺点: 暂时无法进入临界区的进程会占用CPU循环,跟Peterson算法一样,违背让权等待
Swap指令
Swap顾名思义,交换两个字节的内容:
1 | Swap (bool *a, bool *b){ |
Swap不能被中断
1 | //lock是共享的 |
硬件实现互斥的优点:
- 简单,支持多CPU;
- 支持系统有多个临界区,只需为每个临界区设置一个布尔变量
缺点:
- 进入临界区进程会占用CPU执行While循环,违背“让权等待”
- 从等待进程随机挑一个进入临界区,可能会造成某些进程饥饿
互斥锁(mutex lock)
1 | acquire(){ //获得锁 |
进程进入临界区调acquire获得锁,退出临界区调release,释放锁;当进程获取不可用的锁,会被阻塞
acquire和release都是原子操作,通常用硬件实现
上面的mutex lock也称自旋锁,缺点是忙等待;当多个进程共享一个CPU时,连续循环浪费了CPU周期,因此Mutex lock多用于多CPU系统,一个线程可以在一个处理器上旋转,而不影响其他线程的执行。自旋锁优点是,进程在等待锁期间,没有上下文切换,若上锁时间短,则等待代价不高
PV 信号量
两个标准原语wait()和signal()访问,简写为P(),V()
整型信号量
1 | wait(S){ |
在整型信号量机制中的wait操作,只要信号量S<=0,就会不断循环测试,没有让权等待
记录型信号量
记录型就不存在忙等;采用记录型的数据结构:
1 | //记录型信号量 |
信号量实现进程互斥
为了多个进程能互斥访问某个临界资源,对临界资源设置一个互斥信号量S
1 | semaphore S=1; |
- 对不同临界资源要设置不同的互斥信号量
- PV必须成对出现,少P无法保证互斥访问临界资源,少V使得临界资源无法被释放,使等待该资源的进程无法被唤醒
- 有多少资源就将信号量设置为多少
利用信号量实现同步
我们假设情形:Y一定在X后执行
1 | semaphore S=0; |
总而言之,在某个行为会提供某种资源,则这个行为后V;当某个行为要用这个资源,执行P;在互斥问题中,PV操作要夹紧临界资源的行为,中间不能有其他冗余代码
信号量实现前驱关系
信号量也可以用来描述语句之间的前驱关系;
我们可以把每队前驱关系都是一个同步问题,因此为每对前驱关系设置一个同步信号量,初始值均为0;
具体过程不赘述,非常好理解
用信号量实现同步和互斥是常考点,分析步骤如下:
- 关系分析:找出进程数,分析它们的同步和互斥关系
- 整理思路:根据进程流程,确定大致PV操作
- 设置信号量
经典同步问题
生产者-消费者问题
producers 和 consumers共享一个初始为空、大小为n的缓冲区。操作逻辑与管道相同,只有当缓冲区未满,producer才能将消息放入缓冲;只有当缓冲区未空,consumer才能从缓冲区读消息;除此之外,都要被阻塞;由于缓冲区是临界资源,必须互斥访问
1 | semaphore mutex=1; //临界区互斥信号量 |
注意对empty和full的P操作一定是在P(mutex)之前的
如果先P(mutex)会发生死锁:
当procuser 将buffer放满,此时empty=0; consumer未取走item,下一次调producer先执行了p(mutex), mutex被producer锁了,但是遇到p(empty)阻塞了;然而consumer无法V(empty),因为mutex被Producer锁了,产生死锁现象;同理,当consumer将buffer读完,full=0;producer生产item,还是调consumer ,那么mutex被consumer锁了,full=0 ,consumer自身阻塞;producer因为mutex被锁了无法进入临界区,也是死锁
读者-写者问题
读者和写着两组并发进程,共享一个文件;要求
- 允许多个读者同时对文件执行读操作
- 只允许一个写者往文件写信息;
- 任意一个写者在完成写操作前不允许其他读者或写者工作
- writer执行写操作前,应让已有reader和writer全部退出
1 | int count=0; //记录当前读者数量 |
这样的话,reader优先,很可能造成writer饥饿
如果我们写完writer优先,可以增加两对PV操作:
1 | int count=0; //记录当前读者数量 |
哲学家进餐问题
懒,图不放了
五个人围着圆桌坐,每两个人中间都有一根筷子。只有他同时拿到了两根筷子才能进餐;
如果让他们试图同时拿两根筷子,所有人都拿左手边时,发现右手的筷子被强了,死锁
so
我们规定:当一名哲学家左右手的筷子都可用时,才能抓筷子
1 | semaphore chopstick[5]={}; |
我们关键时加了互斥锁mutex,告诉你,我要拿筷子了,其他人不许动!
这样能避免死锁
管程
管程能保证进程互斥,无需用户自己实现互斥.
管程(Monitor)是一种用于多线程互斥访问共享资源的程序结构。管程由四部分组成
- 管程的名称
- 局部于管程内部的共享数据结构说明;
- 对该数据结构进行操作的一组过程
- 对局部与管程内部的共享数据设置初始值的语句
1 | monitor Demo{ //定义名称为Demo的管程 |
管程很像一个类
- 管程将共享资源的操作封装,共享数据结构只能被管程内的过程访问。一个进程只有通过调用管程内的过程,才能进入管程访问共享资源。对于上例,外部进程只能通过take_away()过程来申请一个资源
- 每次只允许一个进程进入管程,从而实现进程互斥。若多个进程同时调用take_away(),give_back(),则只有某个进程运行完它调用的过程后,下一个进程才能开始它调用的过程
当一个进程进入管程被阻塞,在此期间,如果该进程不释放管程,那么其他进程无法进入管程。为此,将阻塞原因定义为条件变量;每个条件变量保存一个等待队列,用于记录因该条件变量而阻塞的所有进程。对条件变量只能两种操作,wait(),signal()
x是条件变量
x.wait: 当x对应的条件不满足时,正在调用管程的进程调用x.wait将自己插入x条件等大地队列,并释放管程
x.signal: 当x对应条件发生变化,调用x.signal,唤醒一个因x阻塞的进程
1 | monitor Demo{ |
条件变量 compare 信号量
- 条件变量的wait和signal类似信号量的PV,实现进程的阻塞/唤醒
- 条件变量没有值,仅仅实现排队等待功能;而信号量有值,反映剩余资源数,在管程中,剩余资源数由共享数据结构来记录