1, 基本操作系统现代时实操作系统主要的补充了多任务处理和任务间通讯两个概念。多任务环境意味着允许在一个时实应用程序中构架一组独立的任务。每一个都有自己独立的执行路线和自己的系统资源。任务间通讯的机制(facility)则允许任务间的同步和通讯,以调整系统的行为。VxWorks中,任务间通讯的机制包括从快速信号量到消息队列,管道,网络传输套接口。
另一个时实系统的关键机制是硬件中断处理。因为中断常常是构成系统外部事件的机制。
为了达到中断的快速响应。中断处理程序(ISR)使用一种特殊的上下文,不同与任务的上下文。以下将讨论多任务内核,任务机制,任务间通讯,和中断处理机制。这些是VxWork s运行时环境的核心。
Wind特征和POSIX特征:POSIX的时实扩展标准(1003.1b)定义了一组特殊的内核机制。为了提高应用程序的集成性,Wind,Vxworks的内核,同时包括了POSIX接口和VxWorks的特殊接口。
在本文中“Wind”特指VxWorks的wind内核机制所有的特性。
任务:我们经常需要把应用程序组织成独立的,相互协作的一些程序。每一个单独执行的程序都是一个任务。在Vxworks中,任务能直接共享所有的系统资源,当然各个独立的线程控制所需要的各自的上下文是必需保留的。
多任务:多任务处理提供了进行对应用程序控制 和对多样的,离散的真实世界事件的反应 的基本机制。WXWORKS的时实内核,提供了基本的多任务环境。多任务机制从表面上看起来是创建了许多同时运行的线程,但实际上是内核在按照一定的数学法则对它们的执行进行调度。
每一个显然是独立的程序都叫做一个任务。每个任务都有自己的上下文。(所谓上下文是指:有系统内核运行调度的任务可能会在任何时刻访问的CPU环境和系统资源。)在一次上下文切换中,一个任务的上下文将保存在任务控制块中(TCB)。一个任务的上下文包括以下内容:
线程执行情况,即任务程序记数器。
CPU寄存器和浮点寄存器。
动态数据堆栈和函数调用。
标准输入,输出和错误的I/O分配。
延时定时器。
时间片定时器。
内核控制结构。
信号句柄。
调试和运行监视值。
在VxWorks中,一个不属于上下文的重要资源是内存和地址空间:所有的代码都在一个共同的单一地址空间中执行。要让每一个任务都有自己的地址空间就需要进行虚拟—物理内存映射,但这需要选择使用VxVMI模块以支持虚存功能。
任务状态切换:内核维护系统中所有任务的当前状态。一个任务的的状态变化是应用程序进行内核函数调用的结果。但任务创建时,它处于挂起状态。只有激活任务,才能进入准备状态。激活阶段是非常短暂的。因此最好让应用程序在早期预先创建好任务,以便在需要时及时启动。另外一种创建任务的方法是使用spawning 原语,它可以同时创建和激活任务。任务可以在任何状态下删除。
Wind微内核的状态迁移表如下图所示:
Figure 2-1
Wind内核任务调度:多任务处理需要一个调度法则对CPU准备运行的任务进行分配。对于wind内核来说,基于优先级的强占调度方式是系统的默认工作方式。当然,也可以根据应用程序的需要选择时间片轮转的调度方式。
优先抢占调度:在一个基于优先级的抢占式调度系统中,每一个任务都有一个优先级。内核保证把CPU分配给优先级较高的已准备好运行的任务。这种调度模式意味着只要有高优先级的任务处于READY状态,内核将立即存储当前任务的上下文,并切换到高优先级任务的上下文。如下图所示,任务T1被高优先级的T2任务抢占,T2又被T3抢占。T3运行完后,T2继续运行,T2运行完后,T1才能继续运行。
Wind内核总共有256个优先级,数目从0——255.优先级0是最高的255最低。(与PSOS正好相反)任务在创建是将分配一个优先级。在运行过程中也可以使用taskPrioritySet()函数改变优先级。这种设计可以使应用程序更贴近现实。
轮转调度:(Round-Robin Scheduling)
优先强占调度方式可以扩展为轮转调度方式。轮转调度方式的主要作用是允许相同优先级的准备好的任务在运行时公平地分享CPU.如果没有轮转调度,那么当多个同优先级任务必需共享处理器时,一个任务如果不阻塞的话,就可能会独占CPU,导致其它所有的任务都无法运行。
轮转调度实现在同级任务中公平分配CPU资源的方法一般是时间片轮转。一组任务中的每个任务执行指定的时间间隔或时间片;然后另一个任务执行相同的时间间隔,依次类推。这种调度方法的公平之处在于,只有所有的任务都获得过依次时间间隔运行后,才能有任务获得第二个时间片运行的机会。
系统中使用函数kernelTimeSlice()允许系统实现轮转调度。
更精确地,每个任务有一个运行时间记数器(run-time counter)记录增加的始终TICK数。当一个特定的时间片结束时。计数器将清空,并且将任务挂到同级任务的队尾。当一个新任务加入优先级组时,本任务将挂到队尾,并且将运行时间计数器初始化为0.如果一个任务在自己的运行时间间隔中被高优先级的任务抢占,它的运行时间计数器将被保存,直到任务重新运行时再恢复。下图是轮转调度系统的示例:T1,T2,T3是同级任务,T2被高优先级的T4任务抢占,当T4运行完后,T2继续运行至结束。
Wind 内核调度器可以在一个任务中通过taskLock()和taskUnLock()函数,明确地禁止或使能。当一个任务通过调用taskLock()函数禁止内核调度时,这个任务将运行在不被其它任务中断的方式下。
但是,如果此任务明确地阻塞或挂起,调度器将选择一个优先级最高的合格任务执行。不过,当关闭了调度器的任务阻塞解除再次执行时,抢占式调度将再次被关闭。
注意:抢占调度关闭只是禁止任务上下文的切换,但是并不禁止中断处理的发生。
抢占禁止可以用于实现互斥(mutual exclusion)。不过最好让调度禁止的时间尽量短。
任务控制:以下将论述VxWorks与任务有关的系统调用的基本情况。它们存在与VxWorks的库文件task Lib中。这些系统调用包括任务创建,控制,和信息获取。
任务创建:
其它的有关任务的系统调用的函数详见系统手册
任务名和ID号当一个任务产生时,可以使用一个指定的任意长的ASCII字符串作为任务的任务名。VxWo rks系统将返回一个四字节的任务ID号作为任务数据结构的句柄。所有的任务程序获得一个ID作为不同任务的标识。VxWorks系统中约定ID=0表示正在调度的任务。
一个任务的名字不得和任何已经存在的任务名字相冲突。此外,在使用Tornado开发工具进行开发时,最好不要让任务名和任何全局变量名以及全局函数名相冲突。为了避免冲突,VxWorks中约定使用前缀“t”作为运行在目标机上任务名的首字母,而运行在主机上任务名的首字母为“u”。
如果你不想给所有的任务命名,NULL指针也是允许使用的,此时系统分配一个唯一的名字“tN”给你的任务,其中N是一个依次排列的整数。
注:在shell中,任务名由与之相关的任务ID号决定,以简化现有任务的交互工作。
如果要使用浮点运算,则一定要在创建任务时选择VX_FP_TASK.任务删除和删除安全保护:任务可以动态从系统中删除。VxWorks包括的关于任务删除的系统调用如下所示:
警告:在删除任务前,一定要先将任务占用的共享资源释放掉。
Exit()函数是任务程序return返回时默认的处理函数,它也可以被任务程序显式地引用,以在任意点终止任务本身。一个任务还可以使用taskDelete()函数终止另一个任务的执行。
当要删除一个任务的时候,系统并不通知其他任务进行了删除工作。可能会产生一些问题,比如要被删除的任务正处于临界区,或则和临界区相关。这时我们不希望任务被删除。
为此,系统提供两个函数调用保护任务的删除。taskSafe(),taskUnSafe()。taskSafe保护任务不被删除,而taskUnsafe()则退出删除保护。
以下是任务保护的使用方法:
taskSafe ();semTake (semId, WAIT_FOREVER); /* Block until semaphore available */。。 critical region。
semGive (semId); /* Release semaphore */ taskUnsafe ();
在进入临界区前进行任务保护操作,退出临界区后撤消任务保护。
由于任务删除保护和互斥操作经常成对出现,为方便起见,系统专门提供了一种特殊的信号量,可以提供任务保护的选项,在这里不在累述。
任务控制:任务控制的系统调用如下表所示
挂起和启动任务的系统调用,主要是为VxWorks调试工具准备的。它们用于冻结一个任务的状态供检测调试使用。
当任务产生崩溃性错误的时候,可能需要使用任务重起的处理方法。在重起机制中,task Restart()函数将重新使用以前的参数创建一个任务。Tornado shell同样使用这种机制响应任务终止请求。
延时操作提供了一个简单的机制给任务以睡眠一个固定的时间……任务延时主要用于定时循环消息检测。如果不知道CPU的主频,可以使用以下方法延迟固定时间:taskDelay(sysClkRateGet()*时间)
如果使用taskDelay(NO_WAIT);则等待时间为0,但是系统将暂停此任务的运行,而运行其它同优先级的任务。
任务扩展:为了能在系统中实现与任务相关的附加机制,而无需改动内核。Wind内核提供了任务创建,切换和删除的挂钩(hook)。允许在进行任务操作时唤醒附加的程序。任务空制块(TC B)中已经预留了支持任务扩展上下文的空间。支持任务扩展的调度函数如下所示:
用户安装的切换挂钩是在内核上下文中调用的。因此,切换挂钩不能进入所有的VxWorks机制。下面列出可以在切换挂钩函数中调用的VxWorks函数。一般地,所有不使用内核访问系统调用的函数,都可以在挂钩函数中调用。
POSIX 系统调度接口:系统由schedPxLib库提供了POSIX 1003.1b标准调度函数。这组函数可以使你能方便地完成诸如任务优先级设置,取得调度策略,取得所有任务中最大或最小的优先级,循环调度是否有效,取得时间片的长度,等等。为了理解怎样使用函数选择本接口,必需先了解PO SIX与Wind内核在调度方法上的一些小的不同。
POSIX和WIND在调度方法上的不同:
POSIX调度方法是基于进程的,而Wind调度这是基于任务。进程和任务的不同表现在以下几个方面:1, 任务可以直接定位内存,而进程不可以。
2, 进程只能继承父进程的一些特定的属性,而任务操作的环境和父任务的环境完全一样。
任务和进程相似的地方在于都可以单独地被调度。
VxWorks使用抢占优先级的调度。而POSIX标准则使用FIFO先进先出的调度方式。(FIFO即同优先级的调度策略)
POSIX调度算法使用一个进程执行完再执行另一个进程的调度方式。而WIND应用的调度算法是基于一个系统时间带宽基准,无论是时间片轮转的设计或者是优先抢占的设计都是如此。
POSIX优先级的排列顺序和Wind的设计相反。POSIX中,大数字代表高的优先级。在Wind设计中,小数字代表高的优先级,0的优先级最高。
因此,在schedPxLib库中使用的POSIX调度方法的优先级与VxWorks其它模块认可的优先级不匹配。你可以通过设置变量posixPriorityNumbering 为FALSE来禁止掉POSIX形式的优先级设计,这样schedPxLib的优先级数就可以和VxWorks的其它模块通用了。
要使用POSIX调度函数,只要把Tornado开发工具中的INCLUDE_POSIX_SCHED模块选上即可。POSIX的调度控制函数如下:
具体用法详见英文文档。
任务的错误状态:错误码(errno)
根据惯例,C的库函数在函数运行遇到错误时,会将一个合适的错误值放到全局变量errn o中,这是ANSI标准中的特约协定中的一部分。
Errno的层定义在VxWorks中,errno 同时用两种方法定义。一种是符合ANSI C标准,有命名为errno的全局变量。另外,errno 同时是在errno.h中定义的一个宏。这个定义除了一个函数外,对所有的VxWorks都是可见的。这个宏的定义是调用一个__errno()的函数,得到全局变量errn o的地址。这是一个很有用的特性。我们可以在调试时在函数中设下断点,检测是否有特殊错误发生。不过,由于errno宏的结果是errno全局变量的地址,在C程序中允许这样的标准赋值方法:errno = someErrorNumber;请注意在编程时不要定义和errno一样的本地变量,以免混淆。
每个任务有分开的errno:在VxWorks中,全局变量errno是一个单个的预定义全局变量。它可以在和VxWorks系统链接时被应用程序代码直接引用。(不论是在主机静态链接时或者是在动态加载时)。不论怎样,errno在多任务环境中是十分有用的。每一个任务都必需观察它自己的errno变量。
这就是说,errno变量是任务进行切换时系统内核保存和恢复的任务上下文的一部份。类似地,中断服务程序(ISR)也有自己的errno变量存储。
内核自动提供中断的入口和出口代码中,完成存储和恢复中断栈中的errno值。也就是说,不管VxWorks的上下文如何,一个错误码都将被存储或者做为参考直接写到全局变量errno中去。
错误返回码的约定;几乎所有的VxWorks函数都遵从这样的约定:函数操作通过返回实际值来简单地指示成功或者失败。许多函数只返回状态值OK(0)或ERROR(-1)。一些函数通常返回非负数,同时也使用ERROR表示错误。函数返回指针时常常用NULL指示错误。在多数情况下,一个函数返回的错误指示同时也和在errno中写入特定的错误码。
VxWorks函数从来不会清除Errno全局变量。这意味着,这个错误值一直指示着最后一个发生的错误状态设置。当一个VxWorks的子程序调用另一个程序而得到错误指示时,它通常只是返回自己的错误指示而并不修改errno.即errno的值只是低层函数才使用,它指示的是低层函数的错误返回值。
比如:VxWorks的函数intConnect(),它把用户程序和硬件中断联系起来,其中调用内存分配函数malloc()分配内存,如果分配失败,它将会设立errno值,标记内存不足,同时返回一个NULL指示失败。而intConnect()函数收到malloc()返回的NULL后,将返回一个自己的错误指示ERROR.但它并不更改errno的值。Errno中仍然标记着“内存不足”的错误码。
我们推荐你最好在自己的程序中使用自己的错误码机制,而系统提供的errno只做调试时检测之用。
错误状态值的分配:VxWorks的errno值表明了一个模块的错误类型,高位两字节的代表模块号,底位两个字节来表示单独的错误码。Errno的模块号从1-500,如果模块号为0,则表示用于资源的兼容性问题。
应用程序可以使用大与501<<16的正数以及所有负数。
错误码请参见 reference 中的errnoLib.
任务异常处理:程序代码和数据中的错误可能会导致硬件异常情况的发生,比如出现非法指令,总线和地址错误,对零除法等等。VxWorks的异常处理软件包能处理好所有的此类异常。默认的异常处理方法是将产生此异常的任务挂起,并将任务在发生异常点的状态保存下来。内核和其他任务继续运行不间断。我们可以使用Tornado检查被挂起任务的状态。
任务也可以通过信号机制把自己的处理程序挂在已有的硬件异常上。如果一个任务为一个异常提供了信号处理程序,默认的异常处理方法将被替代而不起作用。信号同时也可以用于报告软件异常,使用方法和在硬件异常中一样。详细使用情况将在以后介绍。
共享指令码和重入:在VxWorks中,同一个子程序或子程序库被多个不同任务唤醒,是非常普遍的现象。比如printf()函数。但是在一个系统中只会有一份子程序指令码的拷贝。这种一份拷贝供多个任务执行的指令码叫做共享指令码。VxWorks的动态链接机制能够特别轻松地实现此功能。
共享指令码同时也使系统更有效,更便于维护。如下图:
共享指令码必须要能够重入。如果一个程序能够同时无冲突地被几个任务的上下文调用,那么这个子程序就是可重入的。这种冲突最典型的的例子就是子程序修改了全局或静态变量。一个涉及到全局变量的程序再由不同的任务上下文调用时,可能会产生冲突。
许多VxWorks函数都是可以重入的。如果一个函数不能重入,我们把它标记为name_r();
比如:ldiv()函数标记为ldiv_r(),因此我们可以知道此函数是不能重入的。
VxWorks的I/O和驱动函数是可以重入的,但是在设计时一定要仔细。对于I/O缓存,我们推荐每个任务使用一组文件指针。在驱动级,因为VxWorks使用的是全局文件指针描述表,可能会有不同的任务以流的形式写缓存区。这种做法是否合适,取决于应用程序本身。比如:一个包驱动可以将来自不同任务的数据流合成到一起,因为每个包的头上都定义了目标地址。
大多数的VxWorks函数使用了以下的重入机制:动态栈变量信号量保护的全局静态变量任务变量我们推荐在写需要多个任务同时使用的的应用程序代码时,使用以上的机制。
动态栈变量:许多子程序只是一些单纯的代码,而没有动态栈变量的需求。它们只使用由调用者提供的参数数据。LstLib库就是一个典型的例子。它的函数只对调用时提供的列表和节点进行操作。
有一些函数则天然具有重入的特性。多个任务可以同时使用此函数而互不干扰,因为每个任务都有自己的真正的堆栈。
保护全局和静态变量:有些库会访问一些公有数据。不如内存分配函数库。MemLib,它管理许多任务共同使用的内存池。这个库使用自己的静态数据变量来跟踪内存块的定位。
这种库在需要当心,因为它的函数不是天然可以重入的。多哥任务同时调用函数时可能会引起公共变量的冲突。这种库必须使用互斥信号量防止多个任务同时访问临界区,以明确实现函数的可重入。
任务变量:在一些函数中,它可以被多个任务同时调用,但是它需要具有不同的值的全局和静态变量。比如:一些任务需要用同一个变量来指示私有的内存缓冲区。
为了适应这种需求,VxWorks提供了一种叫“任务变量”的机制。它允许将一个四字节的变量家加到任务的上下文中。这样每次进行任务切换操作时,变量的值都会发生切换。
典型的例子中,如果多个任务申明了一个相同的变量(4字节)作为任务变量。每一个任务都可以把这个变量作为自己的私有地址空间。这个功能由taskVarLib提供的taskVarAdd()
,taskVarDelete(),taskVarSet(),taskVarGet()一组函数实现。任务变量的使用见下图:
要尽量得使用这种机制,因为任务变量的值作为任务上下文的一部分,在任务切换时必需要进行存取,每个任务变量将增加几个微秒的任务上下文切换时间。因此把模块中所有的任务变量反倒一个单独的动态分配数据结构中,然后间接通过一个指针访问这个结构,再把这个指针作文所有使用了这个模块的任务变量,这种方法最节省系统资源。
同一程序,多道任务在VxWorks中,允许使用同一个程序产生多个任务。没一个产生的新任务都有自己的堆栈和上下文。每次产生新任务时都可以传递不同的参数。此时,可以使用任务变量的方法实现重入规则。
当同一个函数使用不同的参数设置在系统中同时运行时,任务变量是十分有用的。比如,一个特殊设备监控程序可能要派生出多道任务来监控好几个这样的设备。这是可以使用特定设备的编号来作文主任务的输入参量。
在下图中,机械手的多关节使用同样的代码,任务通过调用joint()函数事项对关节的操作。关节的编号(jointNum)用于指示被操作的关节。
2.3.10 VxWorks系统任务VxWorks包括以下几个系统任务:
根任务:tUsrRoot根任务tUsrRoot是由内核执行的第一个任务。根任务的入口函数是installDir/target/c onfig/all/usrConfig.c下的usrRoot()函数,此函数初试化多数的VxWorks功能。它产生其他任务,诸如,注册任务,常处理任务,网络功能任务,以及tRlogind守护进程。一般情况下,当所有的初始化完成后,根任务将被结束删除。你可以随意在根任务中添加需要的初始化过程。
注册任务:tLogTask注册任务tLogTask,VxWorks模块使用注册任务可以实现不通过当前任务的上下文进行I/O操作而发送系统消息。比如printf将发送要打印的系统消息,可以直接将系统消息挂到Lo gTask的队列中。再由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守护任务才能访问服务。