`

《现代操作系统》读书笔记之——进程间通信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不再运行。


 

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

0
0
分享到:
评论

相关推荐

    操作系统学习笔记

    在个人计算机的早期,操作系统主要在实模式下运行,但随着技术的发展,保护模式成为了现代操作系统的标准模式。保护模式提供了一种更安全的环境,通过内存保护和权限控制防止程序间的相互干扰。它允许操作系统划分...

    操作系统原理与Linux系统试验(庞丽萍 郑然) 复习笔记.docx

    1. **多道程序设计与分时技术**:这两种技术是现代操作系统的基础,多道程序设计允许多个程序同时在内存中运行,而分时技术则使得多个用户可以共享处理机时间,从而提高了系统资源的利用率。 2. **实时操作系统**:...

    ‎嵌入式学习笔记

    理解Linux内核如何管理和分配资源,以及如何实现进程间通信,对于优化系统性能、解决系统瓶颈问题具有重要意义。 总之,这一系列的学习笔记全面覆盖了嵌入式开发的多个层面,从底层硬件到操作系统,再到用户界面,...

    c99 学习笔记

    C语言的编程基础涉及数据类型、控制结构、函数、指针等核心概念,而高级编程则涵盖更复杂的话题,如动态内存管理、文件操作、多线程和进程间通信。 内容部分由于OCR扫描结果存在文字识别错误和遗漏,导致部分信息...

    Operating Systems: Three Easy Pieces

    此外,操作系统还需要处理进程间的通信和同步问题,如信号量、管程等机制,以避免竞态条件和死锁。 3. 文件系统:文件系统是操作系统管理数据存储的重要组件。它负责组织、命名、检索和保护文件,以及管理磁盘空间...

    Professional Linux Kernel Architecture

    本章详细阐述了Linux内核如何管理和调度进程,包括进程创建、销毁以及进程间通信等机制。此外,还会讨论Linux的调度算法,例如O(1)调度器,以及这些算法是如何优化系统性能的。 #### 三、内存管理(第3章) 内存...

    Electron in Action

    - **第 4 章:使用原生文件对话框和进程间通信** —— 探讨如何集成操作系统的原生功能,并实现不同进程之间的数据交换。 - **第 5 章:处理多窗口管理** —— 讲解如何管理和控制多个窗口,以提供更加丰富的用户...

    Linux 操作系统技术合集.pdf

    - **概述**: 用于进程间通信的文件。 - **实践**: 在`/tmp`或`/var/run`目录下可以找到socket文件。 **5. 疑难杂症——删除不掉的文件** - **概述**: 有时文件因为权限问题或其他原因无法删除。 - **实践**: ...

    SAP Basis System and System Environment

    数据库负责存储和管理所有业务数据,应用服务器处理业务逻辑并提供计算能力,而网关则作为不同系统间通信的桥梁,实现数据的高效传输和接口调用。 此外,SAP Basis还涉及到系统管理和监控、性能优化、备份与恢复、...

    Bluez and D-Bus

    D-Bus是一种用于Linux和其他类Unix操作系统上进程间通信的机制。它为应用程序提供了一种简单而强大的方式来进行通信和同步操作。在Bluez项目中,D-Bus扮演着关键角色,它允许不同的应用程序通过一个统一的接口访问和...

    嵌入式系统构建(清华大学教材)

    - **操作系统发展史**:从早期的批处理系统到现代的操作系统。 - **Linux与嵌入式Linux**:开源的类Unix操作系统,广泛应用于嵌入式系统。 **2.2 操作系统内核** ##### 2.2.1 内存管理 - **内存管理功能**:分配...

    清华大学 Arm 培训教材.pdf

    - **操作系统发展史**:从早期的批处理系统到现代的操作系统发展历程。 - **Linux与嵌入式Linux**:介绍Linux操作系统及其在嵌入式领域的应用特点。 - **操作系统内核** - **内存管理** - **内存管理功能**:...

    Golang:Estudos sobre a Linguagem Go

    Go语言的核心特性之一是其并发模型,它采用了轻量级线程——goroutines,以及通道(Channels)来实现进程间的通信。这种模型让开发者能够轻松地编写出高效、安全的并发程序,而无需深入理解复杂的锁和信号量机制。 ...

Global site tag (gtag.js) - Google Analytics