信号量机制
信号量(Semaphore)机制是一种卓有成效的进程同步工具。
整型信号量
把整型信号量定义为一个表示资源数目的整型量S
,除初始化外,仅能通过两个标准的原子操作wait(S)
和signal(S)
来访问。
wait(S)和signal(S)操作可以描述为:
wait(S): while S <= 0 do no-op;
S:=S-1;
signal(S): S:=S+1;
wait(S)和signal(S)是两个原子操作,它们在执行时是不可中断的。当一个进程在修改某信号量时,没有其他进程能同时对该信号量进行修改。
在整型信号量机制中的wait操作中,只要是信号量S<=0,就会不断地测试。因此该机制没有遵循“让权等待”准则,而是使进程处于“忙等”的状态。
记录型信号量
记录型信号量机制采取了“让权等待”策略,是一种不存在“忙等”现象的进程同步机制。记录型信号量时由于它采用了记录型数据结果而得名的。在信号量机制中,除了需要一个用于代表资源数目数的整型变量value
外,还需要一个进程链表指针L
用于链接所有等待的进程。
value
和L
两个数据项可以描述为:
type semaphore=record
value: integer;
L: list of process;
end
记录型信号量的wait(S)和signal(S)操作可以描述为:
procedure wait(S)
var S: semaphore;
begin
S.value:=S.value-1;
if S.value < 0 then block(S.L);
end
procedure signal(S)
var S: semaphore;
begin
S.value:=S.value+1;
if S.value<=0 then wakeup(S.L);
end
S.value
的初值表示系统中某类资源的数目,被称为资源信号量。
对它每次wait操作,意味进程请求一个单位的该类资源,使得系统中可分配的该类资源数减少一个,因此描述为S.value:=S.value-1
;当S.value<0时,表示资源分配完毕,因此该访问进程应调用block原语进行自我阻塞放弃处理机,并插入到信号量链表S.L
中,此时S.value
的绝对值表示该信号量链表中已阻塞进程的数目。可见该机制遵循了“让权等待”准则。
对信号量的每次signal操作表示执行进程释放一个单位资源,使得系统中可供分配的该类资源数增加一个,因此表示为S.value:=S.value+1
。如果+1后仍然是S.value<=0,说明信号量链表中仍然有等待该资源的进程被阻塞,因此还应调用wakeup原语将S.L链表中的第一个等待进程唤醒。
如果S.value的初值为1,表示允许一个进程访问临界资源,此时的信号量转化为互斥信号量用于进程互斥。
利用记录型信号量解决生产者—消费者问题
设在生产者和消费者之间的公用缓冲池中有n个缓冲区,这时可以利用互斥信号量mutex
实现各个进程对缓冲池的互斥作用。利用信号量empty
和full
表示空缓冲区和满缓冲区的数量。假定生产者和消费者相互等效,只要缓冲池未满,生产者就可以将消息送入缓冲池;只要缓冲池为空,消费者就可以从中取走一个信息。
描述如下:
Var mutex, empty, full: semaphore:=1,n,0;
buffer:array[0,...,n-1] of item;
in, out: integer:=0, 0;
begin
parbegin
proceducer: begin
repeat
...
producer an item nextp;
...
wait(empty);
wait(mutex);
buffer(in):=nextp;
in:=(in+1) mod n;
signal(mutex);
signal(full);
until false;
end
consumer: begin
repeat
wait(full);
wait(mutex);
nextc:=buffer(out);
out:=(out+1) mod n;
signal(mutex);
signal(empty);
consumer the item in nextc;
until false;
end
parend
end
需要注意的是,在每个程序中用于实现互斥的wait(mutex)和signal(mutex)必须成对出现;对资源信号量empty和full的wait和signal操作,同样需要成对出现,但它们处在不同的程序中。
如果两个wait操作互换位置,即wait(empty)和wait(metex)互换位置,或wai(full)和wait(mutex)互换位置,都可能因此死锁。若signal(full)和signal(mutex)互换位置或者signal(empty)和signal(mutex)互换位置,则不会引起死锁,只会改变临界资源的释放次序。
利用记录型信号解决哲学家进餐问题
在该问题中,放在桌子上的筷子是临界资源,在一段时间内只允许一位哲学家使用。为了实现筷子的互斥使用,可以使用一个信号量表示一只筷子,这五个信号量构成信号数组,描述如下:
Var chopstick: array[0, ..., 4] of semaphore;
所有的信号量都被初始化为1,第i位哲学家的活动可以描述为:
repeat
wait(chopstick[i]);
wait(chopstick[(i+1) mod 5]);
...
eat;
...
signal(chopstick[i]);
signal(chopstick[(i+1) mod 5]);
...
think;
until false;
当哲学家饥饿时,总是先去拿他左边的筷子wait(chopstick[i]),再去拿它右边的筷子wait(chopstick[(i+1) mod 5]),都成功后便可进餐。用餐后,先放下左边的筷子signal(chopstick[i]),再放下右边的筷子signal(chopstick[(i+1) mod 5])。这种解法可以保证不会有两个相邻的哲学家同时进餐,但是可能会造成死锁。如果五位哲学家同时饥饿而各自拿起左边的筷子时,就会使5个信号量chopstick都为0,当他们试图去拿起右边的筷子时,都将因为没有筷子可拿而无线等待。
死锁问题可以采取下面几个办法:
(1) 最多允许有4位哲学家去同时拿左边的筷子,这样能保证最后至少有一个哲学家能有进餐并且能在用餐后释放出他使用的两只筷子从而使更多的哲学家能进餐。
(2) 仅当哲学家的左右两只筷子均可使用时,才允许他拿。
(3) 规定奇数号哲学家先拿他左边的筷子,然后再拿右边的筷子,偶数号的哲学家相反。
利用记录型信号量解决读者—写者问题
所谓“读者—写者问题(Reader-Writer Problem)”是指保证一个Writer进程必须与其他进程互斥地访问共享对象的同步问题。不允许一个Writer进程和其他Reader进程或Writer进程同时访问共享对象,因为这种访问将会引起混乱。
为了实现Reader与Writer进程在读或者写时的互斥,设置一个互斥信号量Wmutex。设置一个整型变量Readcount表示正在读的进程数。只要有一个Reader进程在读,便不允许Writer进程去写。因此,仅当Readcount=0,表示尚无Reader进程在读时,Reader进程才需要执行Wait(Wmutex)操作;仅当Reader进程执行了Readcount-1操作后其值为0时,才进行signal(Wmutex)操作。Readcount又是一个被多个Reader进程访问的临界资源,因此也应该为他设置一个互斥信号量rmutex。
读者—写者问题可以描述如下:
Var rmutex, wmutex: semaphore:=1,1;
Readcount: integer:=0;
begin
parbegin
Reader: begin
repeat
wait(rmutex);
if readcount=0 then wait(wmutex);
Readcount:=Readcount+1;
signal(rmutex);
...
perform read operation;
wait(rmutex);
readcount:=readcount-1;
if readcount=0 then signal(wmutex);
signal(rmutex);
until false;
end
Writer: begin
repeat
wait(wmutex);
perform write operation;
signal(wmutex);
until false;
end
parend
end