《现代操作系统》读书笔记之——进程间通信2

    7.实现进程互斥的几种方案之——TSL指令

    前面介绍了几种方案,都是通过软件的方式实现互斥,下面的这种方式需要借助硬件设计的帮助来实现互斥。这一点在多CPU电脑的设计中尤其普遍。这种方案需要引进一条指令:

TSL RX,LOCK

    这条指令的含义是,读取内存单元LOCK中的内容到寄存器RX中,并且为内存单元LOCK重新设置一个非0值。TSL指令的操作被设计为不可分的,也就是说,整个读写操作都完成之前,其他进程是没办法访问LOCK这个内存单元的。这一点是通过锁定内存总线(lock memory bus)来实现的。

    前面我们提到了禁用中断的方式。这种锁定内存总线的方式和前者有很大的差别,前面我们提到过,禁用中断只对当前的CPU有效,对于其他CPU是无效的,因此是无法实现多CPU情况下的互斥。而锁定内存总线则不同,一旦锁定内存总线,其他CPU上的进程也无法访问LOCK内存,直到整个TSL指令执行完成。

    使用TSL指令实现互斥的方法如下面的代码所示,可能是汇编代码吧微笑

enter_region:
	TSL REGISTER,LOCK  	| 将LOCK的值复制进寄存器
	CMP REGISTER,#0		| 比较并判断REGISTER的值是不是0
	JNE enter_region	        | 如果非0,那么设置锁成功,循环返回到调用处,进入临界区
	RET
		
leave_region:				
	MOVE LOCK,#0		| 将LOCK的值设置为0
	RET			                | 返回
       其实这个方案的思路和前面的Peterson方案很类似,具体的说明都在注释里面了。当一个进程需要进入其临界区的话,执行enter_region,依然是忙等待直到这个锁是可获得的。然后,进程获取该锁。需要注意的是,和所有的使用忙等待方式的互斥解决方案一样,进程都必须在正确的时候调用enter_region和leave_region,如果有进程cheat(这个词的中文居然被javaeye设置为敏感词,我真的很汗),那这个方法是不会有用的。

    TSL指令还有一个类似的指令XCHG,这个指令能够自动交换两个地址的内容。使用XCHG的方案几乎和TSL方案一样。所欲的Intel X86CPU都使用XCHG指令提供底层同步。使用XCHG实现的代码如下:

enter_region:
	MOVE REGISTER,#1
	XCHG REGISTER,LOCK
	CMP REGISTER,#0
	JNE enter_region
	RET
	
leave_region:
	MOVE LOCK,#0
	RET
       8.休眠与唤醒

    上面提到了几种实现互斥的方案,本质上说,他们都是一种采用忙等待的方式,并且不断地检查当前条件是否允许其运行。这种方式不仅仅会浪费CPU时间,还有一个很大的问题。我们假设一个电脑有两个进程运行,一个是H,具有高的优先级,另一个是L,具有低优先级。现在假设情况是这样的:只要H处在ready状态,CPU就会调度H运行。现在假设某一时刻,阴差阳错也好,还是什么也好,L进入了临界区,但是运行到中途,时间片到期了,这时候H处在ready状态,但是由于L还处在临界区,那其实H也无法运行,而L又没有时间片。结果就是,大家就这么干耗着。这种情况被叫做优先级反转问题(Priority Inversion Problem)。

    下面看两个最基本的进程间通信的原语。sleep和wakeup,sleep是一个系统调用,他可以使调用者阻塞直到有另一个进程唤醒它,wakeup则有一个参数,就是被唤醒的进程编号。

    9.生产者-消费者问题

    作为上面的一对原语的使用例子,介绍生产者-消费者问题。或者叫做有限缓冲问题(bounded-buffer problem):两个进程共享一个大小固定的缓冲区,其中生产者(Producer)将产生信息放入缓冲区的某个单元,消费者(Consumer)则取出缓冲区中的消息。(这么问题可以演化成m个生产者、n个消费者的问题,但这里仅以1个生产者1个消费者为例)。

    如果生产者要将信息放进缓冲,而此时缓冲区已经满了,或者说消费者需要拿出信息而此时缓冲区是空的时,问题就来了。第一情况下,我们可以让生产者sleep,等消费者拿出了消息,有了空的缓存单元时,让消费者wakeup生产者。后一种情况也是类似的。为了避免出现类似于前面的打印池目录出现的问题,我们需要一个count变量,来记录当前缓冲区目前有几个消息。生产者在生产消息时,先要检测一下count是否等于缓冲区的大小N;同样,消费者在消费消息的时候,需要检测count是不是0.

    解释这个问题的代码如下:

