1 实时操作系统概述
1.1 实时操作系统
在计算的早期开发的操作系统的最原始的结构形式是一个统一的实体(monolithic)。在这样的系统中,提供的不同功能的模块,如处理器管理、内存管理、输入输出等,通常是独立的。他们在执行过程中并不考虑其他正在使用中的模块,各个模块都以相同的时间粒度运行,即严格按时间片分时方式运行。
由于现代实时环境需要许多不同的功能,以及在这样的环境中存在的并发活动所引起的异步性和非确定性,操作系统变得更加复杂。所以早期操作系统的统一结构的组织已经被更加精确的内部结构所淘汰。
层次结构的起点----内核
操作系统的最好的内部结构模型是一个层次性的结构,最低层是内核。这些层次可以看成为一个倒置的金字塔,每一层都建立在较低层的功能之上。 内核仅包含一个操作系统执行的最重要的低层功能。正象一个统一结构的操作系统,内核提供了在高层软件与下层硬件之间的抽象层。然而,内核仅提供了构造操作系统其他部分所需的最小操作集。
对一个实时内核的要求。
一个实时操作系统内核需满足许多特定的实时环境所提出的基本要求,这些包括:
多任务:由于真实世界的事件的异步性,能够运行许多并发进程或任务是很重要的。多任务提供了一个较好的对真实世界的匹配,因为它允许对应于许多外部事件的多线程执行。系统内核分配CPU给这些任务来获得并发性。
抢占调度:真实世界的事件具有继承的优先级,在分配CPU的时候要注意到这些优先级。基于优先级的抢占调度,任务都被指定了优先级, 在能够执行的任务(没有被挂起或正在等待资源)中,优先级最高的任务被分配CPU资源。换句话说,当一个高优先级的任务变为可执行态,它会立即抢占当前正在运行的较低优先级的任务。
快速灵活的任务间的通信与同步:在一个实时系统中,可能有许多任务作为一个应用的一部分执行。系统必须提供这些任务间的快速且功能强大的通信机制。内核也要提供为了有效地共享不可抢占的资源或临界区所需的同步、互斥机制。
方便的任务与中断之间的通信:尽管真实世界的事件通常作为中断方式到来,但为了提供有效的排队、优先化和减少中断延时,我们通常希望在任务级处理相应的工作。所以需要杂任务级和中断级之间存在通信。
性能边界:一个实时内核必须提供最坏情况的性能优化,而非针对吞吐量的性能优化。我们更期望一个系统能够始终以50微妙执行一个函数,而不期望系统平均以10微妙执行该函数,但偶尔会以75微妙执行它。
特殊考虑:由于对实时内核的要求的增加,必须考虑对内核支持不断增加的复杂功能的要求。这包括多进程处理,Ada和对更新的、功能更强的处理器结构如RISC的支持。
拥有其它名字的内核
许多商用化的内核支持的功能远强于上面所列的要求。在这方面,他们不是真正的内核,而更象一个小的统一结构的操作系统。因为他们包含简单的内存分配、时钟管理、甚至一些输入输出系统调用的功能。
1.2 基本概念
任务(TASK):任务是由计算机所执行的一项活动,它包括一个程序和于这个程序有关的数据、程序状态及计算机资源等。
进程(PROCESS):在UNIX操作系统里,任务被称作进程。
任务和进程都可以作为调度单位被内核调度执行,但在一些既有任务又有进程的系统里任务和进程是有区别的,主要表现在:任务可以直接对内层进行寻址,而进程不能,进程只能从其父进程里继承一些特定的属性,而任务却能和父任务在几乎相同的环境下运行。
任务的状态:包括运行、就绪、挂起、休眠等在内的任务所处的状态。
任务的调度:负责控制各个任务在各个任务状态之间的转换。
任务的控制块(TCB):任务控制块用来描述一个任务,每一个任务都与一个TCB相关联。TCB包括了任务的当前状态、优先级、要等待的事件或资源、任务程序码的起始地址、初始堆栈指针等信息。此外,TCB还被用来存放任务的“上下文”(CONTEXT)。
任务的同步:对单个任务而言,同步就是使它能在指定的时间执行,对两个或两个以上的任务,同步是任务间需要协调执行的情况。
消息:消息机制使用一个被各有关进程共享的消息队列,进程之间经由这个消息队列发送和接收消息,并用来协调(同步)对共享资源的使用的方法。
管道(PIPE):是一个类似文件的结构,它用于同步读/写操作,使用系统调用PIPE可以产生一个管道并返回两个文件指针,一个用于读管道,一个用于写管道。
中断(Interrupt)和中断处理程序(Interrupt Service Routine):中断一般是指硬件中断,它用于通知操作系统特定外部事件的发生,中断响应速度对于实时系统是至关重要的。可以使用intConnect()函数把特定的ISR与某一中断联系起来。
信号(SIGNAL):它用来通知一个任务特定事情的发生,并立即调用相应的信号处理函数进行处理,它异步的改变任务的控制流程。信号与中断类似,每个信号通常需要和特定的信号处理程序绑定起来(调用sigaction()或signal())。信号通常用于错误或异常处理,用于通知一些硬件中断错误,如:总线错误、非法指令、浮点异常、被零除等。它也可以用于进程间通讯。
信号量(semaphore):很多方面类似一个全局变量,不同的是,对信号量的操作(Creat、Delete、Take、Give)都是特定的原子操作,是不可中断的。信号量主要用于进程间的同步和对共享资源的互斥访问。
2 VxWorks的系统综述
2.1 VxWorks
VxWorks 是美国 Wind River System 公司( 以下简称风河 公司 ,即 WRS 公司)推出的一个实时操作系统。Tornado 是WRS 公司推出的一套实时操作系统开发环境,类似Microsoft Visual C,但是提供了更丰富的调试、防真环境和工具。
VxWorks操作系统有以下部件组成:
l 内核(wind):
n 多任务调度(采用基于优先级抢占方式,同时支持同优先级任务间的分时间片调度)
n 任务间的同步
n 进程间通信机制
n 中断处理
n 定时器和内存管理机制
l I/O 系统
VxWorks 提供了一个快速灵活的与 ANSI C 兼容的 I/O 系统,包括 UNIX 标准的Basic I/O(creat(), remove(), open(),close(), read(), write(), and ioctl().),Buffer I/O (fopen(), fclose(), fread(), fwrite(), getc(), putc()) 以及POSIX 标准的异步 I/O。VxWorks 包括以下驱动程序:网络驱动、管道驱动、RAM盘驱动、SCSI驱动、键盘驱动、显示驱动、磁盘驱动、并口驱动等
l 文件系统
支持四种文件系统: dosFs,rt11Fs,rawFs 和 tapeFs
支持在一个单独的 VxWorks 系统上同时并存几个不同的文件系统。
l 板级支持包 BSP(Board Support Package)
板级支持包向VxWorks操作系统提供了对各种板子的硬件功能操作的统一的软件接口,它是保证VxWorks操作系统可移植性的关键,它包括硬件初始化、中断的产生和处理、硬件时钟和计时器管理、局域和总线内存地址映射、内存分配等等。 每个板级支持包括一个 ROM 启动(Boot ROM)或其它启动机制。
l 网络支持:
它提供了对其它VxWorks系统和TCP/IP 网络系统的"透明"访问,包括与BSD套接字兼容的编程接口,远程过程调用(RPC),SNMP(可选项),远程文件访问(包括客户端和服务端的NFS机制以及使用RSH,FTP 或 TFTP的非NFS机制)以及BOOTP 和代理ARP、DHCP、DNS、OSPF、RIP。无论是松耦合的串行线路、标准的以太网连接还是紧耦合的利用共享内存的背板总线,所有的 VxWorks 网络机制都遵循标准的 Internet 协议。
l 系列网络产品:
n WindNet SNMP
n WindNet STREAMS
n WindNet 第三方产品,包括 OSI、SS7、ATM、Frame Relay、CORBA、ISDN、X.25、
n CMIP/GDMO、分布式网络管理等。
这些产品扩展了VxWorks的网络特性,并增强了嵌入式处理器的网络特性
l 虚拟内存( VxVMI)与共享内存(VxMP)
n VxVMI 为带有 MMU 的目标板提供了虚拟内存机制。
n VxMP 提供了共享信号量,消息队列和在不同处理器之间的共享内存区域。
l 目标代理(Target Agent)
目标代理遵循 WBD(Wind Debug)协议,允许目标机与主机上的 Tornado 开发工具相连。在目标代理的缺省设置中,目标代理是以 VxWorks 的一个任务tWdbTask 的形式运行的。
Tornado 目标服务器(Target Server)向目标代理发送调试请求。调试请求通常决定目标代理对系统中其它任务的控制和处理。缺省状态下,目标服务器与目标代理通过网络进行通信,但是用户也可以改变通信方式。
l 实用库
VxWorks 提供了一个实用例程的扩展集,包括中断处理、看门狗定时器、消息登录、 内存分配、字符扫描、线缓冲和环缓冲管理、链表管理和 ANSI C 标准。
l 基于目标机的工具
在 Tornado 开发系统中,开发工具是驻留在主机上的。但是也可以根据需要将基于目标机的Shell 和装载卸载模块加入 VxWorks。
总之,VxWorks的系统结构是一个相当小的微内核的层次结构。内核仅提供多任务环境、进程间通信和同步功能。这些功能模块足够支持VxWorks在较高层次所提供的丰富的性能的要求。VxWorks与各部件关系如图所示:
图:VxWorks层次组织结构
2.2 Tornado
Tornado组成:
l Tornado一整套交叉开发工具
l VxWorks 实时操作系统
l 连接目标机与宿主机的通信选项
Tornado所提供的工具:
l WindConfig::
建立适合用户特性的 VxWorks 操作系统。
l Loader:
具有动态连接的装载器。
l CrossWind:
源程序(C 或 C++ 以及汇编程序等)的调试工具。
CrossWind结合了图形方式和命令行方式的最大特点。
最普通的调试方式,例如断点设置和程序执行控制可以通过便捷的点击方式实现。同样,程序显示框和数据侦察窗也提供了一个直接的可视窗口来观察应用程序中最关键的一部分。
l Browser:
可观察系统对象(任务、消息队列、信号量等)和存储器使用情况;
Browser汇总了应用进程,内存消耗和一个目标内存的映射。
通过 Browser,可观察信号量、 消息队列、内存分配、看门狗计时器、堆栈使用情况、 目标CPU使用率、对象模块结构和符号表以及每个任务的详细信息。
l WindSh:
提供从宿主机到目标机之间的一个命令shell,它可以解释执行内层目标中的几乎所有C语言函数或指令(it can interpret and execute almost all C-language expressions)。
l WindView:
系统可视诊断和分析工具。可观察各任务,中断程序之间的相互作用。它是在嵌入式系统应用开发期间的可视工具。
l VxSim:
快速原型仿真器。可在硬件设备未完成之前,在宿主机上对应用程序进行仿真分析。他可以在宿主机上模拟目标机的VxWorks操作系统环境,在许多方面他与目标机上的VxWorks系统能力是一样的,他们的区别主要在于:在VxSim里,内存Image是作为宿主机(Windows or UNIX)的一个进程执行,没有与目标硬件的交互,所以不适合于设备驱动的仿真。
l StethoScope:
实时数据图形监控器。收集数据,并将数据以图形方法显示出来。
l ObjectCenter:
C++ 开发环境。选用的是 GNU C++ 编译器,I/O 流库支 持 C++ 中的格式化 I/O。
Tornado调试模式:
l 系统模式
对整个应用系统进行调试,可在系统中设置断点等。调试中应用系统必须停下 来
l 任务模式(动态调试)
调试是针对系统中某一任务模块进行的,整个系统仍可保留在工作状态。
本文中重点介绍了VxWorks操作系统的多任务内核、任务调度、任务间的通信和中断处理机制等核心内容。
3 VxWorks操作系统基础
3.1 VxWorks的任务
在VxWorks中,一般的应用被组织成相互独立又相互协作的程序,每一个程序在运行时称作一个任务。在VxWorks中,任务不仅可以共享和快速访问大部分的系统资源,还保留着充分独立的上下文以实现单独的线程控制。
3.1.1 多任务
多任务为应用程序对多个离散的现实事件的控制和反应提供了基本的机制。VxWorks的实时内核wind提供了一个基本的多任务环境。内核按照一种调度算法交替运行各个任务,造成一种多个任务并行运行的假象,每一个任务都有自己的任务上下文。任务上下文是任务每次被调度运行时所能看到的CPU环境和系统资源。在一次上下文切换中,一个任务的上下文被存在任务控制块(TCB)中。一个任务的上下文包括:
l 一个用于执行的线程,即任务的程序计数器
l CPU的寄存器和可选择的浮点寄存器
l 用于动态变量和函数调用的堆栈
l 对标准的输入、输出、出错的I/O口的分配
l 延时(休眠)时钟
l 时间片时钟
l 内核的控制结构
l 信号句柄
l 调试和性能监视参数
在VxWorks中,一个非常重要但并不是任务上下文的一项资源就是存储器地址空间:所有代码(任务)都在一个单一的公有的地址空间运行,这一点是区别于许多非实时操作系统的(UNIX, Windows)。
3.1.2 任务的状态迁移
内核维护系统中的每个任务的当前状态。状态迁移发生在应用程序调用内核功能服务的时候。任务被创建以后进入挂起态,需要通过特定的操作使被创建的任务进入就绪态,这一操作执行速度很快,使应用程序能够提前创建任务,并以一种快捷的方式激活该任务。在VxWorks中,任务有以下几种状态,并在这几种状态中切换。
就绪态(ready) ----一个任务当前除了CPU不等待任何资源
阻塞态(pend) ----一个任务由于某些资源不可获得而被阻塞
延迟态(delay) ----一个任务睡眠一段时间
挂起态(suspend)----主要用于调试的一个辅助状态,挂起只是禁止任务的执行, 但没有禁止状态迁移。因此处于pended-suspended状态的任务仍然可以解阻塞(unblocked),处于delayed-suspended状态的任务仍然可以苏醒(awaken)。
另外还有其他一些辅助状态:
DELAY+S 任务被delayed+suspended
PEND+S 任务被pended+suspended
PEND+T 任务被pended with a timeout value
PEND+S+T 任务被pended with a timeout value and suspended
State+I 这个任务的状态取决于当前的状态再加一个继承的优先级
状态的转换:
ready pended semTake( )/msgQReceive( )
ready delayed taskDelay( )
ready suspended taskSuspended()
pended ready semGive( )/msgQSend( )
pended suspended taskSuspend( )
delayed ready expied delay
delayed suspended taskSuspend( )
suspended ready taskResume( )/taskActive( )
suspended pended taskResume( )
suspended delayed taskResume( )
3.1.3 Wind内核的任务调度
多任务需要一个调度算法分配CPU给就绪的任务。在VxWorks中默认的调度算法是基于优先级的抢占调度,但应用程序也可以选择使用时间片轮转调度。
任务的调度控制程序有:
调度控制函数
函数 描述
kernelTimeSlice( ) 控制轮转调度
taskPrioritySet( ) 改变一个任务的优先级
taskLock( ) 禁止任务的重调度
taskUnlock( ) 允许任务的重调度
优先级式的抢先调度
基于优先级的抢占调度,每个任务被指定一个优先级,内核分配CPU给处于就绪态的优先级最高的任务。调度采用抢占的方式,是因为当一个优先级高于当前任务的任务变为就绪态时,内核将立即保存当前任务的上文,并切换到高优先级任务的上下文。VxWorks有从0到255共256个优先级。在创建的时候任务被指定一个优先级,在任务运行的过程中可以动态地修改优先级以便跟踪真实世界的事件优先级。外部中断被指定优先于任何任务的优先级,这样能够在任何时候抢占一个任务。
时间片轮转式调度
基于优先级抢占调度可以扩充时间片轮转调度。时间片轮转调度允许在相同优先级的处于就绪态的任务公平地共享CPU。没有时间片轮转调度,当有多个任务在同一优先级共享处理器时,一个任务可能独占CPU,不会被阻塞直到被一个更高优先级的任务抢占,而不给同一优先级的其他任务运行的机会。如果时间片轮转被使能,执行任务的时间计数器在每个时钟滴答递增。当指定的时间片耗尽,计数器会被清零,该任务被放在同一优先级任务队列的队尾。加入特定优先级组的新任务被放在该组任务的队尾,并将运行计数器初始化为零。
抢占锁定
Wind的调度器可以显式通过tasklock( )和taskUnlock( )对一个任务锁定或允许抢占调度。当一个任务通过调用taskLock( )来锁定抢占调度,在任务的运行期间就避免了高优先级的任务的抢占。但如果该任务阻塞或挂起,内核仍然可以调度其他任务运行,一旦该任务重新运行,它又变成不可抢占的。
注意禁止抢先阻止了任务上下文的切换,但不会锁住中断的处理程序。
抢占锁会影响实时操作系统的性能,所以必须保证锁定时间尽量短。
3.1.4 任务控制
在VxWorks中的taskLib可以得到有关任务控制的基本例程。这些程序提供了任务的创建、控制和信息。
任务的创建和激活
任务创建函数
函数 描述
TaskSpawn( ) 创建并激活一个新的任务
TaskInit( ) 初始化一个新的任务
TaskActivate( ) 激活一个已经初始化的任务
taskSpawn()的参数包括新任务的名称(ASCII码串),优先级,选项字,堆栈大小,主程序地址(任务的执行函数地址),和10个可以传递给主程序的起始参数。taskSpawn()创建新任务的上下文,包括分配堆栈、设定具有指定参数的调用主例程的任务环境,新任务从给定的主例程程序入口处开始执行。
id=taskSpawn(name,priority,option,stacksize,main,arg1,…arg10);
taskSpawn()例程屏蔽了低层次的资源分配、初始化、激活步骤。初始化、激活功能分别由taskInit()和taskActivate()函数提供,然而,建议只有当需要在分配或激活上进一步控制的时候才使用他们。
任务的名字和IDs
当一个任务建立后,你可以规定一个任意长度的ASCII码字符串作为任务的名称,VxWorks将返回一个任务的ID,这个ID是一个指向任务数据结构的4字节的句柄。通常,ID=0表示当前任务。
任务名字之间不能冲突,同时为了更好使用Tornado开发环境,任务名也不能和全局变量和全局函数冲突。为了避免命名的冲突,VxWorks约定使用命名前缀:应用于目标机的任务前缀为“t”,应用于宿主机的任务前缀为“u”。
如果用户不指定任务的名字(调用taskSpawn()时,name=NULL),系统自动赋予任务一个唯一的名字----tN(N是一个可自动累加的十进制整数)。
任务名字和ID的例程
调用 描述
TaskName ( ) 通过任务的ID来获得任务的名字
TaskNameTold ( ) 通过任务的名字查找任务的ID
TaskIdSelf ( ) 获得当前任务的ID
TaskIdVerify ( ) 核实一个特定任务的存在
任务选项(Task Options)
当创建一个任务时,通过运行一个逻辑或操作运算来确定选择的任务选项,就是taskSpawn ( )函数的第三个参数。可供设置的选项如下表所列,注意如果任务需要处理浮点运算,那么必须设置VX_FP_TASK选项。
Task Options
选项名 值 描述
VX_FP_TASK 0x8 在浮点协处理器上运行
VX_NO_STACK_FILL 0x100 不用0xEE填充堆栈
VX_PRIVATE_ENV 0x80 在私有环境中执行任务
VX_UNBREAKABLE 0x2 本任务禁止断点
建立一个包含浮点操作的任务采用以下调用格式:
tid = taskSpawn (“tMyTask”,90,VX_FP_TASK,200000,myFunc,2387,0,0,0,0,0,0,0,0,0);
任务创建之后可以通过taskOptionsGet ( )(检查任务参数)和taskoptionsSet ( )(设置任务参数)对任务的参数进行检查和修改,目前只可修改VX_UNBREAKABLE这一项。
任务信息
下表所列的函数用于得到任务的信息,这些函数在被调用时通过对任务的上下文拍摄快照的方式得到调用时刻的任务信息,因为任务状态本身是动态的,所以你所得到的信息可能并不是当前任务真实的信息,除非你知道任务本身处于休眠状态。
函数 描述
TaskIdListGet ( ) 填充所有激活任务的ID队列
TaskInfoGet ( ) 获得一个任务的信息
TaskPriorityGet ( ) 检查任务的优先级
TaskRegsGet ( ) 检查任务的寄存器
TaskregsSet ( ) 设置一个任务的寄存器
TaskIsSuspended ( ) 检查任务是否处于悬挂状态
TaskIsReady ( ) 检查任务是否就绪
TaskTcb ( ) 获得任务控制块的指针
任务删除和删除保护
一个任务可以被动态地删除,任何任务都可以隐式或显式的调用exit ( )函数结束自己,一个任务也可以通过调用taskDelete ( )删除其他任务。结束任务时,内核会自动释放任务堆栈和任务控制块,用户申请的内存不会自动释放,必须由任务自己负责释放。
wind内核提供防止任务被意外删除的机制。通常,一个执行在临界区或访问临界资源的任务要被特别保护。我们设想下面的情况:一个任务获得一些数据结构的互斥访问权,当它正在临界区内执行时被另一个任务删除。由于任务无法完成对临界区的操作,该数据结构可能还处于被破坏或不一致的状态。而且,假想任务没有机会释放该资源,那麽现在其他任何任务现在就不能获得该资源,资源被冻结了。
下表列出了任务删除函数:
任务删除函数
函数 描述
Exit( ) 删除自身
TaskDelete() 删除别的任务
TaskSafe() 保护自己不被其他任务删除
TaskUnsafe() 解除删除保护
任务可以使用taskSafe ( ) 和taskUnsafe ( )来实现删除保护,如果一个任务调用了taskSafe ( ),任何任何试图删除该任务的任务将被阻塞,只到该任务调用taskUnsafe ( )解除删除保护为止。该机制同时支持嵌套调用,这样需要保存一个计数器用来跟踪taskSafe ( )调用次数,只有当该计数器为零时任务才能被删除。下面是使用以上函数来临界区的例子:
taskSafe ();
semTake (semId, WAIT_FOREVER); /* Block until semaphore available */
.
. critical region
.
semGive (semId); /* Release semaphore */
taskUnsafe ();
正如上面所展示的,任务删除保护通常伴有互斥操作,为了方便性和效率,互斥信号量包含了删除保护选项(参见"互斥信号量")。
任务控制
Vxworks的调试系统需要调用能使任务挂起或恢复执行的例程从而可冻结任务的状态以便检查。任务在对严重错误作出反应时也需要在运行期间重启,taskRestart()使用初始参数重新创建任务。TaskDelay()延时操作则可提供简单的机制使任务持续一个固定的时间段的睡眠状态。作为延时操作的一个附加结果,taskDelay( ) 操作把调用任务放到相同优先级的就绪队列的末尾。所以,你可以使用零延时达到让出CPU给同优先级其他任务的目的。零延时只适用于taskDelay( ) ,nanosleep()认为非法。
任务控制函数
函数 描述
taskSuspend() 挂起一个任务
taskResume() 恢复执行一个任务
taskRestart() 重启一个任务
taskDelay() 延时一个任务,延时的时间单位以ticks计
nanosleep() 延时一个任务,延时时间单位以纳秒计
3.1.5 任务扩展性
支持不可预见的内核扩展的能力与已有功能的可配置性是同样重要的。简单的内核接口和互斥方法使内核级功能扩展相当容易;在某些情况下,应用可以仅利用内核钩子函数来实现特定的扩展。内部钩子函数:
为了不修改内核而能够向系统增加额外的任务相关的功能,VxWorks提供了任务创建、切换和删除的钩子函数。这些允许在任务被创建、 上下文切换和任务被删除的时候额外的例程被调用执行。这些钩子函数可以利用任务上下文中的空闲区创建wind内核的任务特性扩展。
任务创建、切换、删除钩子函数
函数 描述
taskCreateHookAdd() 在每一次任务创建时增加一个被调用的程序
TaskCreateHookDelete() 删除在任务创建时增加的程序
taskSwitchHookAdd() 在每一次任务切换时增加一个被调用的程序
TaskSwitchHookDelete() 删除在任务切换时增加的程序
taskDeleteHookAdd() 在每一次任务删除时增加一个被调用的程序
TaskDeleteHookDelete() 删除在任务删除时增加的程序
用户安装的任务切换(switch)钩子函数是在内核上下问中被调用的,所以切换钩子函数不能访问所有的VxWorks资源。有些函数不能被切换钩子函数所调用,具体使用时需要注意。
3.1.6 任务错误状态:errno
按照惯例,当函数遇到一个错误时,C库函数设置一个全局整数变量errno成一个适当的错误号码。这个约定被定义成ANSI C标准的一部分。
Errno的分层定义
在VxWorks中,errno 同时用两种方法定义。一种是符合ANSI C标准,有命名为errno的全局变量。另外,errno 同时是在errno.h中定义的一个宏。这个定义除了一个函数外,对所有的VxWorks都是可见的。这个宏的定义是调用一个__errno()的函数,得到全局变量errno的地址。这是一个很有用的特性。我们可以在调试时在函数中设下断点,来看看到底产生了什么错误。不过,由于errno宏的结果是errno全局变量的地址,在C程序中允许这样的标准赋值方法:
errno = someErrorNumber;
请注意在编程时不要定义和errno一样的本地变量,以免混淆。
每个任务有自己的errno值
在VxWorks中,全局变量errno是一个单个的预定义全局变量。它可以在和VxWorks系统链接时被应用程序代码直接引用。(不论是在主机静态链接时或者是在动态加载时)。显而易见,errno在多任务环境中是十分有用的,但是每一个任务都必需观察它自己的errno变量,所以error值必须采用某种切换机制。这就是说,errno变量是任务进行切换时,系统内核保存和恢复的任务上下文的一部份。类似地,中断服务程序(ISR)也有自己的errno变量存储。
内核自动提供中断的入口和出口代码中,完成存储和恢复中断栈中的errno值。也就是说,不管VxWorks的上下文如何,一个错误码都将被存储或者做为参考直接写到全局变量errno中去。
错误返回码的约定
几乎所有的VxWorks函数都遵从这样的约定:函数操作通过返回实际值来简单地指示成功或者失败。许多函数只返回状态值OK(0)或ERROR(-1)。一些函数通常返回非负数(如open( )函数返回文件句柄),同时也使用ERROR表示错误。函数返回指针时常常用NULL指示错误。在多数情况下,一个函数返回的错误指示同时也和在errno中写入特定的错误码。
VxWorks函数从来不会清除errno全局变量。这意味着,这个错误值一直指示着最后一个发生的错误状态设置。当一个VxWorks的子程序调用另一个程序而得到错误指示时,它通常只是返回自己的错误指示而并不修改errno。即errno的值只是低层函数才使用,它指示的是低层函数的错误返回值。
比如:VxWorks的函数intConnect ( ),它把中断处理程序和硬件中断联系起来,其中需要调用内存分配函数malloc ( )分配内存,如果分配失败,malloc( )将会设立errno值,标记内存不足,同时返回一个NULL指示失败。而intConnect()函数收到malloc()返回的NULL后,将返回一个自己的错误指示ERROR。但它并不更改errno的值。Errno中仍然标记着“内存不足”的错误码。
我们推荐你最好在自己的程序中使用自己的错误码机制,而系统提供的errno只做调试时检测之用。如:
if ((pNew = malloc (CHUNK_SIZE)) == NULL)
return (ERROR);
错误状态值的分配
VxWorks的errno值表明了一个模块的错误类型,高位两字节的代表模块号,低位两个字节来表示单独的错误码。所有VxWorks的errno的模块号从1-500,如果模块号为0,则表示用于资源的兼容性问题,其他模块号可以由应用程序使用。
应用程序可以使用大于501<<16的正数以及所有负数。
错误码请参见errnoLib参考。
3.1.7 任务异常处理
程序代码和数据中的错误可能会导致硬件异常情况的发生,比如出现非法指令,总线和地址错误,被零除等等。VxWorks的异常处理软件包能处理好所有的此类异常。默认的异常处理方法是将产生此异常的任务挂起,并将任务在发生异常点的状态保存下来。内核和其他任务继续运行而不间断。我们可以使用Tornado检查被挂起任务的状态。
任务也可以通过信号机制把自己的处理程序挂在已有的硬件异常上。如果一个任务为一个异常提供了信号处理程序,默认的异常处理方法将被替代而不起作用。信号同时也可以用于报告软件异常,使用方法和在硬件异常中一样。
3.1.8 共享代码和代码重入
在VxWorks中,通常会有许多不同的任务调用一个相同的程序或子程序库。被多个任务执行的同一套代码被称为共享代码。
TASKS SHARED CODE
图:共享代码
共享代码必须具有可重入性。当一个例程可以被几个任务的上下文同时调用而不发生冲突的时候,这个子程序就具有可重入性。由于程序的数据和代码都是同一拷贝,当子程序修改了全局变量或静态变量时就会往往产生冲突。一个程序引用这样的变量的会造成不同任务上下文的重叠和调用干扰。
VxWorks的大多数程序都具有可重入性,但所有具有一个name_r()形式函数的对应函数name()都被认为不具有可重入性。
VxWorks的I/O和驱动程序都具有可重入性,但需要慎重地设计应用程序。对于I/O缓存,我们推荐每个任务使用一组文件指针。在驱动级,因为VxWorks使用的是全局文件指针描述表,可能会有不同的任务以流的形式写缓存区。这种做法是否合适,取决于应用程序本身。比如:一个包驱动可以将来自不同任务的数据流合成到一起,因为每个包的头上都定义了目标地址。大多数的VxWorks的程序都采用如下的可重入技术:
l 动态堆栈变量(局部变量)
l 由信号量机制保护的全局变量和静态变量
l 任务变量
我们推荐在写需要多个任务同时使用的的应用程序代码时,使用以上的机制。
动态堆栈变量
许多子程序都是纯代码,除了动态堆栈变量外没有自己的数据。程序处理的数据都是调用者以参数的形式来提供的。如链接表库lstLib就是一个很好的例子。它的函数只对调用者调用时提供的列表和节点进行操作。
这一类函数天然具有可重入的特性。多个任务可以同时使用此函数而互不干扰,因为每个任务都有自己的的堆栈,所以多个任务可以同时调用程序而不产生冲突。
TASK TASK STACKS COMMON SUBROUTINE
图:堆栈变量和共享代码
被保护全局变量和静态变量
一些封装库可以访问公共数据,例如内存分配库,menLib就管理着由多个任务共用的内存池。这个库说明并使用自己的静态数据变量来跟踪内存池的分配。
这种库在使用时需要当心,因为它的函数不是天然可以重入的。多个任务同时调用库中的程序会干扰相互对公共变量的访问。所以这类库必须提供互斥机制来禁止任务同时访问临界代码。最常用的互斥机制是互斥信号量,互斥机制的实现后面再详细介绍。
任务变量
一些被多任务同时调用的程序需要全局或静态变量,而对每一个调用的任务又要求这些变量都具有不同的值。为了适应这种需求,VxWorks提供了一种叫“任务变量”的机制。它允许将一个四字节的变量家加到任务的上下文中。这样每次进行任务切换操作时,变量的值都会发生切换。
典型的例子中,如果多个任务申明了一个相同的变量(4字节)作为任务变量。每一个任务都可以把这个变量作为自己的私有地址空间。这个功能由taskVarLib提供的taskVarAdd(),taskVarDelete(),taskVarSet(),taskVarGet()一组函数实现。频繁使用这种机制可能会增加任务上下文切换时间,解决的方法是可以把一个模块中所有需要使用任务变量的变量定义在一个单独结构中,对这些变量的访问都可以通过一个指向该结构的指针进行,这样只需要把这个指针定义为任务变量就可以了。
图:任务变量和上下文切换
多个任务使用同一套代码
在VxWorks中,允许使用同一套代码创建多个任务。每一个新任务都有自己的堆栈和上下文。每次产生新任务时都可以传递不同的参数。此时,可以使用任务变量的方法实现重入规则。
当同一个函数使用不同的参数设置在系统中同时运行时,任务变量是十分有用的。比如,一个特殊设备监控程序可能要派生出多道任务来监控好几个这样的设备。这是可以使用特定设备的编号来作为任务的输入参量。
在下图中,机械手的多关节使用同样的代码,任务通过调用joint()函数控制对关节的操作。关节的编号(jointNum)用于指示被操作的关节。
3.1.9 VxWorks的系统任务
VxWorks包括以下几个系统任务:
根任务:tUsrRoot
根任务tUsrRoot是由内核执行的第一个任务。根任务的入口函数是installDir/target/config/all/usrConfig.c下的usrRoot()函数,此函数初始化多数的VxWorks功能。它产生其他任务,诸如,注册任务,异常处理任务,网络功能任务,以及tRlogind守护进程。一般情况下,当所有的初始化完成后,根任务将被被删除。你可以随意在根任务中添加需要的初始化过程。
注册任务:tLogTask
注册任务tLogTask,VxWorks模块使用注册任务可以实现不通过当前任务的上下文进行I/O操作而记录系统消息。比如printf将发送要打印的系统消息,可以直接将系统消息挂到LogTask的队列中。再由Log任务发送此消息,而调用printf的任务并不直接对I/O端口操作。本任务在logLib库中。
异常处理任务:tExcTask
异常处理任务tExcTask支持捕获那些不会引起中断的VxWorks异常。这个任务必需是系统中任务优先级最高的任务。而且不允许挂起,删除和改变优先级。本任务在excLib库中。
网络任务:tNetTask
tNetTask守护任务处理VxWorks网络功能中所需的任务级函数。
目标代理任务:tWdbTask
如果任务模式中设置了目标代理,系统将创建一个tWdbTask任务。它负责响应Tornado的目标服务器。主要用于观察调试。
可选模块的任务:
以下的系统任务是根据VxWorks中相关的配置宏定义创建的任务。
TShell
如果你在VxWorks配置中包含懒得目标shell,系统就会产生这个任务。任何一个任务和函数都可以在tshell任务的上下文中运行。
TRlogind
如果配置了目标shell和rlogin特性,当一个远程用户注册到VxWorks主机上,在连接的两端都产生tRlogInTask和tRlogOutTask作为tty终端提供给用户。
tTelnetd
如果配置了目标shell和telnet特性,守护进程远程用户通过telnet连接在VxWorks上。它可以接受远程用户注册在VxWorks或主机系统上,并产生tTelnetInTask和tTelnetOutTask输入输出任务。提供一个tty的模拟终端给用户使用。
TPortmapd
如果你配置了RPC特性,这个守护任务将成为同一个机器中的所有的RPC服务的注册中心。PRC的客户都必须通过这个tPortmapd守护任务才能访问服务。
3.2 任务间通信
VxWorks提供了几种任务间通讯的方法。
n Shared Data Structures,用于简单的数据共享;
n Semaphores,用于基本的互斥和同步;
n Message Queues & Pipes,实现同一CPU内任务间的消息传递;
n Sockets & RPC,实现网络透明的任务间通信;
n Signals,用于异常处理。
3.2.1 共享数据结构
在VxWorks中,所有任务是存在于单一的线形地址空间中,使得共享数据结构可行。可以共享的数据结构有:全局变量、线形Buffer、环行Buffer、链表、指针等。
图:共享数据结构
3.2.2 互斥
wind内核的任务间通信机制的基础是所有任务所在的共享地址空间。通过共享地址空间,任务能够使用共享数据结构的指针自由地通信。管道不需要映射一块内存区到两个互相通信任务的寻址空间。不幸的是,共享地址空间具有上述优点的同时,带来了未被保护内存的重入访问的危险。UNIX操作系统通过隔离进程提供这样的保护,但同时带来了对于实时操作系统来说巨大的性能损失。
当一个共享地址空间简化了数据交换,通过互斥访问避免资源竞争就变为必要的了。用来获得一个资源的互斥访问的许多机制仅在这些互斥所作用的范围上存在差别。实现互斥的方法包括禁止中断、禁止任务抢占和通过信号量进行资源锁定。
中断禁止和中断延时
最强的互斥方法是屏蔽中断。这样的锁定保证了对CPU的互斥访问。这种方法当然能够解决互斥的问题,但它对于实时是不恰当的,因为它在锁定期间阻止系统响应外部事件。长的中断延时对于要求有确定的响应时间的应用来说是不可接受的。
funcA()
{
int lock=intLock();
...
/*critical region that cannot be interrupted*/
…
intUnlock(lock);
}
中断锁定对于包含ISR(中断服务程序)的互斥有时是必需的,但在任何情况下,要尽量使中断锁定的时间短。
抢占禁止和延时
禁止抢占提供了强制性较弱的互斥方式。 当前任务运行的过程中不允许其他任务抢占,而中断服务程序可以执行。这也可能引起较差的实时响应,就象被禁止中断一样,被阻塞的任务会有相当长时间的抢占延时,就绪态的高优先级的任务可能会在能够执行前被强制等待一段不可接受的时间。为避免这种情况,在可能的情况下尽量使用信号量实现互斥。
funcA()
{
taskLock();
...
/*critical region that cannot be interrupted*/
…
taskUnlock(lock);
}
3.2.3 信号量Semaphores
在VxWorks中,Semaphores是一个高度优化的、提供最快任务间通信的机制。主要定位于两类需求:
?互斥
在互斥方面,信号量是用于锁定共享资源访问的基本方式。不象禁止中断或抢占,信号量限制了互斥操作仅作用于相关的资源。一个信号量被创建来保护资源。VxWorks的信号量遵循Dijkstra的P()和V()操作模式。
当一个任务请求信号量,P(), 根据在发出调用时信号量的置位或清零的状态,会发生两种情况。如果信号量处于置位态,信号量会被清零,并且任务立即继续执行。如果信号量处于清零态,任务会被阻塞来等待信号量。
当一个任务释放信号量,V(),会发生几种情况。如果信号量已经处于置位态,释放信号量不会产生任何影响。如果信号量处于清零态且没有任务等待该信号量,信号量只是被简单地置位。如果信号量处于清零态且有一个或多个任务等待该信号量,最高优先级的任务被解阻塞,信号量仍为清零态。
通过将一些资源与信号量关联,能够实现互斥操作。当一个任务要操作资源,它必须首先获得信号量。只要任务拥有信号量,所有其他的任务由于请求该信号量而被阻塞。当一个任务使用完该资源,它释放信号量,允许等待该信号量的另一个任务访问该资源。
?任务同步
信号量另一种通常的用法是用于任务间的同步机制。在这种情况下,信号量代表一个任务所等待的条件或事件。最初,信号量是在清零态。一个任务或中断通过置位该信号量来指示一个事件的发生。等待该信号量的任务将被阻塞直到事件发生、该信号量被置位。一旦被解阻塞,任务就执行恰当的事件处理程序。信号量在任务同步中的应用对于将中断服务程序从冗长的事件处理中解放出来以缩短中断响应时间是很有用的。
VxWorks提供三种信号量:
?二值信号量(Binary),最快、最通用的semaphore,被优化用于同步和互斥。
?互斥信号量(mutual exclusion),一种特殊的二值信号量,对互斥所固有的问题提供了优化解决方案:优先级继承,删除保护,递归。
?计数信号量(Counting),类似二值信号量, 但是可以跟踪信号量被给于的次数,用于一种资源的多个实例的保护。
VxWorks信号量的控制
VxWorks并不是为每一种信号量提供了一整套的控制机制,相反他对三种信号量提供了统一的操作接口,只是信号量的创建函数针对不同的信号量是不同的。
信号量控制函数
函数 描述
semBCreate() 创建并初始化二值信号量
semMCreate() 创建并初始化互斥信号量
semCCreate() 创建并初始化计数信号量
semDelete() 删除并释放一个信号量
semTake() 获取信号量
semGive() 归还信号量
semFlush() 唤醒所有等待一个信号量的任务(不能用于互斥信号量)
函数semBCreate() semMCreate()semCCreate()返回一个信号量ID,他可以被其他信号量控制函数作为句柄使用。信号量创建时需要指定信号量任务队列的队列类型,主要两种队列方式:优先级方式(SEM_Q_PRIORITY)和先进先出方式(SEM_Q_FIFO)。它决定了多个任务将以何种方式排队等待一个信号量。
3.2.3.1 二值信号量
一个二值信号量被认为是一个具有“可获得”(满)和“不可获得”(空)两种状态的标志量。一个任务能否通过semTake()获得一个二值信号量取决于在调用的时候信号量是满还是空。如果信号量是满,在任务取之后变成空,任务可继续执行;如果信号量是空,且超时参数不等于NO_WAIT,则任务排到阻塞队列中进入阻塞状态;如果信号量是空,且超时参数等于NO_WAIT,任务继续运行,未获取信号量。
同样一个任务通过semGive()还回信号量时,如果信号量是满,则不起任何作用,如果是空且没有等待该信号量的任务,则将其置为满。如果信号量是空,且已经有任务在等待该信号量,那么该信号量的阻塞任务队列中的第一个任务被解阻塞继续运行,信号量仍为空。如图所示:
二值信号量用于互斥
二值信号量有效地锁定了对共享资源的访问,最初的信号量是被设置为“满”。当一个任务希望访问资源,它必须先得到信号量,只要任务持有信号量,其他访问同一临界资源的任务就会阻塞。当任务结束对资源的使用就会归还信号量,从而允许其他任务使用资源。
semTake(semMutex,WAIT_FOREVER);
…
/*critical region ,only accessible by a single task at a time*/
…
semGive(semMutex);
二值信号量用于同步
当用于任务间的同步时,信号量则代表一个任务正在等待的事件或条件。信号量最初设为“空”,一个任务或中断服务程序通过给信号量semGive()来告之事件的发生,而另一个任务通过semTake()取信号量来知道特定事件的发生,这个任务在获得信号量之前一直处于阻塞状态。
/* This example shows the use of semaphores for task synchronization. */
/* includes */
#include "vxWorks.h"
#include "semLib.h"
#include "arch/arch/ivarch.h" /* replace arch with architecture type */
SEM_ID syncSem; /* ID of sync semaphore */
init (int someIntNum)
{
/* connect interrupt service routine */
intConnect (INUM_TO_IVEC (someIntNum), eventInterruptSvcRout, 0);
/* create semaphore */
syncSem = semBCreate (SEM_Q_FIFO, SEM_EMPTY);
/* spawn task used for synchronization. */
taskSpawn ("sample", 100, 0, 20000, task1, 0,0,0,0,0,0,0,0,0,0);
}
task1 (void)
{
...
semTake (syncSem, WAIT_FOREVER); /* 等待事件的发生 */
printf ("task 1 got the semaphore\n");
... /* process event */
}
eventInterruptSvcRout (void)
{
...
semGive (syncSem); /* 通知事件的发生*/
...
}
3.2.3.2 互斥信号量
互斥信号量是一种特殊的双态信号量,它用于解决某些互斥中的一些特有问题,包括优先级倒置、删除保护和对资源的递归调用。互斥信号量是一种特殊的二值信号量,与二值信号量基本是相同的,不同之处有:
l 它只用于互斥
l 它只能被取走它的任务归还
l 它不能在中断服务程序中执行归还操作
l semFlush()操作对它是非法的
优先级逆转
优先级逆转发生在一个高优先级的任务被强制等待一段不确定的时间以便一个较低优先级的任务完成执行。考虑下面的假设:
T1,T2和T3分别是高、中、低优先级的任务。T3通过拥有信号量而获得相关的资源。当T1抢占T3,为竞争使用该资源而请求相同的信号量的时候,它被阻塞。如果我们假设T1仅被阻塞到T3使用完该资源为止,情况并不是很糟。毕竟资源是不可被抢占的。然而,低优先级的任务并不能避免被中优先级的任务抢占,一个抢占的任务如T2将阻止T3完成对资源的操作。这种情况可能会持续阻塞T1等待一段不可确定的时间。这种情况成为优先级逆转,因为尽管系统是基于优先级的调度,但却使一个高优先级的任务等待一个低优先级的任务完成执行。
图:优先级逆转
互斥信号量有一个选项SEM_INVERSION_SAFE,允许实现优先级继承的算法。优先级继承通过在T1被阻塞期间提升T3的优先级到T1解决了优先级逆转引起的问题。这防止了T3,间接地防止T1,被T2抢占。通俗地说,优先级继承协议使一个拥有资源的任务以等待该资源的任务中优先级最高的任务的优先级执行。当执行完成,任务释放该资源并返回到它正常的或标准的优先级。因此,继承优先级的任务避免了被任何中间优先级的任务抢占。
优先级继承使用方法如下,注意SEM_INVERSION_SAFE选项必须和信号量的优先级队列选项一起使用。
semId = semMCreate (SEM_Q_PRIORITY | SEM_INVERSION_SAFE);
删除保护
互斥引起的一个问题会涉及到任务删除。在由信号量保护的临界区中,需要防止执行任务被意外地删除。删除一个在临界区执行的任务是灾难性的。资源会被破坏,保护资源的信号量会变为不可获得,从而该资源不可被访问。通常删除保护是与互斥操作共同提供的。由于这个原因,互斥信号量通常提供选项来隐含地提供前面提到的任务删除保护的机制。
前面讲到,原语taskSafe()和taskUnsafe()对任务的删除提供了一条解决的途径。互斥信号量提供另一种更方便易用的保护方法,互斥信号量提供了一个SEM_DELETE_SAFE的参数使得每个semTake()都含有taskSafe(),且每个semGive()都含有taskUnsafe()。通过这个方法,一个持有信号量的任务就可以得到删除保护,而且代码的效率也更高。
semId = semMCreate (SEM_Q_FIFO | SEM_DELETE_SAFE);
递归资源访问
互斥信号量可以被递归地获取。这意味着占有信号量的任务在释放前可以不止一次地取得信号量。递归可以实现当一组程序需要相互调用又要互斥访问同一个资源时的情况。
在任务释放时,互斥信号量的TAKE的次数必须等于GIVE的次数。
3.2.3.3 计数信号量
计数信号量与二值信号量的唯一区别在于它跟踪信号量获取和归还的次数。每次信号量被获取,计数器减一,每当信号量被归还,计数器加一。当计数器为零时,需要获取信号量的任务则需进入阻塞队列等待。和二值信号量一样,如果归还信号量时,有任务在等待该信号量,则该任务被解阻塞。和二值信号量不同的是,如果归还信号量时,没有任务在等待该信号量,则信号量递增。也就是说如果信号量被归还3次,那么他就可以被获取3次。
计数信号量用于保护具有多个实例的资源。例如对一个具有3个磁带驱动器的应用则需对计数信号量初始化为3,下面显示了计数信号量的使用:
信号量函数调用 调用后信号量值 调用结果
semCCreate() 3 信号量创建,初始值为3
semTake() 2 信号量被获取
semTake() 1 信号量被获取
semTake() 0 信号量被获取
semTake() 0 信号量为空,任务阻塞
semGive() 0 阻塞的任务得到信号量
semGive() 1 没有任务等待信号量,信号量值递增
3.2.3.4 信号量的特殊参数
VxWorks信号量包含两个特殊的参数:
Timeouts
VxWorks信号量提供了一种解除阻塞状态的超时机制。在semTake()函数中有一个参数,该参数可以控制任务在信号量阻塞队列里的等待时间(ticks数)。如果在这段时间内,该任务成功的获取了信号量则返回OK。否则返回ERROR,errno设置为超时。Timeout有两个特殊值:NO_WAIT表示不等待,不管是否取得信号量;WAIT_FOREVER表示永远等待,只到获取信号量为止。
Queues
Wind的信号量能够选择队列机制,用于指定阻塞任务的排队方式。主要两种队列方式:优先级方式(SEM_Q_PRIORITY)和先进先出方式(SEM_Q_FIFO)。它决定了多个任务将以何种方式排队等待一个信号量。
3.2.4 消息队列
在实时系统中,应用被构造成一组相互独立又协调工作的任务。信号量提供了互斥和任务之间的同步。通常,需要更高级的机制来允许任务之间相互通信,在VxWorks中,单个CPU中任务之间的通信可由由消息队列完成。
消息队列:可以存放不定数量的消息,每条消息长度可以也不一样,消息按一定机制排队。任何任务或是中断处理程序都可以向一个消息队列中发送消息。任何任务都可以从消息队列取消息。使用消息队列的全双工通讯模式如下:
VxWorks消息队列控制
函数 描述
MsgQCreate() 创建并初始化一个消息队列
MsgQDelete() 删除并释放一个消息队列
MsgQSend() 向一个消息队列中发送消息
MsgQReceive() 从一个消息队列中接收消息
使用msgQCreate()创建消息队列时,需要指明消息队列的最大消息数,以及消息的最大长度(单位:BYTE),这样内核才能确定分配的内存空间。
一个任务或者中断处理程序可以使用msgQSend()向消息队列中发送消息,如果没有任务在消息队列上等待消息,消息被加入队列,如果已经有任务等待消息,那么消息直接交给等待的第一个任务。
一个任务可以使用msgQReceive()从消息队列中取消息,如果消息队列中已经有消息,那么第一条消息被取出队列并返回给调用者。如果队列为空,则该任务被阻塞,加入到消息队列的等队任务队列。
超时TimeOuts
在消息队列的相关收发操作中,都牵扯到一个参数:超时(TimeOut)。在发送的时候,如果没有空间来对消息进行排队的话,TimeOut指定可以等待直到得到缓存的Tick数。在接收的时候,TimeOut指定在消息到来之前可以等待的Tick数。
TimeOut可以选择:
?NO_WAIT(0),意味着立即返回,不管是否发送/接收成功;
?WAIT_FOREVER(-1),意味着永远等待,只到操作成功;
?非负整数,表示等待的Tick数。
紧急消息Urgent Messages
函数msgQSend()允许规定消息的优先级:
?正常消息 MSG_PRI_NORMAL
?紧急消息 MSG_PRI_URGENT
正常级别的消息被放到消息队列的末尾,而紧急消息放到队列头上。
3.2.5 管道
管道(Pipes)提供了和消息队列类似的另外一种进程间通讯方式。管道是一种虚拟的I/O设备,是由pipeDrv驱动管理的。
创建一个管道:
status = pipeDevCreate ("/pipe/name", max_msgs, max_length);
pipeDevCreate将创建一个管道设备,以及与其相关联的消息队列。其参数分别分别指定了管道的名字,最大消息数,以及消息的最大长度。
Pipes被当作一个标准的I/O设备,可以被open、close、read、write以及ioctrl等函数操作。Pipes提供Message Queue的消息排队和阻塞机制,并能提供一个独特的能力:与select()函数一起使用。通过select(),Pipes允许Task等待接收来自一个I/O设备集合的数据。由于select()也可以用于其他一些异步设备,因此,使用select(),Task可以等待接收多个Pipes、Sockets、Serial Ports设备集合的数据。需要注意的是不能对消息队列使用select()操作。
3.2.6 跨网络的任务间通信
套接字sockets
VxWorks中,网络上任务间的通信可以通过套接字接口。一个套接字就是任务间通信的端点,数据从一个套接字传到另一个套接字上,当一个套接字创建后,就已经规定了网络通信中数据传输的协议。VxWorks支持Internet中的TCP和UDP两种协议。TCP通常作为有连接的可靠传输协议,而UDP则是用于无连接的数据报协议。关于socket本文不做详细描述,具体参见“VxWorks Network Programmer’s Guide”。
远程进程调用
RPC (Remote Procedure Call)允许在一个机器上的进程可以调用在同一个机器或远端机器上的进程的函数(过程)。可以实现跨进程或跨CPU的远程函数调用。RPC采用套接字作为通信机制的。
对于前面讨论过的消息队列和管道,许多实时系统都是由用户-服务器的任务模式所组成。用户任务向服务器任务提出服务请求后便等待回答。RPC规范了这种模式并为传送请求和回应提供了一套标准的协议。
3.2.7 信号
VxWorks提供一个软信号机制来异步地改变任务的执行流程。任何任务或ISR都可以向一个特定任务发送信号,接收到信号后任务当前的执行线程被挂起,执行任务指定的(或缺省的)信号处理函数,信号可以被理解为一个软中断,不过与ISR不同的是:信号处理函数是在任务下一次调度运行的时候执行,而ISR可以随时打断Task的执行。
Signals比其他任务间通信机制更适合错误和异常的处理。信号处理函数的处理类似ISR,因此其中不能使用可能造成阻塞的函数。由于Signals是异步的,很难确定哪个资源会不可靠,因此Signals尽量使用在ISR中可以安全使用的函数。
Wind支持两种类型的信号操作接口:UNIX-BSD方式和POSIX兼容方式,POSIX方式提供了更丰富的控制功能:
Wind所支持的两种信号量编程接口
POSIX 1003.1b 函数 UNIX BSD函数 描述
Signal() signal() 指定特定信号的处理函数
kill() kill() 向任务发送一个信号
Raise() N/A 给自己发送信号
Sigaction() sigvec() 检查或设置特定信号的处理函数
Sigsuspend() pause() 挂起一个任务只到一个信号发送
Sigpending() N/A 得到阻塞的信号集
Sigemptyset()Sigfillset()Sigaddset()Sigdelset()Sigismember() sigsetmask() 控制、更改信号屏蔽字
Sigprocmask() Sigsetmask() 设置一个阻塞信号的信号屏蔽字
Sigprocmask() Sigblock() 向阻塞信号集中增加信号
注:信号屏蔽字规定了当前阻塞而不能传递给该进程的信号集。
3.3 中断服务代码
硬件中断处理在实时系统中具有重要的意义,因为通常都是通过中断来通知系统外部事件的发生。为尽可能快速对中断进行反应,VxWorks的中断服务程序(ISRs)运行在任务上下文之外的一个特殊的上下文中。因此,中断处理不包括任务的上下文切换。库intLib和intArchlIB提供的中断程序如下所示:
中断控制函数
函数 描述
intConnect() 连接一个C程序到中断矢量上
intContext() 如果被中断级调用则返回真
intCount() 获得当前中断嵌套深度
intLevelSet() 设置处理器中断屏蔽级别
intLock() 禁止中断
intUnlock() 恢复中断
intVecBaseSet() 设置矢量的基地址
intVecBaseGet() 得到矢量的基地址
intVecSet() 设置一个异常矢量
intVecGet() 得到一个异常矢量
3.3.1 应用代码与中断连接
VxWorks提供例程intConnect()允许将C函数与任何中断相连,程序的参数是连接的中断矢量的字节偏移量,被连接的C函数的地址和传递到该函数的一个参数。当一个中断发生并通过这种方法建立了一个矢量,连接的C函数在中断级上使用先前指定的参数被调用,当中断处理结束后,被连接的函数返回。以这种方式连接到中断上的程序称为中断服务程序(ISR)。
中断事实上不是直接指向一个C函数,而是通过intConnect()建立部分代码用于保存必需的寄存器,根据传递的参数建立一个堆栈入口(既可在一个特定的中断堆栈也可在一个当前任务的堆栈上),并且调用连接的函数。在函数返回时,它恢复寄存器和堆栈并离开中断。如下图所示:
图:intConnect()的功能
3.3.2 中断堆栈
有些构架允许所有的ISRs使用相同的中断堆栈,在系统启动时系统根据配置的参数而分配和初始化堆栈,它必须足够大到能处理最坏连接情况的嵌套中断。
有些构架不允许使用一个独立的中断堆栈,这样ISRs就使用中断任务的堆栈。如果你使用这种构架,你在创建任务时必须分配足够大的堆栈空间,以处理最坏情况的中断嵌套和调用嵌套。请查看BSP资料看你所使用的硬件构架是否支持独立的中断堆栈。
采用checkStack()可以在开发期间查看中你的断服务程序或任务是否已经用完了全部的堆栈空间。
3.3.3 ISRs的一些特殊限制
许多VxWorks的函数或资源都允许ISRs访问,但有一些重要的限制。这些限制是由于ISR不是运行在一个不同的任务上下文中:它不具有任务控制块,如所有的ISR都共享一个单一的堆栈。所以最基本的一个限制就是它不准调用有可能使其阻塞的程序。例如,他们不能获取信号量,因为如果信号量为空,内核会试图切换调用者到阻塞状态。不过ISR可以归还信号量。
对于mallo()和free(),它们都包含信号量操作,有可能使中断阻塞,所以ISR不能调用这些函数。还有ISRs也不能通过VxWorks的驱动器执行I/O操作,大多数的I/O设备驱动都需要任务上下文,因为他们可能阻塞调用任务以等待特定的设备。也有例外,对于VxWorks管道驱动,他是特意设计来允许由ISR写的。
ISRs也不能调用使用浮点协处理器的程序,这是因为intConnect()创建的中断驱动代码不能存取或恢复浮点寄存器。
VxWorks支持日志功能,用于向系统控制台打印文本信息,这种机制是特意设计成可以由ISR使用的,也是ISRs打印信息的最常用方法。ISR具有自己的errno。
编写ISR时一定要确定那些函数可以调用,那些不能调用。具体参见《VxWorks Programmer’s Guide》。
3.3.4 中断级别的异常
当一个任务致使硬件发生异常如非法指令或总线错误,任务就被挂起而系统其他任务继续运行。然而,如果一个ISR导致了这个异常,系统则没有安全的资源来处理异常。ISR没有上下文可以挂起。而VxWorks则把异常的描述存储在低速存储器的一个特定的地方并执行系统的重启。
VxWorks的boot ROMs在低速存储器中检测到异常描述的出现并在系统控制台上显示。例如:
WorkQPanic:kernel work queue overflow.
这个异常的出现通常是在内核从中断级别产生非常高速的调用时发生。
3.3.5 保持高中断级别
在某些情况下,事件需要一些低级控制例如关键的动作控制或系统故障反应,这需要保持最高的中断级别来对事件实现零延时的反应。为此,VxWorks提供了一个intLockLevelSet()方法,可以将系统范围内的中断锁定级别设置为一个特定的级别。
3.3.6 对高中断级别的ISRs的一些附加限制
对于与没有被锁定中断级别连接的ISR(既包括中断级别高于intLockLevelSet()所设定的,也包括硬件规定的中断级别是非屏蔽的)有如下限制:
?ISR只能通过intVecSet()来连接;
?ISR不能使用依靠中断锁定实现正确操作的任何VxWorks的操作系统的功能。
3.3.7 中断与任务间的通信
VxWorks支持中断处理程序与一般任务间的通讯,以下通讯方式可以用于ISR与任务级代码间的通讯:
?共享存储器和环形缓冲器:ISRs可和任务级代码共享变量、缓冲器和环形缓冲器;
?信号量:ISRs可以归还信号量而任务可以获取或等待相应的信号量;
?消息队列:ISRs能够发送消息到消息队列,任务则可以取消息,若队列已满。则消息被丢弃,因为ISR不能阻塞。
?管道:ISRs可以向管道写消息让任务读。
?信号(SIGNALS):ISRs可以“指示”任务导致任务的信号句柄的异步调度
3.4 看门狗计时器Watchdog Timers
VxWorks里有一个看门狗计时器的功能允许任何一个C函数和一段特定延时相关。看门狗计时器是ISR系统时钟的一部分。通常看门狗计时器调用函数的运行是作为中断级别系统时钟的中断服务代码。而且,如果内核无法立即执行中断,函数就会被放到tExcTask的工作队列里。
一个看门狗计时器的创建通过调用wdCreate(),然后调用wdStart()启动计时器,其参数包括:延时的ticks数,一个C函数func()以及传给这个函数的一个参数para。当指定的ticks数到达时,上面的func()函数会被调用,这个C函数使用para作为其参数。定时器可以在到达前,随时用函数wdCancel()取消。关于以上两个函数的使用方法,请参照以下例程:
#define SECONDS (3)
WDOG_ID myWatchDogId;
task (void)
{
/* Create watchdog */
if ((myWatchDogId = wdCreate( )) == NULL)
return (ERROR);
/* Set timer to go off in SECONDS - printing a message to stdout */
if (wdStart (myWatchDogId, sysClkRateGet( ) * SECONDS, logMsg,
"Watchdog timer just expired\n") == ERROR)
return (ERROR);
/* ... */
}
定时器控制函数
函数 描述
wdCreate() 分配并初始化一个看门狗计时器
wdDelete() 终止并释放一个看门狗计时器
wdStart() 启动计时器
wdCacel() 取消正在计时的计时器
4 VxWorks中的函数库:
4.1 TaskLib:
NAME
taskLib - task management library
ROUTINES
taskSpawn( ) - spawn a task
taskInit( ) - initialize a task with a stack at a specified address
taskActivate( ) - activate a task that has been initialized
exit( ) - exit a task (ANSI)
taskDelete( ) - delete a task
taskDeleteForce( ) - delete a task without restriction
taskSuspend( ) - suspend a task
taskResume( ) - resume a task
taskRestart( ) - restart a task
taskPrioritySet( ) - change the priority of a task
taskPriorityGet( ) - examine the priority of a task
taskLock( ) - disable task rescheduling
taskUnlock( ) - enable task rescheduling
taskSafe( ) - make the calling task safe from deletion
taskUnsafe( ) - make the calling task unsafe from deletion
taskDelay( ) - delay a task from executing
taskIdSelf( ) - get the task ID of a running task
taskIdVerify( ) - verify the existence of a task
taskTcb( ) - get the task control block for a task ID
4.2 ErrnoLib:
NAME
errnoLib - error status library
ROUTINES
errnoGet( ) - get the error status value of the calling task
errnoOfTaskGet( ) - get the error status value of a specified task
errnoSet( ) - set the error status value of the calling task
errnoOfTaskSet( ) - set the error status value of a specified task
4.3 SigLib:
NAME
sigLib - software signal facility library
ROUTINES
sigInit( ) - initialize the signal facilities
sigqueueInit( ) - initialize the queued signal facilities
sigemptyset( ) - initialize a signal set with no signals included (POSIX)
sigfillset( ) - initialize a signal set with all signals included (POSIX)
sigaddset( ) - add a signal to a signal set (POSIX)
sigdelset( ) - delete a signal from a signal set (POSIX)
sigismember( ) - test to see if a signal is in a signal set (POSIX)
signal( ) - specify the handler associated with a signal
sigaction( ) - examine and/or specify the action associated with a signal (POSIX)
sigprocmask( ) - examine and/or change the signal mask (POSIX)
sigpending( ) - retrieve the set of pending signals blocked from delivery (POSIX)
sigsuspend( ) - suspend the task until delivery of a signal (POSIX)
pause( ) - suspend the task until delivery of a signal (POSIX)
sigtimedwait( ) - wait for a signal
sigwaitinfo( ) - wait for real-time signals
sigvec( ) - install a signal handler
sigsetmask( ) - set the signal mask
sigblock( ) - add to a set of blocked signals
raise( ) - send a signal to the caller's task
kill( ) - send a signal to a task (POSIX)
sigqueue( ) - send a queued signal to a task
4.4 LstLib:
NAME
lstLib - doubly linked list subroutine library
ROUTINES
lstInit( ) - initialize a list descriptor
lstAdd( ) - add a node to the end of a list
lstConcat( ) - concatenate two lists
lstCount( ) - report the number of nodes in a list
lstDelete( ) - delete a specified node from a list
lstExtract( ) - extract a sublist from a list
lstFirst( ) - find first node in list
lstGet( ) - delete and return the first node from a list
lstInsert( ) - insert a node in a list after a specified node
lstLast( ) - find the last node in a list
lstNext( ) - find the next node in a list
lstNth( ) - find the Nth node in a list
lstPrevious( ) - find the previous node in a list
lstNStep( ) - find a list node nStep steps away from a specified node
lstFind( ) - find a node in a list
lstFree( ) - free up a list
4.5 MemLib:
NAME
memLib - full-featured memory partition manager
ROUTINES
memPartOptionsSet( ) - set the debug options for a memory partition
memalign( ) - allocate aligned memory
valloc( ) - allocate memory on a page boundary
memPartRealloc( ) - reallocate a block of memory in a specified partition
memPartFindMax( ) - find the size of the largest available free block
memOptionsSet( ) - set the debug options for the system memory partition
calloc( ) - allocate space for an array (ANSI)
realloc( ) - reallocate a block of memory (ANSI)
cfree( ) - free a block of memory
memFindMax( ) - find the largest free block in the system memory partition
转自Tony嵌入式论坛,地址:http://www.cevx.com/bbs/thread-12518-1-1.html