进程同步与信号量、及临界区对其保护
总结:用临界区对信号量进行保护,用信号量实现进程间同步
1.进程合作?
进程合作:多进程共同完成一个任务,即合理有序的推进(通过阻塞、唤醒,“走走停停”)
实例1:
司机进程
while(true)
{
/*等售票员关车门*/
启动车辆
正常行驶
停靠车辆
/*通知售票员车辆停靠好*/
}
售票员进程
while(true)
{
/*等待司机停靠车辆*/
打开车门
售票
关闭车门
/*告知司机车门关闭好*/
}
合理、有序推进的关键是
找到需要同步的位置
- 司机启动车辆之前 (司机进程)
- 售票员打开车门之前(售票员进程)
接收到对方的消息后方可继续下一步的操作
即,发信号
2.发信号解决合作问题?
实例2:
生产者
while(true)
{
if(count==buffer_size)
sleep();
...
counter=counter+1;
if(counter==1) wakeup(消费者);//信号
}
消费者
while(true)
{
if(conter==0)
sleep();
...
counter=counter-1;
if(counter==buffer_size-1)
wakeup(生产者);//信号
}
分析*信号*
能否解决进程间同步问题?
-
缓冲区满后,生产者p1想生产一个item放入时,会sleep()
-
又来一个生产者p2想生产一个item放入时,继续sleep()
-
消费者c1执行一次,消费一个item,counter-1,counter此时=buffer_size-1,故执行wakeup(生产者),唤醒p1
-
消费者c1又执行一次,counter-1,此时counter=buffer_size-2,p2不能被唤醒
-
为何出现这种现象?
根源在于:counter 的含义提供的信息过少,不足以表达有多少进程正在等待,比如counter加到buffer_size就无法继续增加,无法知道有多少生产者来过
如何解决?
——用一个变量,能承载更多信息的变量——信号量
分析如下:
-
缓冲区满后,p1执行,sleep()
value=-1
含义:一个进程等待 -
p2执行,继续sleep()
value=-2
含义:两个进程等待 -
c1执行一次,wakeup(p1)
value=-1
含义:从等待队列唤醒一个进程 -
c1又执行一次,wakeup(p2)
value=0
-
c1再执行一次
value=1
含义:再来一个生产者时直接执行,无需等待 -
p3执行
value=0
含义:用掉一个资源(此时缓冲区满),再来生产者时又需要等待
p,c根据信号量value来决定等待/唤醒
value就是一个信号量
3.靠信号还不够?从信号到信号量
信号量定义
信号量机制是由E.W.Dijkstra提出的一种同步机制,
也称P/V操作,P/V操作是信号量的一种实现。
以上面的例子来看:
-
生产者生产前P一下信号量,决策下一步行为,生产后V一下信号量
-
消费者消费前P一下信号量,决策下一步行为,消费后V一下信号量
-
通过P/V操作实现信号量,用计算机语言模仿上述人脑根据value的值判断下一步的行为
实例3:
struct semaphore
{
int value;//记录资源个数
PCB *queue;//等待队列
}
P(semaphore s);//消费资源
V(semaphore s);//生产资源
P(semaphore s)//申请资源前P一下信号量看看
{
s.value--;
if(s.value<0){//表明之前要么=0没资源,要么<0还欠资源(有人还正在sleep)
sleep(s.queue);//那么该进程也跟着sleep()
}
}
V(semaphore s)
{
s.value++;
if(s.value<=0){//表明之前<0,必定有sleep()的,故从等待队列中唤醒一个
weakup(s.queue);
}
}
用信号量分析生产者/消费者问题
semaphore full=0;//表示消费者可用资源数(有full个东西可用消费,初值为0)
semaphore empty=buffer_size;//表示生产者可用资源数(生产者还有empty个空间可用生产,初值为buffer_size)
Producer(item)
{
生产一个产品
P(empty);//p(empty)的目的是生产者在执行生产之前先看一下,是否可以继续生产,是否要sleep排队等待
P(mutex);//互斥信号量
、、、送产品到缓冲区
V(mutex);
V(full);//v(full)的目的是,释放一个消费者监视的信号量full,即通知消费者可以继续消费
}
Consumer()
{
P(full);
P(mutex);
、、、从缓冲区取产品
V(mutex);
V(empty);
消费一个产品
}
4.有了信号量就没问题?——临界区的保护
信号量有了,就可以保证进程之间合理推进嘛?
前提是该信号量永远正确,信号量是一个共享变量,若多个进程同时去修改信号量,或者某进程修改信号量的过程中另一个进程被调度,切入了另一个进程,信号量就不能保证正确,进程间的同步也就不正确了。
如何保证信号量正确?——保证修改信号量时一次做完(原子操作)
1.某进程进入修改信号量empty的代码时,其他进程不能进入其相应的修改empty的代码——找出进程中的临界代码区
2.信号量保护的实质就是让进程中修改信号量的代码变成临界区代码
如何保护?
进入临界区之前写点代码——进入区代码
临界区
退出临界区之后写点代码——离开区代码
保护原则?
- 互斥进入
- 有空让进
- 有限等待
如何实现?
-
尝试一:轮换法(值日法)
-
尝试二:标记法
以上两种不满足保护原则
-
轮换+标记——Peterson算法
进程P0
flag[0]=true;//进去之前打个标记
turn=1;
while(flag[1]&&turn==1);//发现P1打了标记(P1要去执行),并且轮到1(turn=1),空转,否则进入
临界区
flag[0]=false;
剩余区
c_进程P1
flag[1]=true;
turn=0;
while(flag[0]&&turn==0);//发现P0打了标记(P0要去执行),并且轮到0(turn=0),空转,否则进入
临界区
flag[1]=false;
剩余区
有没有更简单的方法?
临界区怕什么?——怕执行一半另一个进程切入
为啥另一个进程会切入?——被调度才能执行,才能切入
那进入临界区之前阻止调度可以吗?——关中断,可以
但是多核cpu时不好使
那进临界区之前加锁,出来时解锁?——好使,但是锁是啥?
锁是一个变量,进去前看一下,出来时修改一下…这个貌似与信号量概念异曲同工?
故用这个锁也需要临界区保护?
信号量保护信号量保护信号量…?——禁止套娃
将该锁做成硬件原子指令即可
临界区保护的硬件原子指令法
TS 指令
boolean TestAndSet(boolean &x)
{
boolean temp=x;
x=true;
return temp;
}
lock==true 上锁空转(表示无资源)
lock==false 返回false(表示资源空闲),进入,并在TS指令中将lock修改为true(将资源上锁)
TS指令实现:
while(TestAndSet(&lock));//只有该语句通过,进入临界区时,lock一定为true
/****临界区***/
lock=false;
/****剩余区***/
Swap指令
void swap(bool &a,bool &b){
int temp=a;
a=b;
b=temp;
}
为每个临界区设置bool型锁变量,如lock
swap指令实现:
bool lock=false;
bool key=true;
do{
swap(key,lock)
}while(key);//key交换到true时空转,只有当key交换到false,即lock==false时(表示有资源空闲),进入,此时lock被交换为true
/****临界区***/
swap(key,lock);//开锁,将lock改回false
5.信号量解决实际问题的案例
5.1哲学家进餐问题
semaphore fork[5];
for(int i=0;i<5;i++)
fork[i]=1;
progress philosopher_i(){
while(true)
{
think();
P(fork[i]);//拿起左边筷子之前P一下他,确定没人拿(回顾上述P操作含义,申请资源时需要P一下信号量,判断是否需要进入等待队列)
P(fork[(i+1)%5]);
eat();
V(fork[i]);//吃完之后放下左筷子时V一下他,判断是否有人在等待这根筷子,有则唤醒他(回顾上述V操作含义,释放资源时V一下信号量,判断是否需要从等待队列中唤醒一个进程,若>0,表明无人等待,有资源可用,不用唤醒;<=0,唤醒)
V(fork[(i+1)%5]);
}
}
注:以上方法当五位哲学家同时拿起左边筷子时,都会卡在P(fork[(i+1)%5]),会发生死锁,解决方法之一是至多允许4位哲学家同时进餐
修改如下:
、、
semaphore tongshi=4;
、、、
while(true)
{
think();
P(tongshi);//拿筷子之前P一下tongshi,若<0,则之前必定<=0,必定有4位哲学家已经开始进餐,则该进程sleep
P(fork[i]);
P(fork[(i+1)%5]);
eat();
V(fork[i]);
V(fork[(i+1)%5]);
V(tongshi)
}
5.2读者-写者问题
两组并发进程:读者/写者,共享文件F
要求:
1.允许多个读者同时对文件执行读操作
2.只允许一个写者对文件执行写操作
3.任何写者在完成写操作之前不允许其他读者/写者工作
4.写者在执行写操作之前,应让已有的写者和读者全部退出
int readcount=0;
semaphore writeblock=1;//是否允许写
semaphore mutex=1;//对修改readcount计数器的互斥信号量
process reader_i(){
P(mutex);
readercount++;
if(readcount ==1 )
P(writeblock);//第一位读者阻止写者进入
V(mutex);
/*读文件*/
P(mutex);
readcount--;
if(readcount==0)//最后一个读者允许写者进入
V(writeblock);
V(mutex);
}
process writer_i(){
P(writeblock);
/*写文件*/
V(writeblock);
}