操作系统 —— 同步机制
基础概念
-
进程互斥
由于各进程要求使用共享资源(变量、文件等),而这些资源需要排它性使用,各进程之前竞争使用这些资源 —— 这一关系称为进程互斥。 -
临界资源
系统中某些资源一次只允许一个进程使用,称这样的资源为临界资源或互斥资源或共享变量。 -
临界区(互斥区)
各个进程中对某个临界资源(共享变量)实施操作的程序片段。
临界区的使用规则
- 没有进程在临界区时,想进入的临界区可进入
- 不允许两个进程同时处于其临界区中
- 临界区外运行的进程不得阻塞其它进程进入临界区
- 不得使进程无限期等待进入临界区
实现临界区互斥的方案
- 软件方案 Dekker 解法,Peterson 解法
- 硬件方案 屏蔽中断,TSL、XCHG 指令
进程互斥的解决方案
软件解法
-
Dekker 算法
1965年,第一个软件解法 -
Peterson算法
1981年
enter_region(i);
//code
leave_region(i);
硬件解法
1 中断屏蔽法
“开关中断”指令
算法
① 执行关中断指令
② 临界区操作
③ 执行开中断指令
特点
- 简单,高效
- 代价高,限制CPU并发能力(临界区大小)
- 不适用于多处理器
- 适用于操作系统本身,不适用用户进程
2 “测试并加锁”指令
TSL指令:TEST AND SET LOCK
TSL执行前把总线锁住,结束后把总线打开
enter_region:
TSL REGISTER,LOCK ;复制锁到寄存器并将锁置1
CMP REGISTER,#0 ;判断寄存器内容是否是0
JNE enter_region ;若不是0,跳转到enter_region
RET ;返回调用者,进入了临界区
leave_region:
MOVE LOCK,#0 ;在锁中置0
RET ;返回调用者
3 “交换”指令
XCHG指令:EXCHANGE
(类似于 TSL 指令)
enter_region:
MOVE REGISTER,#1 ;给寄存器存1
XCHG REGISTER,LOCK ;交换寄存器和锁变量的内容
CMP REGISTER,#0 ;判断寄存器内容是否是0
JNE enter_region ;若不是0,跳转到enter_region
RET ;返回调用者,进入了临界区
leave_region:
MOVE LOCK,#0 ;在锁中置0
RET ;返回调用者
忙等待(busy waiting)
进程在得到临界区访问权之前持续作测试而不做其它事情。
自旋锁(Spin Lock)
适用于多处理器,通过忙等待减少进程切换的开销
进程同步
进程同步指系统中多个进程中发生的事件存在某种时序关系,需要相互合作,共同完成某一项任务。
信号量及 PV 操作
一种卓有成效的进程同步机制。1965年,由荷兰学者Dijkstra提出。定义如下:
struct semaphore{
int count;
queueType queue;
}
对信号量可以实施的操作:初始化、P和V
- P、V分别是荷兰语的test(proberen)和increment(verhogen)
- P,down,semWait
- V,up,semSignal
- PV操作为原语操作(primitive or atomic action)
用PV操作解决进程间互斥问题
- 分析并发进程的关键活动,划定临界区
- 设置信号量mutex,初值为1
- 在临界区前实施P(mutex)
- 在临界区后实施V(mutex)
P(s){
s.count--;
if(s.count < 0){
//该进程状态置为阻塞状态;
//将该进程插入相应的等待队列s.queue末尾;
//重新调度;
}
}
V(s){
s.count++;
if(s.count <= 0){
//唤醒相应等待队列s.queue中等待的另一个进程
//改变其状态为就绪态,并将其插入就绪队列
}
}
信号量解决生产者消费者问题
void producer(void){
int item;
while(TRUE){
item = produce_item();
P(&empty);
P(&mutex);
insert_item(item);
V(&mutex);
V(&full);
}
}
void consumer(void){
int item;
while(TRUE){
P(&full);
P(&mutex);
item = remove_item();
V(&mutex);
V(&empty);
consume_item(item);
}
}
信号量解决读者写者问题
问题描述:
多个进程共享一个数据区,这些进程分成两组
- 读者进程:只读数据区中的数据
- 写者进程:只往数据区写数据
要求满足条件:
- 允许多个读者同时执行读操作
- 不允许多个写者同时操作
- 不允许读者、写者同时操作
第一类读者写者问题:读者优先
void reader(void){
while(TRUE){
P(mutex);
rc = rc + 1;
if(rc == 1) P(w);
V(mutex);
P(mutex);
rc = rc - 1;
if(rc == 0) V(w);
V(mutex);
}
}
void writer(void){
while(TRUE){
P(w);
写操作
V(w);
}
}
管程
相关概念
管程
管程是一个抽象数据类型。由关于共享资源的数据结构及在其上操作的一组过程组成。
信号量的缺点
程序编写困难、易出错。
进程与管程
进程只能通过调用管程中的过程来间接地访问管程中的数据结构。
管程与互斥
管程是互斥进入的 —— 某一时刻只有一个进程/线程调用管程中的过程。管程的互斥性由编译器负责保证。
管程与同步
管程中通过设置条件变量及等待/唤醒操作以解决同步问题。可以让一个进程或线程在条件变量上等待(此时,应先释放管程的使用权),也可以通过发送信号将等待在条件变量上的进程或线程唤醒。
解决同步问题的具体方式
当第一个进程在管程中等待并释放管程的互斥权,第二个进程进入后执行唤醒操作(例如 P 唤醒 Q),此时管程中便存在两个同时处于活动状态的进程。三种处理方法:
- P等待Q执行。 Hoare
- Q等待P继续执行。 MESA
- 规定唤醒操作作为管程中最后一个可执行的操作。
HOARE 管程
基本原理
因为管程是互斥进入的,所以当一个进程试图进入一个已被占用的管程时,应当在管程的入口处等待。为此,管程的入口处设置一个进程等待队列,称为 入口等待队列。如果进程P唤醒进程Q,则P等待Q执行;如果进程Q执行过程中又唤醒进程R,则Q等待R执行。如此,在管程内部可能会出现多个等待进程。在管程内也需要设置一个进程等待队列,称为 紧急等待队列,紧急等待队列优先级高于入口等待队列。
条件变量的实现
条件变量 —— 在管程内部说明和使用的一种特殊类型的变量。对于条件变量,可以执行wait/signal操作。
wait(c):
如果紧急等待队列非空,则唤醒第一个等待者;否则释放管程的互斥权,执行此操作的进程进入c链末尾。
signal(c):
如果c链为空,则相当于空操作,执行此操作的进程继续执行;否则唤醒第一个等待者,执行此操作的进程进入紧急等待队列的末尾。
MESA 管程
介绍
Hoare 管程的缺点 —— 两次额外的进程切换。
MESA 管程的解决:
signal -> notify
notify:当一个正在管程中的进程执行 notify(x) 时,它使得x条件队列得到通知,发信号的进程继续执行。
基本原理
当 x条件队列得到通知时,位于条件队列头的进程将在合适的时候且当处理器可用时恢复执行。由于不能保证在它之前没有其它进程进入管程,因为这个进程必须重新检查条件(需要使用 while 循环)。
进程间通信
信号量及管程的不足:不适用多处理器的情况。
基本通信方式
消息传递、共享内存、管道以及 远程过程调用RPC
linux 内核同步机制
原子操作、自旋锁、读写自旋锁、信号量、读写信号量、互斥体、完成变量、顺序锁、屏障
原子操作
atomic_t v = atomic_init(0);
atomic_set(&v,4);
atomic_add(2,&v);
atomic_inc(&v);
printk("%d\n",atomic_read(&v));
屏障
一组线程协同完成一项任务,需要所有线程都到达一个汇合点后再一起向前推进。