2.2.5 信号量
信号量原使用一个整型变量来累计唤醒次数。在发明者的建议下,引入了一个新的变量类型,称为信号量。一个信号量的值可以为0,表示没有积累下来的唤醒操作;或者为正值,表示有一个或多个被积累下来的唤醒操作。
两种操作:down和up。(分别为一般化后的sleep和wakeup)
对一信号量执行down操作是检查其值是否大于0。如果如此,则将其值减1,即用掉一个保存的唤醒信号,并继续。如果值为0,则进程将睡眠,而且此时down操作并未结束。检查数值、改变数值以及可能发生的睡眠操作均作为一个单一的不可分割的原子操作完成。这种原子性对于解决同步问题和避免竞争问题是非常重要的。
up操作递增信号量的值。如果一个或多个进程在该信号量上睡眠,无法完成一个先前的down操作,则由系统选择其中一个并允许其完成它的down操作。于是,对一个有进程在其上睡眠的信号量执行一次up操作之后,该信号量的值仍旧是0,但在其上睡眠的进程却少了一个。递增信号量的值和唤醒一个进程同样也是不可分割的。不会有进程因执行up操作而堵塞,正如进程不会因wakeup操作而堵塞一样。
用信号量解决生产者-消费者问题
以下代码解决了用信号量解决丢失的唤醒问题。最重要的是它采用一种不可分割的方式来实现。通常是将up和down作为系统调用实现,而且操作系统只需在执行以下操作时短暂地关掉中断,这些操作包括:检测信号量、修改信号量以及在需要时使进程睡眠。由于这些动作只需要几条指令,所以关中断不会带来什么副作用。如果使用多个CPU,则每个信号量由一个锁变量进行保护。
该解决方案使用了三个信号量:full用来记录满的缓冲槽数目,empty用来记录空的缓冲槽总数,mutex用来确保生长者和消费者不会同时访问缓冲区。
#define N 100 /*缓冲区内槽数*/
typedef int semaphore; /*信号量是一种特殊的整型变量*/
semaphore mutex = 1; /*控制对临界区的访问*/
semaphore empty = N; /*记录缓冲区内空的槽数*/
semaphore full = 0; /*记录缓冲区内满的槽数*/
void producer(void)
{
int item;
while(TRUE)
{
item = produce_item(); /*产生一个需放入缓冲区的数据项*/
down(&empty); /*递减空槽数*/
down(&mutex); /*进入临界区*/
insert_item(item); /*将一个新数据放入缓冲区*/
up(&mutex); /*离开临界区*/
up(&full); /*递增满槽数*/
}
}
void consumer(void)
{
int item;
while(TRUE)
{
down(&full); /*递减满槽数*/
down(&mutex); /*进入临界区*/
item = remove_item(); /*从缓冲区取走一个数据项*/
up(&mutex); /*离开临界区*/
up(&full); /*递增空槽数*/
consume_item(item); /*对数据项进行操作*/
}
}
2.2.6 互斥
信号量的简化版本就是互斥。互斥仅仅适用于管理共享资源或一小段代码时。
互斥是一个可以处于两个状态之一的变量:解锁和加锁。这样只需要一个二进制位来表示它,而事实上通常用一个整数表示,0表示解锁,其他值表示加锁。互斥适用于两个过程。当一个进程或线程需要进入临界区时,它调用mutex_lock,如果此时互斥是解锁的,则调用成功,调用进程可以进入临界区。
另一方面,如果该互斥已经加锁,调用者被阻塞,直到临界区中的进程完成操作并调用mutex_unlock退出为止。如果多个进程在互斥上阻塞,则随机选择一个进程并允许它获得锁。