进程互斥
互斥的概念
临界区(critical section):不允许多个并发进程交叉执行的一段程序
互斥:不允许两个以上的共享该资源的并发进程同时进入临界区。
一个好的互斥方案应满足以下条件:
- 不能假设并发进程的相对执行速度。也就是说各并发程序享有平等、独立的竞争资源的权力。
- 临界区外的进程不应阻止其他进程进去临界区
- 不允许两个进程同时处于临界区
- 临界区的代码执行时间要短
在这里,前三个准则是保证并发程序进程享有平等的机会占有资源。
最后一个准则是为了防止死锁的产生。
实现互斥的方案
1.加锁实现
设置共享变量lock
lock=0 临界区内无进程
lock=1 临界区内有进程
while(lock);
lock = 1;
<Critical Section>
lock = 0;
<NoCritical Section>
但是加锁实现的方法是有缺陷的:当上锁的时候,lock=1执行之前的时候,可能已经有多个进程进去临界区。
2.严格轮转法
设置共享变量turn,指示进入临界区的进程号
以2个进程为例
turn=0 允许进程0进入临界区
turn=1 允许进程1进入临界区
进程0:
while(turn != 0);
;
<Critical Section>
lock = 1;
<NoCritical Section>
进程1:
while(turn != 1);
;
<Critical Section>
lock = 0;
<NoCritical Section>
很显然,这不是一个现实的方案
3.Peterson解决方法
enter_region(process);
<Critical Section>
leave_region(process);
<NoCritical Section>
当一个进程想进入临界区的时候,先调用enter_region判断是否能够安全进入,不能的话等待。退出的再调用leave_region允许其他进程进入临界区。
两个函数的参数都是进程号。
#define FALSE 0;
#define TRUE 1;
#define N 2//进程的个数
int turn;//轮到谁
int interested[N];//兴趣数组,初始值都为FALSE
/*
*turn表示要进入临界区的进程号
*interested[i] == TRUE 表示进程i要求进入或者正在临界区执行
*/
void enter_region(int process){
int other;
other = 1 - process;
interested[process] = TRUE;//希望进入临界区
turn = process; //设置标志位
while(turn == process && interested[other] == TRUE);
}
void leave_region(int process){
interested[process] == FALSE;
}
4.关中断
因为CPU在中断的时候才会发生进程切换
所以进程要进入临界区关中断,离开了再开中断就可以实现
关中断;
<Critical Section>
开中断;
<NoCritical Section>
缺点:
1. 对于多处理机系统无效。在多处理机系统中,可能存在一个以上的进程在不同的处理机上同时执行,关中断不能保证互斥。
2. 将关中断的操作交给用户很危险。
5.机器指令
TSL指令:执行TSL指令的CPU锁住总线,以禁止其他的CPU在本指令结束之前访问内存
前面的方法虽然能够实现互斥,但是存在着忙等待的问题
当一个进程想进入临界区的时候,要不断的检测是否能够进入,这样会浪费大量的CPU时间去等待,还会有优先级反转的问题
信号量
定义
下面让我们仔细的思考一下为什么会出现忙等待的情况:
首先,每个进程自己维护自己的一个测试,去判断自己能否进入临界区。这样就导致了每个进程能否进入临界区依靠自己的测试去判断,这样没有获得执行机会的进程当然无法判断,出现不公平的现象。而获得执行机会的进程又要不断地去测试,占用了CPU的执行时间。
如何解决问题,最主要的是让一个超越应用进程的管理者负责如何分配资源,这个管理者就是信号量。
信号量是一个整数。
在sem>=0的时候代表可供并发进程使用的资源实体数;
sem<0表示正在等待使用临界区的进程数
信号量的使用:
必须置一次并且只能置一次初值
只能用PV操作信号量
除此之外不能使用其他方式访问信号量
PV操作
P、V操作是原子操作(不可分割)
信号量的数据仅能由PV原语操作改变
原语的两方面的含义:
1. 机器指令级:原语的程序段在执行过程中不允许中断
2. 功能级:原语的程序不允许并发执行
P操作的主要动作:
- sem-1
- 若sem>=0,p原语返回,进程继续执行
- 若sem<0,进程阻塞,并插入到该信号量的等待队列中。之后进程调度
V操作的主要动作:
- sem+1
- 若sem>0,V原语停止执行,返回调用初继续执行
- 若sem<=0,从等待队列中唤醒一个进程,然后在返回或者将进程调度
信号量的实现方式
忙等待
//信号量类型定义
typedef struct{
int value;//信号量的值
int lock;//锁,初始值为0
}Semophore_t;
//V操作
void V(Semophore_t *ps){
while(TSL(&ps->lock));//上锁
ps->value++;
ps-lock=0;
}
//P操作
void P(Semophore_t *ps){
for(;;){
while(TSL(&ps->lock));//上锁
if(ps->value > 0){
ps->value--;
break;
}
ps->lock = 0;
}
ps->lock = 0;
}
这里P操作这样写是为了当信号量的值小于的时候立马放开锁
阻塞
//信号量类型定义
typedef struct{
int value;//信号量的值
SemaQueue *list; //等待该信号量的等待队列
int lock;//锁,初始值为0
}Semophore_t;
//P操作
void P(Semophore_t *ps){
while(TSL(&ps->lock));//上锁
if(ps->value > 0){
ps->value--;
ps->lock = 0;
}else{
将该进程加入ps->list;
阻塞该进程;
ps->lock = 0;
}
}
//V操作
void V(Semophore_t *ps){
while(TSL(&ps->lock));//上锁
if(ps->list == NULL){
ps->value++;
}else{
从ps->list移除一个进程;
将该进程放入就绪队列;
}
ps-lock=0;
}
进程同步
把异步环境下的一组并发进程,因直接制约而互相发送消息而进行互相合作、互相等待,使得各进程按一定的速度执行的过程就做进程同步
生产者消费者问题
#define N 100
#define TRUE 1
//信号量定义
Semaphore_t mutex = 1,//保证进程互斥访问缓冲区
empty = N,//空槽的个数
full = 0;//非空槽的个数
void producer(){
while(TRUE){
product();//生产一个产品
p(&empty);//申请一个空槽
p(&mutex);//互斥访问临界区
append();
v(&mutex);//离开临界区
v(&full);//非空槽+1
}
}
void consumer(){
while(TRUE){
p(&full);//申请一个非空槽
p(&mutex);//互斥访问临界区
remove();//缓冲区拿走一个
v(&mutex);//离开临界区
v(&empty);//非空槽+1
consume();
}
}
理发师问题
理发店有1位理发师和1个椅子。
没有顾客的时候理发师在椅子上睡觉
顾客来的时候,如果理发师在睡觉,就叫醒理发师,剪完之后离开。
Semaphore_t customers = 0,//等待的顾客数
barbers = 0;//空闲的理发师数
//理发师进程
void barber(){
while(TRUE){
p(&customers);//没有顾客时候睡觉
V(&barbers);//一个理发师休息
cut_hair();//为一位顾客理发
}
}
//顾客进程
void customer(){
V(&customers);//来了一个顾客
p(&barbers);//等候理发
get_haircut();//开始理发
}
理发店有1位理发师、一个理发椅子,和3个等候理发椅子。
没有顾客的时候,理发师在椅子上睡觉
顾客来的时候,如果没有空椅子坐,则离开。
如果理发师忙,有空椅子坐,坐下等候
理发师在睡觉,就叫醒理发师,剪完之后离开。
#define CHAIRS 3
Semaphore_t customers = 0//等候理发的顾客数
barber = 0//等候顾客的理发师数
mutex = 1//互斥
int waiting = 0;//等待理发的人数
void barber(){
while(TRUE){
p(&customers);
p(&mutex);
waiting--;
V(&mutex);
V(&barbers);
cut_hair();
}
}
void customer(){
p(&mutex);
if(waiting >= CHAIRS){//如果没有椅子就离开
V(&mutex);
return ;
}
waiting++;
V(&customers);
V(&mutex);
p(&barbers);
get_haircut();
}
哲学家进餐问题
#define TRUE 1
#define N 5//哲学家的个数
Semaphore_t fork[] = {1,1,1,1,1};
void philosopher(int i){
while(TRUE){
think();
p(&fork[i]);
p(&fork[i+1] % N);
eat();
V(&fork[i]);
V(&fork[i+1] % N);
}
}
这样写会导致死锁的问题,当5位同时拿起左边的叉子,就会相互等待
所以我们就需要设置一个互斥访问的信号量,能保证5个人不要同时拿起左边的叉子
#define TRUE 1
#define N 5//哲学家的个数
Semaphore_t fork[] = {1,1,1,1,1}, mutex=1;
void philosopher(int i){
while(TRUE){
think();
p(&mutex);
p(&fork[i]);
p(&fork[i+1] % N);
V(&mutex);
eat();
V(&fork[i]);
V(&fork[i+1] % N);
}
}
这里除了互斥叉子的5个信号量外,再引入一个信号量来互斥拿叉子的动作。
但是这样做的问题是同时只能有一个人能够吃饭。
#define TRUE 1
#define N 5//哲学家的个数
Semaphore_t fork[] = {1,1,1,1,1}, e=N-1;
void philosopher(int i){
while(TRUE){
think();
p(&e);
p(&fork[i]);
p(&fork[i+1] % N);
eat();
V(&fork[i]);
V(&fork[i+1] % N);
V(&e);
}
}
这里除了互斥叉子的5个信号量外,再引入1个信号量e=4,最多同时允许4个人同时吃饭,保证一个人能够拿到左右两个叉子
#define TRUE 1
#define N 5//哲学家的个数
Semaphore_t fork[] = {1,1,1,1,1};
void philosopher(int i){
while(TRUE){
think();
if(i == N -1){
p(&fork[0]);
p(&fork[N-1]);
}else{
p(&fork[i]);
p(&fork[i+1]);
}
eat();
V(&fork[i]);
V(&fork[i+1] % N);
}
}
这里提供的一种解决方案是不要让所有的人都同时拿起左边的叉子
也就是到最后一个人的时候,先拿右边的叉子,这样就避免了相互等待
#define TRUE 1
#define N 5//哲学家的个数
#define LEFT(i-1+N) % N //哲学家i的左邻居
#define RIGHT(i+1) % N //哲学家i的右邻居
#define THINKING 0 //哲学家正在思考
#define HUNGRY 1//哲学家想取叉子
#define EATING 2//哲学家正在吃饭
int state[] = {THINKING,THINKING,THINKING,THINKING,THINKING};//哲学家状态
Semaphore_t mutex = 1;//临界区互斥
s[] = {0,0,0,0,0};//表示哲学家是否具备到得到叉子吃饭的条件
void philosopher(int i){
while(TRUE){
think();
take_forks(i);//取左右2把叉子
eat();
put_forks(i);//放左右2把叉子
}
}
void take_forks(int i){
p(&mutex);//进入临界区
state[i] = HUNGRY;
test(i);//看是否能进餐
V(&mutex);//离开临界区
P(&s[i]);//取得叉子进餐
}
void put_forks(int i){
p(&mutex);//进入临界区
state[i] = THINKING;
test(LEFT);//唤醒满足条件的左邻居进餐
test(RIGHT);//唤醒满足条件的右邻居进餐
V(&mutex);//离开临界区
}
void test(int i){
if(state[i] == HUNGRY && state[LEFT] != EATING && state[RIGHT] != EATING){
state[i] = EATING;
V(&s[i]);
}
}
这里哲学家进餐的时机:饥饿并且左右邻居不在进餐