2.3 VxWorks任务间通信策略
无论内存是否通过MMU机制进行管理,每个任务都有自己的内存空间。通过MMU机制进行管理时,内存空间被分为两个区域:虚拟内存区域和物理内存区域。此时程序中使用的是虚拟内存地址,当一个程序需要访问外部内存时,一般CPU中MMU硬件单元会自动完成虚拟地址到物理地址的转换。基于MMU机制的操作系统,任务运行的代码中使用虚拟地址。由于需要通过虚拟地址到物理地址的变换,故不同任务之间虚拟地址的重叠不会造成任何问题,而实际上,几乎所有的操作系统在MMU机制下将系统内所有任务的虚拟地址空间都设置为完全重叠的区域,如Linux操作系统下一个任务(进程)的虚拟地址空间范围为0~3GB,最高1GB被内核和所有的任务共享(即内核代码使用的虚拟地址空间)。
不同的任务具有完全重叠的虚拟地址空间并不会对任务的实际运行造成任何影响,因为不同的任务虽然具有相同的虚拟地址,但是通过MMU的映射都被映射到不同的物理地址,而物理地址才是访问外部实际RAM时使用的地址。由于不同的任务具有完全相同的虚拟地址,故为了保证任务间不相互影响,每个任务都具有自己的内存映射表,这个映射表作为任务自身组成的一个关键部分而存在。如图2-1所示,表示了两个不同任务地址的映射关系。
图2-1 虚拟地址空间到物理地址空间的映射
由于物理地址空间有限,故对于暂时不使用的物理页面可以先将其置换到硬件缓冲区中,即通常所述的交换区,从而让出相应的物理页面供其他任务使用。使用这种交换的方法,从表面上或者从任务使用页面的角度表现为一个比实际内存设备容量大得多的物理存储。对于无MMU机制下的任务而言,虚拟地址就是物理地址,此时不同任务的地址空间必须严格隔离开(当然除了刻意使用共享内存),保证不同任务间不会造成相互影响。
结合以上的说明,对于任务间通信,其本质就是在使用共享物理内存的机制。虽然手册上将内核提供的任务间通信机制与共享内存机制区分开来,只把用户层次的诸如全局变量之类的使用方式作为共享内存来看,但是从操作系统底层来看,所有的任务间通信实现方法的内在本质都是共享物理内存。
VxWorks内核本身主要提供了5种任务间通信的机制:信号量、消息队列、管道、网络Socket、信号机制。这几种机制无一例外地都是在使用共享物理内存机制(可以将网络通信暂且也看做是一种广义上的内存共享),只不过这块共享的内存由内核进行管理,任务无法直接进行访问,必须通过内核提供的接口函数进行访问,这就提供了一种保护和管理机制,使得任务间通信安全有序地进行。
2.3.1 信号量
信号量的主要用途是互斥和同步。互斥主要保护资源,即某个时刻只允许有一个任务在使用该资源。由于使用该资源的潜在用户可能很多,故信号量此时此地就作为一把“钥匙”,要使用该资源的任务,必须首先取得这把钥匙,方可进行资源的使用,否则就等待。同步则是任务间协同完成某一项共同工作的机制,典型的例子即一个任务产生资源,另一个任务使用资源,使用任务的资源平时处于等待状态,等待产生资源的任务完成其资源的生成,而一旦资源产生完毕,此时产生资源的任务就会触发同步信号量,让等待使用资源的任务启动(即唤醒)其处理资源的工作。
信号量的底层实现可以简单地看做是一个内核维护的全局变量,对于用于互斥机制的信号量,这个内核全局变量初始化为1,当一个任务需要访问该信号量保护的资源时,其首先检查这个内核全局变量的值是否为1,如非1,则表示已存在其他任务在使用资源,就等待;如为1,表示资源当前可被访问,则这个任务首先将这个内核全局变量的值设置为0,阻止其他任务的访问,而自身就可以安全地使用该资源。此处的一个漏洞是,在当前任务修改内核全局变量的同时,另一个任务可能同时在检查这个全局变量的值,很可能造成另一个任务检查到全局变量值为1后,当前任务才完成全局变量0值的设置,此时就有两个任务在使用资源,造成内核状态的不一致,极端情况下,将造成整个系统的崩溃。内核对这种情况进行了特殊处理,一般是将变量的改变操作作为一个原子操作(如x86下提供的Lock指令)完成。这也是内核提供的任务间通信机制和用户层任务间通信机制的根本区别:内核提供的机制已经从根本上保证了足够的安全性。
基于各种资源不同的使用方式,VxWorks信号量机制具体提供了三种信号量:通用信号量、互斥信号量、资源计数信号量。通用信号量既可用于同步,也可用于资源计数,此时资源数通常为1(当资源数为1时,也可以称之为互斥)。互斥信号量针对在使用过程中的一些具体问题(如优先级反转)做了优化,更好地服务于任务间互斥需求;资源计数信号量用于资源数较多,同时可供多个任务使用的场合。
2.3.2 消息队列
消息队列内核实现上实际是一个结构数组,数组大小和数组中元素的容量在创建消息队列时被确定。在创建消息队列时指定的另外一个参数是消息队列满时任务等待基于的策略:FIFO或者优先级排序。消息队列是VxWorks内核提供的任务间传递较多信息的一种机制,不过这种机制存在很大的局限性,即每个消息的最大长度是固定的。当然,在这个最大长度范围内从用户层而言是可变的,但是对于内核维护而言,所有的消息都具有相同的长度,因为无论实际消息的长度如何,内核都将按最大长度分配内存空间。当然,如果对每个消息都采用动态内存分配方式,可以消除最大长度限制,但是这并不是VxWorks提供的消息机制。VxWorks内核提供的消息机制在创建消息队列时就必须指定单个消息的最大长度以及消息的数量,在消息队列成功创建后,这些参数都是固定不变的。我们可以如此想象内核对于消息队列的实现,在消息队列创建之时,内核分配一个大小为单个消息最大长度与消息数量乘积的内存区域,可以将此看做是一个数组,数据元素个数为消息数量,每个元素的大小为单个消息最大长度。
当用户发送一个消息时,内核将消息内容存入数组中下一个空闲元素中,用户读取消息时,将读取数组中下一个非空元素,底层基本实现为一个环形缓冲区。VxWorks最多只区分两个优先级的消息,对于高优先级的消息将从数组开始处存储,对于普通优先级的消息将从数组尾部开始存储,而读取时从数组头部开始读取,从而保证高优先级的消息优先被传递。
当然,以上只是一种简单的类比,有助于读者理解VxWorks内核对于消息队列的实现。由于消息队列在创建时指定了消息数量,一旦任务传递的消息总数大于这个数量,那么传递消息的任务则需要等待,即将任务暂时设置为挂起状态,并统一挂起到一个内核分配的专门附属于对应消息队列的任务队列中,在任务被放入这个任务队列时,需要依据一定的策略,这个策略也是在消息队列创建时指定,可以是FIFO,也可以是基于优先级。FIFO方式即任务将以先进先出的方式挂入到队列中,如果消息队列中空出一个元素可供传递消息,那么最早挂入队列的任务将得到消息传递权;而基于优先级的任务队列在任务挂起时,将根据优先级将任务插入到队列中,如此当消息队列可用时,最高优先级的任务将优先得到消息传递权。
2.3.3 管道
管道相比消息队列提供了一种更为流畅的任务间信息传递机制。消息队列对于每个消息的大小存在限制,而且必须将信息分批打包,而管道可以像文件那样进行读写,是一种流式消息机制,其提供的基本操作方式类似于对一个文件的读写,支持Select函数,所以,可以对多个管道进行信息监测。管道在底层实现上是一种更直接的共享物理内存机制。信号量和消息队列还需要对传递的数据进行某种方式的封装,而管道不对传递信息做任何包装,直接分配一块连续的内存空间作为任务间信息交互的中转站。传统意义上的管道分为两种:命名管道和非命名管道。通常,任务间通信使用的都是命名管道。非命名管道使用在线程意义上,如父子进程,进程关系密切,且某些变量存在继承关系,如文件描述符,一般无法使用在两个执行路线完全不同的任务之间。
VxWorks内核提供的管道机制在创建时如同消息队列也要指定一个消息数量以及单个消息的最大长度,事实上,VxWorks提供的管道机制在底层实现上完全基于消息队列,用户层使用上完全类似于对一个文件的操作,但是对于每次读写的字符数存在最大长度限制,这个限制就是管道创建时指定的最大消息长度。由于管道底层实现上是基于消息队列的,故管道只是在应用层进行了文件系统层次的封装,可以支持open、write、read、close等普通文件操作行为,文件系统层次下的层次为字符驱动层,提供pipe_open、pipe_write、pipe_read、pipe_close等响应函数,字符驱动层之下就是消息队列实现函数,诸如msgQReceive、msgQSend等函数。所以,本质上讲,VxWorks下的管道机制只是在消息队列之上提供了文件系统层次的支持,其本质上还是一个消息队列,每次read、write(读、写)的最大数据长度受限,且read、write连续调用的次数受限,即如果连续调用read,而无对应的write操作,内核维护的消息队列为空,此时read任务将挂起等待。注意:基于管道的消息队列任务等待策略是基于FIFO方式的。
2.3.4 网络套接字Socket
Socket是一种特殊的任务间通信机制,其特殊之处在于通信的任务双方不需要限制在同一台PC上,可以是联网的任何两台计算机上的两个任务。由于传递的信息需要通过非稳定的网络介质,故用户需要指定信息传输策略:是否要求数据可靠。通常而言,使用Socket机制进行信息传递的两个任务间大多使用在批量数据的传输上,但也有进行控制信息传输的。Socket通信虽然底层实现相比其他机制要复杂得多,但这些都是操作系统实现的一部分,在用户使用层次上,与以上讨论的其他三种机制没有区别。用户只需遵循一套规定的操作接口,即可完成不同主机上两个任务间的信息传输。
Socket机制下的通信由于涉及不同主机,故传输方式从根本上区别于其他任务间通信机制,其底层实现上由网络栈加网卡驱动组成,网卡驱动完成传输介质上(如双绞线)数据的接收,通过网络栈的层层剖析,最后将传递的纯数据暂存于内核空间供任务读取,发送方任务发送的信息经过网络栈的层层封装,暂存于内核空间,依次按序地通过网卡驱动最终发送到网络传输介质上。由于内核提供的暂存空间有限,故无论是发送方还是接收方,都有数据速率的限制。
2.3.5 任务间通信的特殊机制:信号
信号不是信号量,二者不是一个概念。信号量是一种任务间互斥和同步机制,而信号则用于通知一个任务某个事件的发生。一个信号产生后,对应任务暂停当前执行流程,转去执行一个特定的函数进行处理。信号机制有些类似于中断,也可以将信号看做是一种用户层提供的软件中断机制(区别于CPU本身提供的软件中断指令)。这种用户层软件中断机制相比硬件中断和中断指令而言具有较长的延迟时间,即从信号产生到信号的处理之间大多将经历较长的延迟。
VxWorks下的信号处理有些特别,当一个任务接收到一个信号时,在这个任务下一次被调度运行之时进行信号的处理(即调用相关信号处理函数)。事实上,由于VxWorks在退出内核函数时都会进行任务调度,故一个任务,无论是否是当前执行的任务,都将在被调度运行时执行信号的处理。通用操作系统上对于某些信号将不允许用户修改其默认处理函数,如SIGKILL、SIGSTOP,然而VxWorks操作系统中可以对任何信号的处理函数进行更换。