同步和同步机制
一个进程在某一特殊点上被迫停止执行直到接收到一个对应的特殊变量值,通过特殊变量这一设施,任何复杂的进程交互要求可得到满足,这种特殊变量就是信号量(semaphore)。在操作系统中,信号量用以表示物理资源的实体,它是一个与队列有关的整型变量。实现时,信号量是一种变量类型,常常用一个记录型数据结构表示,它有两个分量:一个是信号量的值,另一个是信号量队列的队列指针。
原语是操作系统中执行时不可中断的过程、即原子操作(atomicoperation)。Dijkstra 发明了两个信号量操作原语:P 操作和V 操作(荷兰语中“测试(Proberen)”和“增量(Verhogen)”的头字母),此外,还常用的符号有:wait 和signal;up 和down;sleep 和wakeup 等。利用信号量和P、V 操作既可以解决并发进程的竞争问题,又可以解决并发进程的协作问题。
信号量按其用途可分为两种:
? 公用信号量:联系一组并发进程,相关的进程均可在此信号量上执行P 和V操作。初值常常为1,用于实现进程互斥。
? 私有信号量:联系一组并发进程,仅允许此信号量拥有的进程执行P 操作,而其他相关进程可在其上施行V 操作。初值常常为0 或正整数,多用于并发进程同步。
信号量按其取值可分为两种:
? 二元信号量:仅允许取值为0 和1,主要用于解决进程互斥问题。
? 一般信号量:允许取值为非负整数,主要用于解决进程同步问题。
1、整型信号量
设 s 为一个正×××量,除初始化外,仅能通过P、V 操作来访问它,这时P 操作原语和V 操作原语定义如下:。
? P(s):当信号量s 大于0 时,把信号量s 减去l,否则调用P(s)的进程等待直到信号量s 大于0 时。
? V(s):把信号量s 加1。P(s)和V(s)可以写成:
P(s):while s≤0 do null operation
s:=s-1;
V(s): s:=s+1;
整型信号量机制中的P 操作,只要信号量s<=0,就会不断测试,进程处于“忙式等待”。后来对整型信号量进行了扩充,增加了一个等待s 信号量所代表资源的等待进程的队列,以实现让权等待,这就是下面要介绍的记录型信号量机制。
2、记录型信号量
设 s 为一个记录型数据结构,其中一个分量为×××量value,另一个分量为信号量队列queue,value 通常是一个具有非负初值的整型变量,queue 是一个初始状态为空的进程队列。这时P 操作原语和V 操作原语的定义修改如下:
? P(s):将信号量s 减去l,若结果小于0,则调用P(s)的进程被置成等待信号量s 的状态。
? V(s):将信号量s 加1,若结果不大于0,则释放一个等待信号量s 的进程。
记录型信号量和P 操作、V 操作可表示成如下的数据结构和不可中断过程:
type semaphore=record
value:integer;
queue: list of process;
end
procedure P(var s:semaphore);
begin
s.value:= s.value – 1; /* 把信号量减去1 */
if s.value< 0 then W(s.queue); /* 若信号量小于0,则执行P(s)的进程调用 W(s.queue) 进行自我封锁,被置成等待信号量s 的状态,进入信号量队列queue*/
end;
procedure V(var s:semaphore);
begin
s.value:= s.value + 1; /* 把信号量加1 */
if s.value≤ 0 then R(s.queue); /* 若信号量小于等于0,则调用R(s.queue)从信号量s 队 列queue 中释放一个等待信号量s 的进程并置成就绪态*/
end;
其中W(s.queue)表示把调用过程的进程置成等待信号量s 的状态,并链入s 信号量队列,同时释放CPU;R(s.queue)表示释放一个等待信号量s 的进程,从信号量s 队列中移出一个进程,置成就绪态并投入就绪队列。进程按照什么次序从队列中移出?公平的策略是先进先出法,被阻塞时间最久的进程最先从队列释放,该策略能保证进程不会被饿死。
3、二元信号量
设 s 为一个记录型数据结构,其中一个分量为value,它仅能取值0 和1,另一个分量为信号量队列queue,这时可以把二元(binary)信号量上的P、V 操作记为BP 和BV,其定义如下:
type binary semaphore=record
value(0,1);
queue: list of process
end;
procedure BP(var s:semaphore);
if s.value=1;
then
s.value=0;
else begin
w(s.queue);
end;
procedure BV(var s:semaphore);
if s.queue is empty;
then
s.value:=1;
else begin
R(s.queue);
end;
虽然二元信号量仅能取0 和1 值,但可以证明它与记录型信号量一样,有着同等的表达能力。下面来看一下,如何用二元信号量实现记录型信号量。
var
s1: binary-semaphore;
s2: binary-semaphore;
c:integer;
其中,初始化为s1=1,s2=0,c 被赋予记录型信号量s 所需的值,那么,在记录型信号量s 上的P、V 操作定义为:
P(s) : V(s):
begin begin
BP(s1); BP(s1);
c:=c-1; c:=c+1;
if c<0 then BP(s2); if c≤0 then BV(s2);
BV(s1); BV(s1);
end end
实际上把记录型信号量s 所需的值放到c 上,把P、V 操作中对信号量s 的增、减变成为对c 的增、减,把本来应该在s 队列上挂起的进程改为挂在s2 信号量上。信号量s1 的初值为1,用作为操作c 变量时的互斥信号量。
用记录型信号量实现互斥
记录型信号量和 P、V 操作可以用来解决进程互斥问题。与TS 指令相比较,P、V操作也是用测试信号量的办法来决定是否能进入临界区,但不同的是P、V 操作只对信号量测试一次,而用TS 指令则必须反复测试。用信号量和P、V 操作管理几个进程互斥进入临界区的一般形式如下:
Var mutex: semaphore;
mutex := 1;
cobegin
……
process Pi
begin
……
P(mutex);
临界区;
V(mutex);
……
end;
……
coend;
下面的程序用记录型信号量和P、V 操作解决了飞机票售票问题。
Var A : ARRAY[1..m] of integer;
mutex : semaphore;
mutex:= 1;
cobegin
process Pi
var Xi:integer;
begin
L1:
按旅客定票要求找到A[j];
P(mutex);
Xi := A[j];
if Xi>=1
then begin
Xi:=Xi-1;A[j]:=Xi;
V(mutex);{输出一张票};
end;
else begin
V(mutex);{输出“票已售完”};
end;
goto L1;
end;
coend.
Var A : ARRAY[1..m] of integer;
s : ARRAY[1..m] of semaphore;
s[j] := 1;
cobegin
process Pi
var Xi:integer;
begin
L1:
按旅客定票要求找到A[j];
P(s[j]);
Xi := A[j];
if Xi>=1
then begin
Xi:=Xi-1;A[j]:=Xi;
V(s[j]);{输出一张票};
end;
else begin
V(s[j]);{输出“票已售完”};
end;
goto L1;
end;
coend.
记录型信号量解决生产者-消费者问题
记录型信号量和 P、V 操作不仅可以解决进程的互斥,而且更是实现进程同步的有力工具。进程的同步是指一个进程的执行依赖于另一个进程的信号或消息,当一个进程没有得到来自于另一个进程的信号或消息时则等待,直到信号或消息到达才被唤醒。生产者和消费者问题就是一个典型的进程同步问题,出现不正确结果的原因在于它们访问缓冲器的相对速率。为了能使它们正确工作,生产者和消费者必须按一定的生产率和消费率来访问共享的缓冲器。用P、V 操作来解决生产者和消费者共享一个缓
冲器的问题,可以使用两个信号量empty 和full,它们的初值分别为1 和0,empty 指示能否向缓冲器内存放产品,full 指示是否能从缓冲器内取出产品。于是生产者和消费者问题的程序如下所示。
var B : integer;
empty:semaphore; /* 可以使用的空缓冲区数*/
full:semaphore; /* 缓冲区内可以使用的产品数*/
empty:= 1; /* 缓冲区内允许放入一件产品*/
full:= 0; /* 缓冲区内没有产品*/
cobegin
process producer
begin
L1:
Produce a product;
P(empty);
B := product;
V(full);
Goto L1;
end;
process consumer
begin
L2:
P(full);
Product:= B;
V(empty);
Consume a product;
Goto L2;
end;
coend.
要提醒注意的是P、V 操作使用不当的话,仍会出现与时间有关的错误。例如,有m 个生产者和n 个消费者,它们共享可存放k 件产品的缓冲器。为了使它们能协调的工作,必须使用一个信号量mutex(初值为1),以限制它们互斥地对缓冲器进行存取,另用两个信号量empty(初值为k)和full(初值为0),以保证生产者不往满的缓冲器中存产品,消费者不从空的缓冲器中取产品。程序如下:
var B : array[0..k-1] of item;
empty:semaphore:=k; /* 可以使用的空缓冲区数*/
full:semaphore:=0; /* 缓冲区内可以使用的产品数*/
mutex:semaphore:=1; /* 互斥信号量
in :integer:= 0; /* 放入缓冲区指针*/
out :integer:= 0; /* 取出缓冲区指针*/
cobegin
process producer_i
begin
L1:produce a product;
P(empty);
P(mutex);
B[putptr] := product;
in:=(in+1) mod k;
V(mutex);
V(full);
goto L1;
end;
process consumer_j
begin
L2:P(full);
P(mutex);
Product:= B[out];
out:=(out+1) mod k;
V(mutex);
V(empty);
consume a product;
goto L2;
end;
coend.
记录型信号量解决读者-写者问题
读者与写者问题(reader-writer problem)(Courtois,1971)也是一个经典的并发程序设计问题。有两组并发进程:读者和写者,共享一个文件F,要求:(1)允许多个读者可同时对文件执行读操作;(2)只允许一个写者往文件中写信息;(3)任一写者在完成写操作之前不允许其他读者或写者工作;(4)写者执行写操作前,应让已有的写者和读者全部退出。单纯使用信号量不能解决读者与写者问题,必须引入计数器rc 对读进程计数,mutex 是用于对计数器rc 操作的互斥信号量,W 表示是否允许写的信号量,于是管理该文件的程序可如下设计:
var rc: integer;
W,mutex: semaphore;
rc := 0; /* 读进程计数 */
W := 1;
mutex := 1;
procedure read;
begin
P(mutex);
rc := rc + 1;
if rc=1 then P(W);
V(mutex);
读文件;
P(mutex);
rc := rc - 1;
if rc = 0 then V(W);
V(mutex);
end;
procedure write;
begin
P(W);
写文件;
V(W);
end;
cobegin
process readeri;
process writerj;
coend.
process readeri;
begin
read;
end.
process writerj
begin
write;
end.