初步接触,若有不足之处,请各位不吝赐教。谢谢!
首先介绍:信号量
信号量包括整型信号量、结构型信号量、二值信号量。
①
整型信号量
最初,将信号量定义为一个共享的整型量,它保存可供使用的唤醒数目。如果信号量的值为0,表示没有保存唤醒;如果它的值大于0,表示有一个或多个保留的唤醒。
对信号量的操作有以下限制:
1、信号量可以初始化为一个非负值。
2、只能由P和V两个操作来访问信号量。
P操作最初源于荷兰语proberen,表示测试;V操作源于荷兰语verhogen,表示增加。请读者注意,在有些书上将P操作称作wait或者DOWN操作,将V操作称作signal或者UP操作。
P和V操作定义的伪代码形式如下:
P(S)
//测试信号量S的值是否大于0,若是,则S的值减1,程序向下执行,如果不大于0,则循环测试。
{
while (S <= 0);//不执行任何操作
S--;
}
V(S)//只是简单地把S的值加1
{
S++;
}
P和V操作都是原语,即单个的、不可分割的原子操作。
(原语:是机器指令的延伸,往往是为完成某些特定的功能而编制的一段程序,它在执行时不可分割、不可中断。即一个操作中的所有动作要么全做,要么全不做。执行原语时要屏蔽中断,以保证其操作的不可分割性。)
一般使用方式是:当多个进程互斥进入临界区时,需要设置一个信号量mutex,其初值为1,这些进程进入、使用和退出临界区的构造形式是一样的。下面是其中任一进程Pi利用信号量实现互斥的伪代码形式:
do
{
P(mutex);
临界区
V(mutex);
其他代码区
} while (1);
主要缺点:忙式等待问题:当一个进程处于临界区时,其它试图进入临界区的进程必须在入口处持续进行测试,很显然,这种循环测试、等待进入的方式在单CPU多到程序系统中存在很大问题,因为忙式等待要消耗CPU的时间,即使其它进程想用CPU做有效工作,也无法实现。这种类型的信号量也称“转锁”(Spinlock),因为当进程等待该锁打开时要“原地转圈”。然而,在多处理器系统中转锁仍得到应用。
②
结构型信号量
又称为记录型信号量、计数信号量,一般是由两个成员组成的数据结构。其中一个成员是整型变量,表示该信号量的值另一个是指向PCB(进程控制块,进程存在的唯一标识)的指针。当多个进程都等待同一个信号量时,它们就排成一个队列,由信号量的指针项指示该队列的队首,而PCB队列是通过PCB自身所包含的指针项进行链接的。最后一个PCB(即队尾)的链接指针为0。
可以将信号量定义为如下所示的一个C语言结构:
typrdef struct
{
int value;
struct PCB *list;
}semaphore;
信号量的值与相应资源的使用情况有关。当它的值大于0时,表示当前可用资源的数量
当它的值小于0时其绝对值表示等待使用该资源的进程个数,即在该信号量队列上排队的PCB的个数。
对信号量的操作有如下严格的限制:
1、信号量可以赋初值,且初值为非负数。信号量的初值可由系统根据资源使用情况和使用需要来确定。在初始条件下,信号量的指针项可以置为0,表示队列为空。
2、在使用过程中信号量的值可以修改,但只能由P和V操作访问,不允许通过其他方式查看或操纵信号量。
设信号量为S,对S的P操作记为P(S),对S的V操作记为V(S)。
P、V操作的定义分别如下:
void P(semaphore S)
{
S.value--;//信号量的值S.value减1
if (S.value < 0)
//如果其值大于0,则该进程继续运行;如果其值小于0,则把该进程的状态置为阻塞,把相应的PCB链入该信号量队列的队尾,
//放弃处理机,进行等待(直至其它进程在S上执行V操作,把它释放出来为止)
{
把这个进程加到S.list队列;
block();//block操作挂起调用它的进程
}
}
void S(semaphore)
{
S.value++;//信号量的值S.value加1
if (S.value <= 0)
// 如果其值大于0,则该进程继续运行;如果其值小于等于0,则释放信号量队列上的第一个PCB(即信号量指针项所指向的PCB)
// 所对应的进程Q(把阻塞状态改为就绪状态),执行V操作的进程继续运行。
{
从S.list队列中移走进程Q;
wakeup(Q);//wakeup(Q)操作恢复被阻塞进程Q的执行
}
}
block()操作和wakeup(Q)操作被操作系统作为基本系统调用。
在具体实现时应注意P,V操作都是原语。
上述阻塞状态进程链入相应队列末尾,以及将队列的队首进程从阻塞队列中移至就绪队列中采用的方法是FIFO(即先进先出)策略。然而,具体实现并不限于此,管理队列可以采用任何一种链接策略。信号量使用是否正确与信号量队列的排队策略无关。
③
二值信号量
它是上述信号量的一种特例,它的值只能在0和1之间选择。依赖于底层的硬件体系结构,二值信号量比计数信号量更容易实现二值信号量类似互斥锁。
二值信号量及其操作的定义用C语言描述如下:
typedef struct
{
enum{false,true}value;//枚举量
struct PCB *list;
}B_semaphore;
void P_B(B_semaphore S)
{
if (S.value == true)
S.value = false;
else
{
把该进程放入S.list队列;
block();
}
}
void V_B(B_semaphore S)
{
if (S.list == NULL)
S.value = true;
else
{
从S.list队列中移走进程Q;
wakeup(Q);
}
}
设S是一个计数信号量,下面用二值信号量来实现它首先要定义如下数据结构:
B_semaphore S1, S2;
int c;
//对S1、S2初始化
S1.value=true;//true的值等于1
S2.value=false;//false的值等于0
并且整数c的值被置为计数信号量S的初值。计数信号量S上P、V操作的实现过程如下:
P操作:
P_B(S1);
c--;
if (c < 0)
{
V_B(S1);
P_B(S2);
}
else V_B(S1);
V操作:
P_B(S1);
c++;
if (c <= 0)
V_B(S2);
V_B(S1);
信号量的一般应用:**利用信号量(以下均指计数信号量)机制可以解决并发进程的互斥和同步问题。
一、用信号量实现进程互斥
利用信号量实现互斥的一般模型是:
进程P1 进程P2 进程P3
... ... ...
P(mutex); P(mutex); P(mutex);
临界区 临界区 临界区
V(mutex); V(mutex); V(mutex);
... ... ...
其中,信号量mutex用于互斥,初值为1。
使用P、V操作实现互斥时应注意两点:
1、在每个程序中用于实现互斥的P(mutex)和·V(mutex)必须成对出现,即先做P,进入临界区;后做V,退出临界区。
2、互斥信号量mutex的初值一般为1。
二、用信号量实现进程简单同步
在代码中P、V操作出现的顺序与信号量的初值设置有关。
④
下面我将分析一道题,以便进一步理解P、V操作。
某寺庙,有小和尚、老和尚若干。有一水缸,由小和尚提水入缸,老和尚从缸中取水饮用。水缸可容纳10桶水,水取自同一水井中,水井径窄,每次只能容一个水桶取水。水桶总数为3个,每次入、取缸水仅为1桶,且不可同时进行。试给出取水、入水的算法描述。
看题过程把重点信息找出来(即找出资源),然后开始进一步捋清流程。
小和尚打水入缸流程:
拿桶--------->去水井取水(互斥,P、V操作)--------->把水倒入水缸(互斥,P、V操作)--------->放桶
老和尚取水饮用流程:
拿桶--------->去水缸取水(互斥,P、V操作)--------->放桶
semaphore mutex_well = 1, mutex_vat = 1;//互斥量
semaphore pail = 3,empty = 10, full = 0;//定义自然量,empty表示水缸的总容量,full表示满的标志
project small()//小和尚
{
while (true)
{
P(empty);//判断水缸是否还有容量,有则减一,程序向下执行
P(pail);//申请一个桶
P(mutex_well);//占用水井
从水井中打水;//活动
V(mutex_well);//用完水井,释放资源
P(mutex_vat);//占用水缸
将水倒入水缸中;//活动
V(mutex_vat);//释放资源
V(pail);//放桶
V(full);//full+1,注意成对出现问题!!!
}
}
project old()//老和尚
{
while (true)
{
P(full);//看是否有水,有则减一,程序向下执行
P(pail);//拿桶
P(mutex_vat);//占用水缸
从水缸中取水;//活动
V(mutex_vat);//用完水缸,释放资源
喝水;//此处虽可省,但有这一步更为具体形象
V(pail);//放桶
V(empty);//容量加一
}
}
⑤
实例部分
很多经典进程同步问题,如生产者-消费者问题、读者-写者问题、哲学家进餐问题和打瞌睡的理发师问题等都是进程同步和互斥的一般化形式,同样可用信号量解决。
⑤_1、 生产者-消费者问题:
1、 定义:针对某类资源抽象地看,如果一个进程能产生并释放资源,则该进程称作生产者;如果一个进程单纯使用(消耗)资源,则该进程称作消费者。
2、 因此,生产者-消费者问题是同步问题的一种抽象,是进程同步、互斥关系方面的一个典型。
3、 这个问题可以表述如下:一组生产者进程和一组消费者进程(设每组有多个进程)通过缓冲区发生联系。生产者进程将生产的产品(数据、消息等统称为产品)送入缓冲区,消费者进程从中取出产品。
4、分析:假定缓冲区共有N个,不妨把它们设想成一个环形缓冲地,如下图所示:
其中,有斜线的部分表示该缓冲区中放有产品,否则为空。in表示生产者下次存入产品的单元,out表示消费者下次取出产品的单元。
为使这两类进程协调工作,防止盲目的生产和消费,他们应该满足如下同步条件:
一、任一时刻所有生产者存放产品的单元数不能超过缓冲区的总容量(N)。
二、所有消费者取出产品的总量不能超过所有生产者当前生产产品的数量。
设缓冲区的编号为0~N-1,in和out分别是生产者进程和消费者进程使用的指针,指向下面可用的缓冲区,初值都是0。
为使两类进程实行同步操作,应设置三个信号量:两个计数信号量full和empty,一个互斥信号量mutex。
各个信号量含义:
full:表示放有产品的缓冲区数,其初值为0.
empty:表示可供使用的缓冲区数,其初值为0。
mutex:互斥信号量,初值为1,表示各进程互斥进入临界区,保证任何时候只有一个进程使用缓冲区。
下面是解决这个问题的算法描述:
生产者进程Producer:
while (TRUE)
{
P(enpty);//若empty大于0,则表示有可使用的缓冲区,empty减1,程序向下执行
P(mutex);//互斥信号量,占用资源,
产品送往buffer(in);//活动
in = (in + 1)mod N;//以N为模 环形,遂取模计算生产者下次存入产品的单元
V(mutex);//释放资源
V(full);//full加1,即放有产品的缓冲区数增加1
}
while (TRUE)
{
P(full);//若full大于0,则表示有放着产品的缓冲区,full减1,程序向下执行
R(mutex);//互斥信号量,占用资源
从buffer(out)中取出产品;//活动
out = (out + 1)mod N;//以N为模 环形,取模计算消费者下次取出产品的单元
V(mutex);//释放资源
V(empty);//empty加1,即可供使用的缓冲区数加1
}
在生产者-消费者问题中应注意下面三点:
一、在每个程序中必须有先做P(mutex),后做V(mutex),二者要成对出现。夹在二者中间的代码就是该进程的临界区。
二、对同步信号量full和empty的P、V操作同样必须成对出现,但它们分别位于不同的程序中。
三、无论在生产者进程中还是消费者进程中,两个P操作的次序不能颠倒:应先执行同步信号量的P操作,然后执行互斥信号量的P操作。否则可能造成进程死锁。
(死锁:是指在一个进程集合中的每个进程都在等待仅由该集合中的另一个进程才能引发的事件而无限期地僵持下去的局面。 产生死锁的根本原因:资源有限且操作不当。)
⑤_2、读者-写者问题:
1、读者-写者问题也是一个著名的进程互斥访问有限资源的同步问题。
2、例如,一个航班预定系统有一个大型数据库,很多竞争进程要对它进行读、写。允许许多个进程同时读该数据库,但是在任何时候如果有一个进程写(即修改)数据库,那么就不允许其它进程访问它——既不允许写,也不允许读。
3、分析:很显然,系统中读者(进程)和写者(进程)各有多个,各个读者的执行过程基本相同。同样,各个写者的执行过程也基本相同。
设置两个信号量:读互斥信号量rmutex和写互斥信号量wmutex。另外设立一个读计数器readcount,它是一个整型变量,初值为0。
各个信号量具体含义:
rmutex:用于读者互斥地访问readcount,初值为1。
wmutex:用于保证一个写者与其他读者/写者互斥地访问共享资源,初值为1。
下面是解决这个问题的一种算法:
读者Readers:
while (TRUE)
{
P(rmutex);
readcount = readcount + 1;
if (readcount == 1)
P(wmutex);
V(rmutex);
执行读操作;
P(rmutex);
readcount = readcount - 1;
if (readcount == 0)
V(wmutex);
V(rmutex);
使用读取的数据;
}
写者Writers:
while (TRUE)
{
P(wmutex);
执行写操作;
V(wmutex);
}
在这个算法中,仅第一个访问数据库的读者才对信号量wmutex执行P操作,后续的读者只是增加readcount的值。当读者完成读操作后,减少readcount的值。最后一个离开的读者对wmutex执行V操作。如果有写者在等待,则唤醒它。
这个算法隐含读者的优先级高于写者。当若干读者正使用数据库时,如果出现一个写者,它必须等待。即写者必须一直等到最后一个读者离开数据库,才得以执行。(修改算法使写者的优先权高于读者,在尝试中…)
⑤_3、哲学家进餐问题:
1、问题描述:五位哲学家围坐在一张圆桌旁进餐,每人面前有一只碗,各碗之间分别有一根筷子。每位哲学家在用两根筷子夹面条吃饭前独自进行思考,感到饥饿时便试图占用其左、右最靠近他的筷子,但他可能一根也拿不到。他不能强行从邻座手中拿过筷子,而且必须用两根筷子进餐;餐毕,要把筷子放回原处并继续思考问题,如下图所示:
2、分析:简单的解决方案是:用一个信号量表示一根筷子,五个信号量构成信号量数组chopstick[5],所有信号量初值为1。第i个哲学家的进餐过程可描述如下:
while (TRUE)
{
思考问题
P(chopstick[i]);
P(chopstick[(i + 1)mod 5]);
进餐
V(chopstivk[i]);
V(chopstivk[(i + 1)mod 5]);
}
上述算法可保证两个相邻的哲学家不可能同时进餐,但不能防止五位哲学家同时拿起各自左边的筷子、又试图去拿右边的筷子,这样子会引起他们都无法进餐而无限期等待下去的状况即发生了死锁。
针对这种情况,解决死锁的方法有以下几种:
(1)最多只允许4个哲学家同时拿筷子,从而保证有一人能够进餐。
(2)仅当某哲学家面前的左、右两支筷子均可用时,才允许他拿起筷子。
(3)奇数号哲学家先拿左边的筷子,偶数号的先拿右边的筷子。
3、其中方法(1)最为简单,下面给出其算法(C语言)描述。程序中使用了一个信号量数组chopstick[5],对应5根筷子,各元素初值均为1;将允许同时拿筷子准备进餐的哲学家数量看作一种资源,定义成信号量count,初值为4;哲学家进餐前先执行P(count),进餐后执行V(count)。
typedef struct
{//定义结构型信号量
int value;
struct PCB *list;
}semaspore;
semaspore chopstick[5] = { {1},{1},{1},{1},{1} };//信号量数组初始化
semaspore count = { 4 };//允许同时拿筷子进餐的人数
int I;//第几位哲学家
第I个哲学家进程Process I:
while (TRUE)
{
Think;//哲学家在思考哲学家
P(count);
P(chopstick[I]);//试图拿左边筷子
P(chopstick[(I + 1)mod 5]);//试图拿右边筷子
Eat;
V(chopstick[I]);//左边筷子放回原处
V(chopstick[(I + 1)mod 5]);//右边筷子放回原处
V(count);
}
上面是用C语言描述的算法,并不是完整的程序。
⑤_4、打瞌睡的理发师问题:
1、描述:理发店有一名理发师,还有一把理发椅和几把座椅,等待理发者可坐在上面。如果没有顾客到来,理发师就坐在理发椅上打盹。当顾客到来时,就唤醒理发师。如果顾客到来时理发师正在理发,该顾客就坐在椅子上排队;如果满座了,他就离开这个理发店,到别处去理发,如下图所示:
2、要求:为理发师和顾客各编写一段程序,描述他们的行为,并且利用信号量机制保证上述过程的实现。
3、分析:理发师和每位顾客都分别是一个进程。
3.1、理发师开始工作时,先看一看店内有无顾客:如果没有,他就在理发椅上打瞌睡;如果有顾客,他就为等待时间最久的顾客服务,且等待人数减1。
3.2、每位顾客进程开始执行时,先看店内有无空位:如果没有空位,就不等了,离开理发店;若有空位则排队,等待人数加1;如果理发师在睡眠,则唤醒他工作。
可见,理发师进程和顾客进程需要协调工作。另外,要对等待人数进行操作,所以对表示等待人数的变量waiting要互斥操作。
4、设立三个信号量:
customers:用来记录等候理发的顾客数(不包括正在理发的顾客),初值为0。
barbers:等候顾客的理发师数,初值为0。
mutex:用于对waiting变量的互斥操作。
还需设立一个计数变量waiting,表示正等候理发的顾客人数,初值为0。实际上,waiting是customers的副本。但它不是信号量,所以可在程序中对它进行增减等操作。另外,设顾客座椅数(CHAIRS)为5。
下面是解决这个问题的一种算法:
#define CHAIRS 5
typedef struct
{
int value;
struct PCB *list;
}semaspore;
semaspore customers = { 0 };
semaspore barbers = { 0 };
semaspore mutex = { 1 };
int waiting = 0;
void barber(void)
{
while (TRUE)
{
P(customers);//如果没有顾客,则理发师打瞌睡
P(mutex);//互斥进入临界区
waiting--;
V(barbers);//一个理发师准备理发
V(mutex);//退出临界区
cut hair();//理发(在临界区之外)
}
}
void customer(void)
{
P(mutex);//互斥进入临界区
if (waiting < CHAIRS)
{
waiting++;
V(customers);//若有必要,唤醒理发师
V(mutex);//退出临界区
P(barbers);//如果理发师正忙着,则顾客打瞌睡
get haircut();
}
else
V(mutex);//店里人满了,不等了,离开理发店
}
5、理发师为一名顾客理发之后,要查看还有无等候的顾客。当没有等候的顾客时,理发师才能打瞌睡。所以在程序中要使用while语句,保证理发师循环地为下一位顾客服务。每位顾客在理完发之后就离开理发店,不会重复理发。
⑥
使用信号量的几点提示
从上面4个经典进程同步问题的算法中可以看出,用信号量和P、V操作可以实现进程互斥或同步,但在编写具体算法时往往难度较大,这涉及进程和信号量的设定以及P、V操作的灵活使用等。下面给出解决此类问题的一般方式及应注意要点,供读者参考。
(1)根据问题给出的条件,确定进程有几个或几类;
(2)确定进程间的制约关系是互斥还是同步,确定信号量种类。对每一个共享资源都要设立信号量:互斥时对一个共享资源设立一个信号量;同步时对一个共享资源可能要设立两个或多个信号量,这需要根据由几个进程来使用该共享变量而定;
(3)各相关进程间通过什么信号量实现彼此的制约,标明信号量的含义和初值。信号量的初值与相应资源的数量有关,也与P、V操作在程序代码中出现的位置有关;
(4)编写相应进程的代码段。应先确定该进程要完成的主要任务是什么,做该任务前后用到什么共享资源,要用相应信号量及其P、V操作保证任务的正常进行;
(5)同一信号量的P、V操作要“成对”出现,P、V操作的位置一定要正确。互斥时对同一信号量在临界区前作P操作,临界区后作V操作;同步时则在不同进程间实现同一信号量的P、V成对操作;对复杂的进程同步问题,P、V操作可能会嵌套,一般同步的P、V操作在外,互斥的P、V操作在内;
(6)验证代码的正确性。设以不同的次序调度运行各进程,查看是否能保证问题的圆满解决,切忌按固定顺序查验各进程是否正常执行。
总结:
利用信号量和P、V操作可以很好地解决进程间的互斥和同步问题。从物理概念上讲,信号量表示系统中某类资源的数目,其值大于0时,表示系统中当前可用的资源数目;其值小于0时,其绝对值表示系统中因请求该类资源而被封锁的进程数目。P操作意味着请求系统分配一个单位资源,而V操作意味着释放一个单位资源。
初次接触整理出来的,算是比较具体详细的笔记。实例部分有的也还不是特别理解,慢慢理解ing。如有错误欢迎纠正,谢谢!
和尚打水问题具体代码实现请点击–>火箭,送你直达!
有帮助?或者–>开启传送大阵 !希望其他的分享也对大家有所帮助哈!