typedef struct {
int value; //信号量值
struct process_cntrol_block *list; //阻塞队列
}semaphore;
在应用信号量的时候,信号量的值往往是临界资源的数量。当临界资源数量不足时,新的进程就阻塞,并插入到信号量阻塞队列中。
wait(S)和signal(S)操作是信号量机制的基本操作(通常称作P,V操作),定义如下:
wait(semaphore *S) {
S->value--; //信号量的值减一
if(S->value < 0) block(S->list); //如果信号量的值小于零,进程阻塞
signal(semaphore *S) {
S->value++; //信号量的值加一
if(S->value <= 0) wakeup(S->list); //唤醒
}
在实现进程同步时,一般先将S->value的初值设定为临界资源的初始数量,一旦进程要请求一个临界资源,先对代表此进程的信号量执行P操作,也就意味着资源数减一,当该进程运行完毕时,执行V操作,意味着资源数加一。值得注意的是,如果当前资源数量为0,再有进程想请求临界资源,就会在执行P操作时进入阻塞队列,信号量值变为-1,此后执行P操作的进程也都会被阻塞,信号量的绝对值为阻塞的进程数目。
有了P,V操作,我们就能很简单的实现进程互斥。方法是:设mutex为互斥信号量,初值为1。在需要互斥的临界区前后分别使用P,V操作即可,示意代码如下:
semaphore mutex = 1; //一般初值为1的信号量用来实现互斥
P_A() { //进程A
while(1) {
P(mutex);
临界区;
V(mutex);
剩余区;
}
}
P_B() { //进程B
while(1) {
P(mutex);
临界区;
V(mutex);
剩余区;
}
}
在互斥问题中,我们可以把P操作理解成“上锁”,把V操作理解成“解锁”,当P_A和P_B两个进程并发执行时,无论哪一个进程先执行到了P操作,都会接下来执行P操作的进程阻塞,直到前一个进程执行到了V操作为止,这样就实现了临界区的互斥。
思路如下:为每一组想要实现前后关系的进程,都分别定义一个信号量,初值设为0,把你想要先执行的进程后面加一个V操作,想要后执行的进程前面加一个P操作。这样一来,你想要后执行的进程如果先执行了,就会因为执行了其前面的P操作而阻塞,直到你想要先执行的进程执行完了,执行V操作后,才能解除阻塞继续执行。以上就是用信号量实现前驱关系的过程。
假如我们要实现四个语句S1,S2,S3,S4需要按照一定的顺序同步执行,比如S1执行完S2,S3才能执行,S2,S3执行完了S4才能执行,代码如下:
p1() {S1; V(a); V(b);}
p2() {P(a); S2; V(c);}
p3() {P(b); S3; V(d);}
p4() {P(c); P(d); S4;}
main() {
semaphore a,b,c,d = 0;
cobegin
p1();p2();p3();p4();
coend
}
假定生产者和消费者之间的公用缓冲池有n个缓冲区,生产者可以向缓冲区生产一个产品,消费者可以从缓冲区消耗一个产品,注意:生产者不能同时生产,消费者也不能同时消费,也不能同时生产和消费,当缓冲区空时无法消费,当缓冲区满时无法生产。请用信号量机制解决这一问题。
解法如下:
semaphore mutex = 1; //实现生产与消费、生产与生产、消费与消费
semaphore full = 0, empty = n; //full表示已用缓冲区数,empty表示空缓冲区数
void producer() {
while(1) {
P(empty); //每次生产会减少一个空缓冲区,如果没有空缓存区了就阻塞
P(mutex); //保证互斥
生产一个产品;
V(mutex); //保证互斥
V(full); //每次生产增加一个已用缓冲区
}
}
void consumer() {
while(1) {
P(full); //每次消费会减少一个已用缓冲区,如果没有已用缓存区了就阻塞
P(mutex);
消耗一个产品;
V(mutex);
V(empty); //每次生产增加一个空缓冲区
}
}
假设一个文件可被多个进程共享,我们允许多个进程同时读这个共享对象,但是不允许一个进程写这个共享对象的同时,别的进程进行读或写。换句话说:读和读不互斥,读和写互斥,写和写互斥。请用信号量机制解决这一问题。
解法如下:
semaphore rmutex, wmutex = 1; //wmutex用以实现写进程与其它进程的互斥
int readcount = 0; //记录读进程的数量
void reader() {
while(1) {
P(rmutex); //见下文解释
if(readcount == 0) P(wmutex); //第一个读进程去把写进程上锁
readcount++;
V(rmutex); //见下文解释
读者读;
P(rmutex); //见下文解释
readcount--;
if(readcount == 0) V(wmutex); //解锁
V(rmutex); //见下文解释
}
}
void writer() {
while(1) {
P(wmutex); //实现写进程与其他进程都互斥
写者写;
V(wmutex);
}
}
这个问题的程序中有个关键点,就是明明读进程是不互斥的,为什么还需要rmutex来实现if判断的互斥呢。原因如下:当我们在进程互斥中使用条件语句和数值变化的时候,如果不把那一段也实现互斥的话,很有可能出问题,比如在这段代码中,要是不加第5行和第8行的话:如果一个读进程执行完了第6行,还没有执行第7行的readcount++操作之前,这个读进程被剥夺了处理机,换另一个读进程上处理机运行了,那么另一个读进程就会阻塞在第6行的P操作上,就无法实现多个读者一起读了;10行和13行同理,如果一个读进程执行完了第11行就被剥夺处理机,换上另一个读进程也执行了11行,这样readcount被连减两次,那么这两个进程都能通过12行的if判断,会执行两次V操作,产生错误。
一个主修人类学、辅修计算机科学的学生参加了一个课题,调查非洲狒狒是否能被教会理解死锁。他找到一处很深的峡谷,在上边固定了一根横跨峡谷的绳索,这样狒狒就可以攀住绳索越过峡谷。同一时刻可以有几只狒狒通过,只要它们朝着相同的方向。但如果向东和向西的狒狒同时攀在绳索上则将发生死锁(狒狒将被卡在中间),因为它们无法在吊在峡谷上时从另一只的背上翻过去。如果一只狒狒想越过峡谷,它必须看当前是否有别的狒狒正在逆向通过。使用信号量写一个避免死锁的程序来解决该问题。
解法如下:
semaphore wmutex, emutex, mutex = 1;
int wcount, ecount = 0; //分别记录东西狒狒上绳索的个数
void west_monkey {
while(1) {
P(wmutex); //使if语句互斥
if(wcount == 0) P(mutex); //第一个西狒狒给东狒狒上锁
wcount++;
V(wmutex); //使if语句互斥
西狒狒过桥;
P(wmutex);
wcount--;
if(wcount == 0) V(mutex);
V(wmutex);
}
}
void east_monkey { //和上面的一样
while(1) {
P(emutex);
if(ecount == 0) P(mutex);
ecount++;
V(emutex);
西狒狒过桥;
P(emutex);
ecount--;
if(ecount == 0) V(mutex);
V(emutex);
}
}
该问题属于读者写者问题的改进,如果完全理解了读者写者问题的解法,那么这个问题也能很快解决。东狒狒之间不互斥,西狒狒之间不互斥,东西狒狒之间互斥,思路是第一个东狒狒给西狒狒上锁,第一个西狒狒给东狒狒上锁,注意if判断也要实现互斥。
理发店里有一位理发师、一把理发椅和n把供等候理发的顾客坐的椅子。如果没有顾客,则理发师便在理发椅上睡觉。当一个顾客到来时,他必须先叫醒理发师,如果理发师正在理发时又有顾客来到,则如果有空椅子可坐,他们就坐下来等。如果没有空椅子,他就离开。这里的问题是为理发师和顾客各编写一段程序来描述他们的行为,要求不能带有竞争条件。
解法如下:
semaphore customer, barber = 0; //一开始没有顾客,理发师也是睡着的
semaphore mutex = 1; //互斥信号量
int empty = N; //空椅子数量为N
void Barber() {
while(1) {
P(customer); //只有顾客进程的V执行后才能执行,没有顾客就阻塞(睡觉)
P(mutex); //把数量的变化实现互斥,以免影响到顾客进程的if判断语句
empty++; //椅子上的顾客起身
V(barber); //有了一个理发师可以理发
V(mutex);
理发;
}
}
void Customer() {
P(mutex);
if(empty > 0) {
empty--; //不管是不是第一个顾客,来了先得坐凳子上,因为理发师在理发椅上睡觉呢
V(customer); //增加一个顾客,唤醒沉睡的理发师
V(mutex);
P(barber); //消耗一个理发师,没有理发师就阻塞
理发;
} else {
V(mutex);
离开;
}
}
这道题本身不难,但其中很多细节的实现需要实现,比如座位是有上限的,这样就不得不设置判断条件和计数,来使超过N个的顾客离开,而加入计数和判断条件后就又要实现其互斥,增加了问题的复杂性。
五个哲学家共用一张圆桌,分别坐在周围的五张椅子上,在桌子上有五只碗和五只筷子,他们的生活方式是交替地进行思考和进餐。平时,一个哲学家进行思考,饥饿时便试图取用其左右最靠近他的筷子,只有在他拿到两只筷子时才能进餐。进餐毕,放下筷子继续思考。请用信号量机制解决。
这是一个讲解死锁的时候的经典例子,解决这道题的直接思路如下:
semaphore chopstick[5] = {1,1,1,1,1};
void philosopher() {
while(1) {
/*当哲学家饥饿时,总是先拿左边的筷子,再拿右边的筷子*/
P(chopstick[i]);
P(chopstick[(i+1)%5]);
吃饭;
V(chopstick[i]);
V(chopstick[(i+1)%5]);
}
}
但是这样可能会出现死锁问题:如果所有哲学家都拿起了左手边筷子,五个进程就会死锁。对于避免哲学家进餐问题发生死锁的方法有很多,这里只讲一种:只有当同时一个哲学家能同时拿起左右两只筷子时,才允许拿筷子,解法如下:
semaphore chopstick[5] = {1,1,1,1,1};
semaphore mutex = 1;
void philosopher() {
while(1) {
P(mutex);
P(chopstick[i]);
P(chopstick[(i+1)%5]);
V(mutex);
吃饭;
V(chopstick[i]);
V(chopstick[(i+1)%5]);
}
}
但仅仅这样做还是有问题,如果一个哲学家获得了两只筷子,开始进餐,此时他左边或者右边的哲学家进入了临界区后,被阻塞在第6行或者第7行,那么其他的哲学家就无法进入临界区了,也就是说这个时间只能有一个哲学家进餐,但显然同一时间是可以有不相邻的两个哲学家同时进餐的。
对这个问题的更好解决方法可以去看Dijkstra在1965年给出的算法。