信号量学习笔记
为了进程间通信的竞争条件,必须阻止多个进程同时读写共享的数据。Peterson解法,TSL(test and set lock)指令和XCHG(X86 CPU中跟TSL等价的指令)能够正确的防止多个进程同时读写共享数据,但是这三个解法都有一个缺点,那就是忙等待。
忙等待显然非常的浪费CPU时间,但是比浪费CPU时间更严重的问题是忙等待会导致优先级反转问题(Priority inversion
problem)。所谓优先级反转问题,就是当低优先级的进程占用了跟高优先级进程共享的资源,此时高优先级进程就绪了,根据调度规则只要高优先级出于就
绪就可以运行它。但是低优先级进程占用了共享资源,于是高优先级进程开始忙等待,而低优先级进程得不到调度无法走出临界区释放共享资源,结果就导致高优先
级进程一直忙等待。
为了避免忙等待带来的优先级反转问题,有个办法,那就是当进程无法进入临界区获取共享资源时,不是忙等待,而是被阻塞,既sleep和wakeup。
那么sleep和wakeup是否能很好的解决问题或者没有弊端?先看经典的生产者消费者问题:
#define N 100 /*缓冲区中的槽数目*/ int count = 0; /*缓冲区中的数据项数目*/ void producer(void) { int item; while(TRUE) { item=produce_item(); if(count==N) sleep(); insert_item(item); count=count+1; if(count==1) wakeup(consumer); /*count==1说明生产之前count==0,consumer进程应该在睡眠,遂唤醒它*/ } } void consumer(void) { int item; while(TRUE) { if(count==0) sleep(); item=remove_item(); count=count-1; if(count==N-1) wakeup(producer); /*count==N-1说明消费之前缓冲区满了,生产者应该在睡眠,遂唤醒*/ consume_item(item); } }
这里有可能会出现竞争条件,因为对count的方位未加限制。有可能出现以下情况:缓冲区为空,消费者刚刚读取count的值发现它为0.此时调度程序把 CPU切换给了生产者。生产者想缓冲区中加入一个数据项,count加1,并且它推断由于刚才count为0,所以消费者一定在睡眠,所以调用 wakeup来唤醒消费者。但消费者并未睡眠,所以wakeup信号丢失。当消费者下次运行时,它将测试先前读到的count值,发现为0,于是睡眠。生 产者迟早会填满整个缓冲区,然后睡眠。最后两个进程都将永远睡眠下去。
问题的实质在于发给一个尚未睡眠的进程的wakeup信号丢失了。好了,接下来信号量登场。
信号量(semaphore)是一个用来累计唤醒次数的整型变量,一个信号量的取值可以为0或正值。信号量有down和up两种操作。
down操作是指检查其值是否大于0,若大于0,将其减1并继续;若该值为0,进程睡眠,down操作结束。特别要注意的是,信号量是一种新的变量类型,它检查数值、修改变量值以及可能发生的睡眠操作均为一个单一的、不可分割的原子操作。
up操作是指对信号量增1.如果在对一个信号量做up之前,信号量值为0,并且有一个或多个进程想对该信号量做down操作,但因为信号量值为0导致进程
睡眠,无法完成down操作,此时另外一个进程对该信号量做up操作,那么系统将选择其中一个睡眠进程允许其完成down操作,但是信号量值仍然为0,只
不过在该信号量上睡眠的进程少了一个,因为让它完成了down操作。
用信号量解决了丢失的wakeup问题。为确保信号量能正确工作,最重要的是要采用一种不可分割的方式来实现它。通常是将up和down作为系统调用实
现,而且系统只需要在执行以下操作使暂时屏蔽全部中断:测试信号量、更新信号量以及在需要时使某个进程睡眠。由于这些动作只需要几条指令,所以屏蔽中断不
会带来什么副作用。如果使用多个CPU,则每一个信号量应该由一个锁变量进行保护,通过TSL或XCHG指令来确保同一时刻只有一个CPU在对信号量进行
操作。
应该注意的是,使用TSL或XCHG忙等待另一个CPU对一个信号量的操作与使用TSL或XCHG忙等待另一个进程离开临界区是完全不同的,因为信号量的
操作就是几条指令而已,时间是固定的几个毫秒,而等待另一个进程离开缓冲区的时间则是未知的,可能任意长。
下面是一个用信号量实现的生产者消费者代码:
#define N 100 /*缓冲区中的槽数目*/ typedef int semaphore; /*信号量是一种特殊的整型数据*/ semaphore mutex = 1; /*计数缓冲区的空槽数目*/ semaphore empty = N; /*计数缓冲区的满槽数目*/ void producer(void) { int item; while(TRUE) { item=produce_item(); down(&empty); /*空槽数目减一*/ down(&mutex); /*进入临界区*/ insert_item(item); up(&mutex); /*离开临界区*/ up(&full); /*将满槽的数目加1*/ } } void consumer(void) { int item; while(TRUE) { down(&full); down(&mutex); item=remove_item(); up(&mutex); up(&empty); consume_item(item); } }