有关进程同步的问题,我主要分3节来表述,第一节就是本节主要讲一些底层的同步方法: 禁用中断,软件方法,锁机制。第二节即下一节主要是讲信号量机制,第三节主要讲管程机制
本节内容组织如下:
- 同步互斥的背景
- 同步互斥的解决方案,禁用中断,软件方法,锁机制
同步互斥的背景
如果程序之间是独立的,没有并发的,那么肯定不会出现同步互斥问题。由于引入了操作系统对进程的调度,特别是中断机制允许在任何时候发生中断切换到下一个进程执行,这就会发生进程同步问题。比如下面的一个例子:
fork():创建新的进程的时候会将新进程的id赋值为next_id++,即有下面一条语句:
new_id = next_pid++
而这条语句并不是原子操作,转化成汇编指令如下:
load next_pid reg1
store reg1 new_pid
INC reg1
store reg1 next_pid
由于中断发生的任意性没人知道会在什么时候发生,在一种情况下如果在第二条指令后插入一个新进程,同时执行这段代码,那么就会发生同步错误,如下图所示:
同样的例子可以在生活中找到,这里就不在举例了。
临界区:
OS中将进程访问资源的互斥代码称为临界区(同一时刻仅仅允许一个一个进程进入)
对于临界区我们希望做到以下几点访问规则:
- 空闲则入(没进程访问的时候任何进程都能访问)
- 忙则等待 (有进程访问的时候当前必须等待)
- 有限等待 (等待的进程应该可以进入,不能发生饥饿)
- 让权等待 (等待的进程应该释放掉CPU)
接下来我们说一说,关于临界区保护的一些机制:
- 禁用中断
- 软件同步方法
- 原子操作-锁机制
禁用中断
这是一种最简单的硬件同步方法,也就是硬件提供了两条指令:
- cli : 关中断
- sti:开中断
这个可以说是从源头上解决了进程同步问题,不过也会出现一些其他问题:
- 进程无法被抢占
- 可能导致其他进程饥饿
- 无法确定响应中断所需的时间,无法适应应用层临界区很长的情况。
软件同步方法
这个是相当复杂的一种方法,现代OS已经不再使用,不过他提供了一种基于软件同步的解决方案,这里我们简要介绍一下 Peterson 算法
turn变量的值决定了谁能进入临界区,前两条指令执行后,turn值总会变成 i or j , (假设两个进程同时访问这段代码)那么这是的while代码不会将两个进程都”悬挂”在这里。这时 flag 均为ture,而turn 总是仅能为其中一个所以总有一个能进入。满足”空闲则入”和”互斥进入”,而且 在其中一个进程,不妨设为 Pj 进入临界区后,会将flag[j]设为false,这时 Pi 就能进入了,也即满足 “空闲则入”
缺点
这种算法最大的问题是复杂,想想你是应用程序员,你去写这段代码来实现临界区保护,是不是很可怕,更别说多个进程的情况了。2)第二个问题是忙等,进程等待的时候在空转。
接下来我们来说一下基于硬件同步的第二种机制-锁
高级抽象方法-锁
原子操作指令
现代CPU体系结构都会提供一些特殊的原子操作指令(就是在硬件层实现的一条指令的操作?)
如 TS 和Exchange
测试并置1
bool TestAndSet(bool *tar ){
bool rv = *tar;
*tar = true;
return rv;
}
Exchange
void Exchange(bool *a,bool *b){
bool tmp = *a;
*a = *b;
*b = tmp;
}
用这两条指令我们就可以显现高层抽像-锁
原子锁
自旋锁
这种说顾名思义,如果锁被占用,访问锁的进程将会忙等。
无忙等锁
这种锁的进步就是加一个等待队列,将无法获得锁的进程加入等待队列。
优点:
- 实现简单,任意多个进程都容易处理
- 很容易扩展到多处理器
缺点:
- 可能导致饥饿?
- 忙等锁占用空耗CPU
- 死锁(低优先级进程占有锁等待CPU,高优先级进程占有CPU等待锁)