#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);
		}
	}
}

void consumer(void) {
	int item;
	while(TRUE) {
		if(count == 0) {
			sleep();
		}
		item = remove_item();
		if(count == N - 1) {
			wakeup(producer);
		}
		consume_item(item);
	}

}
       但尽管如此,还是有可能有问题。因为对count的访问是无限制的,换句话说,不是原子性的。我们假设下面的情况。一开始,buffer是空的,消费者监测count,发现是0,这时,消费者进程时间片到期,消费者进程变成runnable状态,生产者这时候被调度运行,它检测count,发现<N,于是生产一个消息,并且将它放进buffer。现在count等于1,于是生产者调用wakeup(consumer),问题就出在这了,这时候消费者进程是处在Runnable状态,wakeup对它来说是没有作用的。接着又轮到消费者运行了,消费者之前检查过的count值为0,消费者一看,就以为buffer中还是没有消息,于是,sleep自己。最终的某个时刻,生产者最终会将buffer填满,于是自己也沉沉睡去,但是生产者等不到消费者的唤醒,因为他们两个都在sleep。

    解决这个问题的一种临时办法是设置一个wakeup waiting bit标记位。

    10.信号Semaphore

    Dijkstra于1965年提出了信号量的概念。信号量是一种变量类型,它允许值为0,代表目前没有wakeup等待的进程,或者一个正整数,代表当前wakeup等待的进程数目。Dijkstra提出信号的信号类型有两种操作:up和down操作(Dijkstra提出的是P和V操作)。up和down操作其实是sleep和wakeup原语的一种实现。

    对于down操作来说,它先检查值是否大于0,如果是,则值减1,并且继续操作;如果不是,进程就会被休眠,此刻down操作就无法完成。检查值、减少值,或者可能的sleep被打包成一个原子操作,也就是不可分的。它保证一旦一个信号操作开始,除非它完成或者sleep了,都则,别的进程都无法接触这个信号。

    对于up操作来说,它将值加1,如果当前这个信号有一个或者多个进程在这个信号上由于之前的down操作未完成(当时值为0)休眠了,那么系统会随机的选择一个进程,并且唤醒它。如果有多个进程在该信号上休眠,那么执行一次up操作后信号的量的值依然是0,但是在这个信号上休眠的进程就少了一个。增加值,并且唤醒休眠在该信号的操作也是不可分的。up操作是不会造成阻塞的。

    11.使用信号来解决生产者-消费者问题

    用信号量来解决生产者消费者问题,涉及到三个信号量:full——代表有多少个buffer单元是满的,empty——有多少个buffer单元是空的,mutex——用来确保生产者消费者不能同时访问一个buffer单元。其原理代码如下:

#defone N 100   /*buffer的大小*/
typedef int semaphore;
semaphore mutex = 1;
semaphore full = 0;
semaphore empty = N;

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;
    wile(TRUE) {
        down(&full);
        down(&mutex);
        item = remove_item();
        up(&mutex);
        up(&empty);
        consume_item(item);
    }
} 

    像mutex这样,最开始初始化为1,并且用于多个进程之间来保证他们中一次只有一个能进入其临界区的信号被叫做二元信号量(Binary Semaphore)。如果每个进程在进入临界区之前进行一次down操作,而在离开临界区之后做一次up操作,那么互斥就可以得到保证。

    在上面的代码中,使用信号量有两种不同的方式:第一种方式,用来保证互斥,mutex的使用就是为了确保同一时刻只有一个进程进入其临界区;第二种方式,用来确保同步,full和empty的使用确保当buffer已经满的时候producer不再运行而当buffer为空的时候consumer不再运行。


 

~~~~~未完待续~~~~~

猜你喜欢

转载自wawlian.iteye.com/blog/1346486