在临界区的调度原则中有:
- 互斥使用
- 有空让进
- 忙则等待
- 有限等待
- 择一而入
- 算法可行
在实际应用中,我们考虑对临界区的管理有软件算法,也有硬件设施,但是这些偏软,偏硬的方法,或存在复杂、效率低下,或存在浪费CPU时间等问题。下面笔者将和大家谈谈一种新的同步工具:信号量和PV操作。
PV操作
PV操作是属于原语操作,原语操作即是执行时是不可被打断的,如原子一般不可再分,通过PV操作我们可以保证执行时不可打断且信号量值的完整性。
-
P(s)
:将信号量的值减一(s.value - 1
)- 如果结果小于0(
s.value < 0
),执行P操作的进程将被阻塞,放入到与s信号量有关的list所指向的队列中 - 如果结果大于0(
s.value>=0
),则执行P操作的进程继续执行。
- 如果结果小于0(
-
V(s)
:将信号量的值加一(s.value+1
)- 若结果不大于0(
s.value <= 0
),则执行V操作的进程从与信号量有关的list所指向的队列中释放一个进程,使其转于就绪态,自己继续执行; - 若结果大于0(
s.value>0
),则执行V操作的进程继续执行。
PV操作的推论:
- 若信号量
s.value
为正值,此值等于在封锁之前对信号量s可施行的P操作数,即s
所代表的实际可用的物理资源数。 - 若信号量
s.value
为负值,其绝对值等于登记排列在s信号量队列之中等待的进程个数,即恰好等于对信号量s
实施P操作而被封锁的并进入信号量s
等待队列的进程数 - 通常P操作意味着请求一个资源,V操作意味着释放一个资源,在一定条件下,P操作代表挂起进程的操作,V操作代表被唤醒被挂起的进程的操作。
- 若结果不大于0(
信号量
在操作系统中将用信号量来表示物理资源的实体,与队列有关。同时,须知:
信号量是一种变量类型,有如下两个分量:
- 信号量的值(value)
- value>0,表示实际可用资源数
- value=0,表示资源为0
- value<0,表示正在等待资源的进程数
- 信号量的指针:用于指向等待的进程
按用途来分,信号量分为两种:
- 公用信号量:
- 联系一组并发进程,相关进程可以在此信号量上做PV操作,初值为1,这个为1,是为了实现进程的一个互斥;
- 私有信号量:
- 联系一组并发进程,仅允许此信号量所拥有的进程执行P操作,其他相关的进程可实施V操作,初值一般为0或正整数,在进程同步中常用。
按取值来分,又有如下两种:
- 二值信号量
- 仅能取值为0或1,解决进程互斥
- 一般信号量
- 允许取值大于1,常用于解决进程同步。
信号量的数据结构以及PV操作的细节
typedef struct semaphore
{
int value; //信号量的值
struct pcb * list;//信号量的队列指针
}
void P(semaphore s){
s.value --;
if(s.value < 0) // 小于0 被阻塞
sleep(s.list);
}
void V(semaphore s){
s.value ++;
if(s.value <= 0)
wakeup(s.list); //小于等于0 从信号量队列中释放一个等待的进程,并转为就绪态
}
信号量实现互斥
通过使用信号量和PV操作来管理并发进程进入临界区的形式:
semaphore mutex;
mutex=1;
cobegin
process Pi(){
P(mutex);
/**临界区*/
V(mutex);
}
当有进程在临界区中时,mutex的值为0或者负值,否则mutex为1.因为只能有一个进程的P操作能把mutex的值减0,从而能够保证互斥操作
实际应用(C语言实现)
现在有以下场景:
有一家人,小明专门吃苹果,小红专门吃橘子,爸爸放苹果,妈妈放橘子,盘子上面只能有一个水果,而只有相对应的水果孩子们(孩子专门吃其对应的水果)才能拿来吃,只有当盘子空的时候,爸爸妈妈才能放水果,试用进程并发实现这个操作。
首先要思考的是,临界资源是什么?显然,是盘子,盘子有多少种状态?三种!分别是:
- 盘子是空的
- 盘子上有橘子
- 盘子上有苹果
当有多个进程并发时,我们要保证资源区在每时每刻都能保证:互斥使用、有空让进、忙则等待、有限等待、择一而入,算法可行。下面我将使用PV操作实现这个操作:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include<semaphore.h>
#define P sem_wait
#define V sem_post
#define orange &orange_
#define apple &apple_
#define empty &empty_real1
sem_t orange_;
sem_t apple_;
sem_t empty_real1;
int num=0;
void * mother(){
while(num < 30)
{
P(empty);
num++;
printf("妈妈放橘子 %d\n",num);
V(orange);
}
}
void * father()
{
while(num < 30)
{
P(empty);
num++;
printf("爸爸放苹果 %d\n",num);
V(apple);
}
}
void * xiaoming()
{
while(num < 30)
{
P(apple);
num++;
printf("小明吃苹果 %d\n",num);
V(empty);
}
}
void * xiaohong(){
while(num < 30)
{
P(orange);
num++;
printf("小红吃橘子 %d\n",num);
V(empty);
}
}
int main()
{
sem_init(apple,0,0); //初始化信号量的值为0,并且设定其为进程内的线程共享
sem_init(orange,0,0);
sem_init(empty,0,1); //初始信号量的值为1,表示刚开始的时候盘子是空的,资源可用
pthread_t tid1;
pthread_t tid2;
pthread_t tid3;
pthread_t tid4;
pthread_create(&tid1,NULL,mother,NULL);
pthread_create(&tid2,NULL,father,NULL);
pthread_create(&tid3,NULL,xiaoming,NULL);
pthread_create(&tid4,NULL,xiaohong,NULL);
pthread_exit(0);
return 0;
}
以上代码的执行结果:
这里可能大家看到为啥num
的值为啥会大于30,这时思考以下num
的范围,并且在线程等待的时候,是否num
值已经被判断过了了?即可得知答案。
好了相关内容就分享到这里,如果你觉得有用的话可以给我点个赞,谢谢!