第2章 VxWorks操作系统的基本组件
本章将对VxWorks操作系统中的任务、任务调度、任务间通信、内存管理、中断处理几方面知识进行介绍。
2.1 VxWorks任务
VxWorks是实时操作系统,这决定了它的任务调度必须是基于优先级的,而且必须是可抢占式调度方式,这样才能够区分实际情况下不同状态的处理级别,对高优先级的情况进行优先响应。
2.1.1 内核实现基本原理
VxWorks内核维护三个队列:tick队列、ready队列、active队列。另外还有一个队列涉及任务,即任务等待资源时所处的队列,这个队列可以是VxWorks内核提供的,也可以是用户提供的,此处令其为pend队列。
所谓tick队列,即当调用taskDelay函数让任务延迟一段固定的时间时,任务所处的队列,此时任务被设置为Delay状态,无资格竞争使用CPU;ready队列即有资格竞争使用CPU的所有任务,该队列以优先级为序排列任务,队列头部是除了当前运行任务外,系统中最高优先级的任务;active队列有些误导,实际上称之为task队列更合适,因为系统中所有的任务无论当前状态如何,都将在这个队列中,这个队列维护着系统中当前所有的任务,即通过该队列可以查找到当前系统中的所有任务,在Shell下运行“i”命令,显示系统中所有的任务,就是通过遍历active队列完成的;pend队列即当任务竞争使用某资源,而资源当前不可得时,任务就被设置为pend状态,进入pend队列。
函数taskSpawn创建一个新的任务。首先,其创建一个任务控制结构,对该结构进行初始化后,将结构加入active队列以作为系统任务管理之用。此时任务仍无资格竞争使用CPU,taskSpawn函数的最后一步就是将这个任务结构再加入到ready队列,此时这个任务才真正可以称为已经在竞争使用CPU了。当系统中所有的优先级高于这个任务的其他任务运行完毕或者由于等待资源而处于阻塞时,这个新创建的任务就将被调度运行。所以,在VxWorks下,如果一个新创建的任务优先级不高,创建后将等待一段时间才能被真正执行。在实际项目中,有时需要一个新的任务被创建后立刻得到执行,那么就需要在创建任务时,指定一个较高的任务优先级。VxWorks内核将任务分为256个优先级,标号从0到255。其中0表示最高优先级,255表示最低优先级。任务在调用taskSpawn函数进行创建时就指定了优先级,当然任务的优先级并非在创建后就无法改变,用户可以通过调用taskPrioritySet函数在任务创建后重新设定优先级,taskPrioritySet专门针对嵌入式平台下不同的情况对同一任务不同运行级别的需求进行设置。值得注意的是,taskPrioritySet函数不同于通用操作系统提供的类似函数,taskPrioritySet函数可以提高或者降低任务的运行级别,而不是通用操作系统下在任务创建后就只能动态地降低任务优先级。
VxWorks下对于应用层任务,推荐使用100~250 之间的优先级,驱动层任务可以使用51~99之间的优先级。要特别注意的是,内核网络数据包收发任务tNetTask的优先级为50,如果使用网口进行调试,则一定注意不要创建任务优先级高于50 的任务,否则tNetTask任务将无法得到运行,表现形式是死机,因为系统将无法再从网口接收调试命令,无法响应Tornado Shell或者Telnet下输入的任何命令。以上只是推荐值,事实上,由于嵌入式系统下的特殊应用,对于某个任务优先级的设置需要根据该任务完成的具体工作而定,而不可一味地遵循推荐值。如笔者曾在项目中为了与Shell“争读”从串口发送的命令,就将一个应用层任务优先级设置为0。要谨记在任务达到某一特殊目的后,必须将任务优先级设置回正常(推荐)值。
无论何种操作系统,任务在设计中都由一个数据结构表示。这个数据结构包含一个任务运行时需要的所有信息,我们一般将这些信息称为任务上下文。具体的任务上下文(广义上)包括以下内容:
1)所在平台CPU内部所有的寄存器值,特别是指令寄存器,这代表了任务当前的执行点。这一般是狭义上的任务上下文。除了寄存器值,每个任务有自己的内存映射空间、任务名称、任务优先级值、任务入口函数地址、打开文件句柄数组、信号量和用于各种目的的队列等。
2)任务运行时暂时存放函数变量以及函数调用时被传递参数的栈。从操作系统底层实现来看,很多操作系统将表示任务的数据结构和任务栈统一管理,如Linux下在分配任务结构的同时分配任务的内核栈,这两者作为一个整体进行内存分配,通常将一页(如4KB)的开始部分作为任务结构,用以存储任务关键信息,而将页的末尾作为任务内核栈的顶部。如此,实际上用一页页面的大小(通常为4KB)减去任务信息占据的空间,剩下的空间都作为任务的内核栈在使用。VxWorks与Linux在栈的分配和管理上基本类似,不过VxWorks区分于Linux的一个最大不同是VxWorks下所有的代码都运行在一个状态下,不区分内核态和用户态(当然,VxWorks下也有内核态的概念,但含义完全不同,见下文分析)。所以,不存在Linux下的内核态栈和用户态栈。VxWorks下的任务自始至终都在使用同一个栈,不论这个任务在运行过程中调用了任何VxWorks内核函数,都不存在栈的切换。正因如此,VxWorks对栈的大小无法预先进行把握,栈的大小将由被创建的任务决定,而且不同于通用操作系统,VxWorks下任务栈在任务创建时就被确定,而且此后不可以改变栈的大小。所以,对于一个存在很多递归调用的任务,必须在任务创建时指定一个较大的任务栈,防止在后续的运行中造成栈的溢出,导致任务异常退出。
3)各种定时信息。这些信息实际上都作为任务结构中的一个字段而存在。任何操作系统都必须有一个系统时钟进行驱动,该系统时钟通常称为系统的脉搏。系统时钟一定与一个高优先级的中断联系,这样,每当时钟前进一个滴答(Tick),操作系统就会响应一次中断,该中断通常就被作为操作系统进程调度的触发点。每次滴答,操作系统都会增加内核维护的一个全局变量(如VxWorks操作系统维护的vxTick变量),通过该变量为系统各种定时器提供定时依据。每个任务都有一个内部固定的定时器,用于任务内部特定的需求,每次系统时钟产生一个中断,操作系统都会对当前系统内所有需要关注的任务定时器进行处理,从而完成任务定时器特殊的用途。定时器的一个特殊变相应用即Round-Robin任务调度(简称RR调度)。RR调度实际上对每个支持RR调度的任务内部都维护有一个定时器。当一个支持RR调度的任务被调度进入运行状态时,在任务运行期间,每次系统时钟前进一个滴答时,该定时器指针都会前进一个单位。当到达预定的值时,该任务就要主动让出CPU,以便让相同优先级的其他任务运行。定时器可以以加法或者减法运算运行。对于减法运算,一般根据定时时间计算出一个Tick数,每次系统前进一个滴答,Tick数减一,当到达0时,表示定时器到期。而对于加法运算,则一般需要使用操作系统维护的全局Tick变量。VxWorks下,如设置定时时间间隔为N,则定时器到期时间为vxTick+N=T0,每次系统前进一个滴答,操作系统会对当前系统内维护的所有定时器进行检查,判断T0是否大于vxTick,一旦T0小于或等于vxTick,则表示该定时器到期,此时将根据定时器的目的做出相应的响应。
4)信号处理函数。事实上,操作系统的每个信号都有一个默认的响应方式,如用户在命令行按“Ctrl+C”组合键时,则系统默认响应方式是中止当前前台任务。每个任务可以根据自身情况定制对某个信号的响应方式。如一个任务可以将用户的“Ctrl+C”组合键操作响应为打印输出当前任务中某个变量的值。每个任务内部对每个信号都维护一个响应函数句柄,操作系统在创建任务时已经将所有的句柄设置为系统默认方式,用户在创建任务后,可以针对某个信号安装自己的信号响应句柄。
5)其他辅助信息。这些信息包括统计上的一些数据,如任务运行总时间、任务最终返回值等。
2.1.2 任务操作函数
前文中说到VxWorks是基于优先级方式的抢占式任务调度操作系统,同时对于相同优先级的任务,支持Round-Robin循环调度方式(以下简称RR调度)。RR调度方式通过kernelTimeSlice函数使能。该函数调用原型如下。
STATUS kernelTimeSlice ( int ticks /* time-slice in ticks or 0 to disable round-robin */ )
RR调度在默认情况下是禁用的。当用户以非零参数明确调用kernelTimeSlice函数后,RR调度方才使能,此时相同优先级的任务可以轮流使用CPU,但是整个系统仍然是按优先级方式进行调度的。换句话说,只有在当前系统中最高优先级下存在多个任务运行时,RR调度才有效,如果存在多个低优先级的任务,那么系统仍然运行着高优先级的任务,这些低优先级任务根本无法使用CPU。注意,kernelTimeSlice函数参数为tick系统时钟嘀答数,一般情况下,VxWorks下一个滴答为1ms,故很多时候直接将参数作为以ms为单位的时间值。如果在调用kernelTimeSlice函数使能RR调度方式后,又想禁用RR调度方式,则只需要以参数0再次调用kernelTimeSlice函数即可。
控制任务调度的另一个函数是taskPrioritySet,该函数通过改变任务优先级来控制调度。注意,taskPrioritySet函数可以任意改变任务的优先级,该函数的调用原型如下。
STATUS taskPrioritySet ( int tid, /* task ID */ int newPriority /* new priority */ )
第一个参数(tid)为需要改变优先级的任务的ID号,这是一个整型值,是调用taskSpawn创建任务时的返回值。第二个参数(newPriority)即对应任务需要设置的优先级,该参数范围为0~255,即可以改变某个任务到任意优先级,而不是只能降低任务的优先级。
注意
一个任务可以在运行过程中通过taskPrioritySet函数按照需要自己改变自己的优先级,此时只需要将第一个参数设置为0即可。这种可以动态改变任务自身优先级的方式给任务的运行提供了极大的灵活性,特别是当任务需要在某个特定时刻从特殊渠道获取数据时,必须提高自身的优先级才能读取到数据,而在读取数据后,又必须退回到正常优先级进行数据的处理,taskPrioritySet函数就是针对这种情况专门设计的。通常,任务不需要使用taskPrioritySet函数。
另外一对控制任务调度的关键函数是taskLock和taskUnlock。函数调用原型分别如下:
STATUS taskLock (void) STATUS taskUnlock (void)
taskLock函数即关闭任务调度,taskUnlock函数即重新开启任务调度,这两个函数必须成对使用。这两个函数通常用于系统级的资源共享上,是一种变相的互斥机制。不过需要注意的是,这两个函数并不禁止中断,所以并不能与在中断上下文中运行的函数形成互斥。另外要注意的是,taskLock并不是信号量,它仅仅是暂时禁止了任务调度机制,保证了接下来的一段时间内(直到调用taskUnlock),当前任务一直主动占用CPU。taskLock主要使用在需要以原子方式执行一段代码的情况下,如对某个变量进行某种操作(如加1或减1)。刚才说到任务主动占用CPU,即没有其他外界手段可以将任务调度出去,但是任务自身可以进入阻塞状态(如阻塞于某个资源,虽然不应在此种方式下调用taskLock),此时taskLock应有的作用将被取消,任务调度机制重新工作,直到该任务又重新被调度运行。换句话说,只要调用taskLock的任务处于运行状态,则任务调度机制一直被禁止,而一旦该任务由于某种原因主动让出CPU,则任务调度机制重新启动。taskLock的作用对于某个任务而言,只有taskUnlock可以取消它。
VxWorks提供任务创建函数taskSpawn,该函数的调用原型如下。
int taskSpawn ( char * name, /* name of new task (stored at pStackBase) */ int priority, /* priority of new task */ int options, /* task option word */ int stackSize, /* size (bytes) of stack needed plus name */ FUNCPTR entryPt, /* entry point of new task */ int arg1, /* 1st of 10 req'd task args to pass to func */ int arg2, int arg3, int arg4, int arg5, int arg6, int arg7, int arg8, int arg9, int arg10 )
① 参数1:char * name
任务名。可以为NULL,此时系统将使用默认的任务名。默认任务名形式为“tN”,其中,“t”为前缀,“N”是一个数字,系统将对所有的任务创建时未提供任务名的任务依次进行编号,从1开始。
② 参数2:int priority
任务优先级。该优先级决定了任务的调度级别。在任务创建完毕后,仍然可以使用taskPrioritySet函数对任务优先级进行动态改变。
③ 参数3:int options
任务选项。用于控制任务的某些行为。任务可用选项如表2-1所示,一般情况下,将该参数设置为0。
表2-1 任务可用选项
④ 参数4:int stackSize
任务栈大小。注意,任务栈在创建任务时指定,此后不可更改。任务从创建到结束自始至终都在使用这个栈。VxWorks下任务栈和表示任务的结构连续存放,在内存分配时是作为一个整体进行分配的。
⑤ 参数5:FUNCPTR entryPt
任务开始执行时入口函数的地址。当任务被调度运行时,从该参数指定的地址开始执行。
⑥ 参数6~15:int arg1~int arg10
入口函数参数,最多可同时传递10个参数,多于10个时,可以通过指针的方式传递。
⑦ 返回值
taskSpawn函数返回一个整型数据,表示刚创建任务的ID。实际上,这个ID是一个内存地址,指向这个被创建任务的TCB(Task Control Block)。故,原则上可以通过以下方式通过任务ID得到任务的TCB:
WIND_TCB *tPcb; tPcb = ((WIND_TCB *) tid); …
但是并不建议这样做,因为对于任务ID的解释可以随着VxWorks内核版本的不同而改变,故要从任务ID得到任务对应的TCB,建议使用内核提供的函数taskTcb。taskTcb函数调用原型如下。
WIND_TCB *taskTcb ( int tid /* task ID */ )
使用方式的代码如下。
WIND_TCB *tPcb; tPcb = taskTcb(0);
注意
taskTcb要求传入任务ID,当以0作为参数传递给该函数时,taskTcb将返回当前任务的TCB结构地址。参数0表示得到当前任务的某种信息,这种处理方式在VxWorks下很常见。
taskSpawn调用举例:
taskSpawn ("demo", 20, 0, 2000, (FUNCPTR)usrDemo, 0,0,0,0,0,0,0,0,0,0); void usrDemo (void) { …… }
taskSpawn函数调用完毕后,任务就进入运行状态,即与其他任务竞争使用CPU。有时创建一个任务后,需要暂时使其处于挂起状态,不具有调度运行的资格,这可以通过调用taskCreate函数完成。taskCreate调用原型如下。
int taskCreate ( char * name, /* name of new task */ int priority, /* priority of new task */ int options, /* task option word */ int stackSize, /* size (bytes) of stack needed */ FUNCPTR entryPt, /* entry point of new task */ int arg1, /* 1st of 10 req'd args to pass to entryPt */ int arg2, int arg3, int arg4, int arg5, int arg6, int arg7, int arg8, int arg9, int arg10 )
taskCreate函数参数与taskSpawn完全一致,其区别于taskSpawn函数之处就在于taskCreate创建后的任务暂不具有运行的资格,需要另一个函数taskActivate来“激活”。taskActivate函数将任务结构加入到ready队列,从而使得任务有资格竞争使用CPU。至于任务确切的运行时间,则根据系统当前任务情况决定。一般而言,某个任务结构被加入到系统ready队列,我们就认为其已经在运行状态,因为余下的情况已经无法由系统和用户掌控。如果用户需要在任务被激活后立刻得到运行,可以设置任务为较高优先级。任务激活函数taskActivate调用原型如下。
STATUS taskActivate ( int tid /* task ID of task to activate */ )
该函数接收taskCreate函数返回的任务ID,对指定ID的任务进行激活,即将任务结构添加到系统任务ready队列中,使得任务成为竞争使用CPU的一员。
2.1.3 深入了解任务栈
任务在VxWorks内核中作为调度的基本单元,每个任务是程序的一个实例,程序可以由单个或多个函数构成。任务在运行这些函数的过程中,不可避免地使用一些函数变量(定义在函数内部的变量),这些函数变量的存放地就在任务栈中;当存在函数间调用时,需要某种机制传递参数,使用较多的就是栈机制。栈实际上就是一块地址连续的内存块,只不过由于其特殊的使用方式,从而被封装成一个特殊的结构类型。一般而言,栈的使用以向下递减地址方式,即栈顶位于内存高地址处,栈底位于内存低地址处,使用时从栈顶开始使用,逐渐向栈底逼近。当然也有相反方向的工作模式。无论方向如何,其目的都是一样的,只要在某个平台保持全局范围内的统一性,就可以保证系统工作的正常性。注意:栈的方向一般是由CPU硬件决定的,更进一步地说,是由指令集决定的。如Intel x86系列提供的push、pop指令就是向下递减栈地址的,操作系统编写者对此必须了解。
通用操作系统进程栈(通用操作系统一般称每个调度单元为进程)分为两个层次,这是由于通用操作系统一般是由两个运行态(内核态和用户态)决定的。通用操作系统需要提供高安全性的运行环境,它必须明确地隔离两个不同层次上的操作,从而知道哪些操作是被允许的,哪些是被禁止的,当然安全措施不仅仅通过运行态来实现,运行态的区分只是其中重要的措施之一。通用操作系统进程在两个不同运行态下具有不同的栈,当进程运行在用户态时,其使用用户态栈,当发生系统调用或由于中断进入到内核态时,其切换到进程的内核态栈。
一般而言,用户态栈是使用不尽的,由平台内存空间决定,地址空间一般不造成限制(如Linux下用户态栈原则上有接近3GB的空间可用),而内核态栈大小是存在很大限制的,如Linux下内核态栈一般只有大约4KB的大小。所以,进程运行在内核态时,要求大空间的结构必须使用堆的方式分配,不要使用栈进行分配。事实上,由于运行在内核态的代码都是严格限制的,所以一般都不会发生内核态栈溢出的问题,而由于用户态栈可使用的空间极大,虽然其先分配给用户态栈的实际内存较小,但是可以根据需要动态地增加用户态栈内存,所以很难使得用户态栈溢出(你能想象一个应用程序可以使用完3GB的栈吗?)。
VxWorks内核不使用通用操作系统下的运行态,虽然VxWorks下也有内核态的概念,不过其本质上就是一个内核数据结构的简单保护机制,仅仅由一个全局变量kernelState表示是否在内核态,当用户进行内核函数调用或者中断发生进入到VxWorks内核运行时,其并不发生运行态的本质切换,而任务自始至终都在使用同一个栈。换句话说,VxWorks下的任务栈既被应用函数(用户编写的函数)使用,也被内核函数(VxWorks内核提供)使用,其VxWorks下任务栈在初始创建后,不可以根据需要在后期动态地增加内存容量。这就对VxWorks任务栈的初始创建大小提出了一个要求,即其在创建栈(即创建任务)时指定的栈大小必须足够大,以满足任务后续运行期间对栈容量的需求,而这一点是很难由用户在创建时进行判断的。故实际上,用户在创建任务时会指定一个比实际需要大得多的栈容量,这对于一个嵌入式系统而言,对宝贵的内存资源造成了极大的浪费。VxWorks官方文档的建议是通过试验法,在程序开发期间,在任务创建时,指定一个比较大的栈,在任务运行期间,不断地使用checkStack函数查看任务的栈使用情况,从而得到一个任务栈使用数据的大致统计情况。其后,用这个得到的统计值作为任务栈的实际容量。checkStack函数的调用原型如下。
void checkStack ( int taskNameOrId /* task name or task ID; 0 = summarize all */ )
参数(taskNameOrId)指定要检查的栈对应任务的ID。以参数0调用该函数时,会打印出系统内所有任务的栈的使用情况。checkStack函数的一个使用实例如下。
-> checkStack tShell NAME ENTRY TID SIZE CUR HIGH MARGIN ------------ ------------ -------- ----- ----- ----- ------ tShell _shell 23e1c789208832 36325576
2.1.4 任务名长度问题
当在Shell下使用“i”命令查看当前系统中的任务时,对于任务名的显示有些令人误解:任务名长度较长的字符串(如大于11B)都发生了截断。这让很多人误解为任务名最大只能有10B的长度。故在创建任务时刻意减少任务名的长度。其实这只是显示上的一些问题,taskShow函数(“i”命令的底层调用函数)在打印任务名时进行了截断操作,而非在taskSpawn函数中对传入的任务名参数进行截断。事实上,VxWorks支持任意长度的任务名。换句话说,在调用taskSpawn或者taskCreate创建任务时,用户可以指定一个其需要的任何名称。VxWorks内核在内部以无损方式保存了这个用户传入的任意名称。只是在调用taskShow对任务信息进行显示时,在打印格式上的一些操作只打印出了任务名前11B。用户可以根据需要自编写一个任务信息显示函数,将任务名称全部打印出来。
VxWorks下并不要求任务名的全局唯一性,两个完全不同的任务可以使用相同的名称而不会造成底层(内核)使用上的任何问题,但是这会让查看任务信息的用户产生疑问。同样,上文中讨论到的taskShow对于任务名的截断显示也会造成同样的问题。故如果仍然使用内核提供的taskShow显示任务信息,则建议用户在任务创建时尽量将任务限制在11B之内,或者两个不同任务的名称最好在前11B内有所区别。任务信息显示函数taskShow调用原型如下。
STATUS taskShow ( int tid, /* task ID */ int level /* 0 = summary, 1 = details, 2 = all tasks */ )
参数1(tid):需要显示信息的任务ID。
参数2(level):任务信息开放程度。
注意
taskShow根据第2 个参数值对任务信息和任务范围做不同程度的信息显示。当参数2传入的值为2时,即对系统内所有的任务进行信息显示,此时将忽略第1个参数值;否则将只显示参数1指定的任务信息。而参数2的值表示显示任务信息的详细程度。0表示只显示大概信息,这些信息包括函数名称、入口函数、任务栈使用情况总结;1表示显示任务的详细信息,这些信息除了以上所说的大概信息外,还包括任务栈的详细信息(栈基址、栈容量)、任务所包含的事件、任务当前寄存器值。
2.1.5 正确结束任务
除了极少数几个任务自始至终保持存活外,大多数任务都只是在一段时间内保持活动,最后都不可避免地要消亡。消亡有两种方式:一是任务正常运行到结束,通过exit函数结束;二是被直接删除,即非正常方式结束。一般情况下,用户创建一个任务,完成某个特定的功能,最后“功成身退”,从系统中消失,这是一般任务的工作方式。例如,任务执行一个main函数,最后通过reture返回。代码在编译时,编译环境实际上在函数最后加上了一个exit函数,这是对用户透明的,用户只需要调用reture语句或者直接退出都可以。但是从底层代码运行来看,在运行完所有的用户代码后,还会执行一个exit函数,这个函数是由编译环境在编译用户程序时自动加入的,与在函数结束时加上一个exit函数的道理相同。实际上,编译环境在调用用户提供的入口函数之前,也加上了一个类似于init的函数,创建程序的运行环境,进而调用用户提供的入口函数,真正执行用户的代码。这些机制有些类似于C++中自动加入的初始化(constructor)和消亡(destructor)函数。
VxWorks下exit函数用于结束一个任务的寿命,其底层调用windDelete函数,完成表示任务的数据结构的释放和任务栈的释放。注意:windDelete只释放任务栈和表示任务本身的结构,并不负责释放用户在任务运行过程中分配的任何内存空间。exit函数用于一个任务正常的自动消亡。内核同样提供了一个非正常结束任务的方式:taskDelete函数,该函数的调用原型如下。
STATUS taskDelete ( int tid /* task ID of task to delete */ )
注意
当以参数0调用taskDelete时,表示删除当前运行的任务自身。此时的功能类似于被动调用exit函数退出。一般而言,如果一个任务需要删除自身,那么直接调用exit即可,当然也可以以参数0调用taskDelete。但是taskDelete更多地被用于删除一个其他正常运行的任务,而非自身。该函数提供的功能必须慎用。通常对于普通的用户不需要调用这么一个任务删除函数,这个函数极容易造成内核的不一致性,从而导致内核崩溃。设想一个任务刚刚获取一个资源的使用权,正在所谓的“关键区域”执行代码,而另外一个任务却“霸道”地将其直接删除,此时被删除任务将停止运行余下的代码,表示任务的结构和任务栈被释放,进而造成内核资源状态的不一致性,因为任务退出之前没有相应地释放其已获取资源的使用权,导致其他任何竞争使用该资源的任务处于无限等待状态。以一个简单的例子说明,如多个函数需要竞争使用同一项资源,该资源有一个变量进行保护,任何使用该资源的任务在使用之前必须检查该变量的值,如果该变量为1,则表示资源可用;若为0,则表示资源已被其他的任务使用,所有其他的任务必须等待,直到该变量重新变为1。为了保证对变量本身操作的原子性,一般会使用某种特定编码方式,如在Intel x86结构下,会使用lock指令修改该变量的值。
某个竞争使用资源的任务准备使用资源时,其检查该变量的值,如果为0,则等待,等待一段时间后,其重新检查该变量的值,此时该变量值为1,表示资源可以访问,那么任务将会将该变量的值设置为0,表示“我”这个任务现在正在使用资源,其他任务必须等待。而在该任务使用资源期间,一个“外来不明是非者”将这个任务进行了删除,即中断了正在使用资源的任务的执行,致使其非正常消亡,即系统中已经不存在这个任务了,那么这个任务原先在使用完资源后将该变量重新设置为1,表示资源可用的代码永远也无法得到运行。换句话说,该资源被一个“死亡”的任务无限期地“使用”,所有其他竞争使用该资源的任务都无限期地处于等待状态。所以,不正当地删除一个正在运行的其他任务是非常危险的一个操作,除非明确知道被删除的任务当时所处的状态,如被删除任务已经设置了一个标志位,表示它可被删除,因为其已经释放了它所使用的一切资源,正在处于“无为”状态,但是在外界其他某个条件满足之前,其又不能提前结束,故等待其他任务对其进行删除。但是这种情况除了某个特殊情况,一般都不会发生。所以,taskDelete函数的使用场合有限,用户只需对其进行了解即可,建议不要使用。
与taskDelete“分庭抗礼”的一对函数就是taskSafe和taskUnsafe。一个任务为了防止在预先无任何提示的情况下被其他任务删除,其可以调用taskSafe函数进行自身的保护,一个任务在调用taskSafe函数后,则其他任何任务都不可对其进行删除操作,这个函数就是为了应对taskDelete而设计的,以防止上文所述的一个处于“关键区域”的任务被野蛮删除的行为发生。taskSafe和taskUnsafe函数调用原型如下。
STATUS taskSafe (void) STATUS taskUnsafe (void)
如下代码列举了这两个函数的典型应用环境。
taskSafe (); semTake (semId, WAIT_FOREVER); /* Block until semaphore available */ . . /* critical region code */ . semGive (semId); /* Release semaphore */ taskUnsafe ();
VxWorks内核还提供如下三个函数对运行中的任务施加影响。第一个是taskSuspend,即挂起一个正在执行的函数,该函数调用原型如下。
STATUS taskSuspend ( int tid /* task ID of task to suspend */ )
当以参数0调用taskSuspend时,表示挂起当前正常执行的任务。而以非0任务ID方式指定一个其他被挂起的任务。这个其他被挂起的任务原先可以是由于优先级较低而等待运行,也可以是一个处于延迟(delay)睡眠的任务或者是一个已被挂起的任务。
第二个函数taskResume的功能是取消taskSuspend的作用,其将一个被挂起的任务重新设置为可运行状态。当然,此时被恢复的任务一定是一个其他任务,因为一个任务是不可能Resume其自身的。taskResume函数的调用原型如下。
STATUS taskResume ( int tid /* task ID of task to resume */ )
第三个比较重要的控制任务运行的函数是taskDelay,即置当前正在运行的任务为睡眠状态,睡眠时间长度以调用taskDelay时输入的参数为据。taskDelay函数的调用原型如下。
STATUS taskDelay ( int ticks /* number of ticks to delay task */ )
注意
参数以系统滴答数为单位,即以系统时钟的时间间隔为单位。一般而言,这个间隔为1ms,所以可以简单地认为是毫秒级的任务延迟。任务调用taskDelay进行延迟在代码中使用较多,特别是以轮询方式对某个设备进行操作的任务一般都是以while循环加taskDelay的方式工作。
另外要注意taskDelay的一种特殊工作方式,就是以NO_WAIT参数调用taskDelay,此种方式主要是在RR调度禁止使用的前提下,给相同优先级的其他任务一个使用CPU的机会。当以NO_WAIT调用taskDelay,VxWorks内核将当前任务置于ready队列中所有相同优先级的任务之后,从而给相同优先级的其他任务提供一个运行的机会。注意,这种方式一般使用在RR调度被禁用的情况下。如果使能了RR调度,则一般无须使用此种方式,因为RR调度会循环让相同优先级的任务使用CPU资源的。
2.1.6 任务的钩子函数——黑客机制
涉及VxWorks任务一个重要的使用是可以在任务创建、消亡、调度之时调用用户注册的钩子函数,这在某些情况下会特别有意义。VxWorks提供一种机制可以让用户注册一种钩子函数,当系统中有新的任务被创建,一个任务消亡以及任务调度时,可以执行用户注册的这些函数,从而达到用户的特殊目的。如对于低功耗的实现,可以利用VxWorks内核提供的这种机制,虽然这不是很优美的一种实现,但可以用于说明钩子函数的作用。低功耗要求当CPU中没有用户任务运行时,将整个平台尽量置于低功耗状态。当VxWorks下没有用户任务时,VxWorks将执行其自身提供的一个Idle任务,Idle任务具有最低优先级。换句话说,只要存在一个可运行的用户任务,无论这个任务的优先级是什么,其都将被调度运行,如此,我们可以使用一个最低优先级的后台任务加上一个任务调度钩子函数实现平台低功耗目的。在系统初始化阶段,创建一个最低优先级的后台任务,这个任务的优先级要低于所有的有“责任”在身的其他任务。换句话说,当这个后台任务被调度运行时,就表示当前系统可以处于低功耗模式。我们同时注册一个调度任务时被调用的钩子函数,后台任务实现代码就是一个FOREVER语句,其伪代码如下。
FOREVER //等同于for(;;) { if(powerdown==FALSE){ put board to power down state; powerdown=TRUE; } }
钩子函数实现如下。
void taskSwitchHook(WIND_TCB *pOldTcb, WIND_TCB *pNewTcb){ if(old task is our daemon task){ /*即后台任务被其他任务替代了*/ put board back to Normal state; powerdown=FALSE; } }
一般而言,将平台从低功耗模式唤醒的方式有很多种,如可以使用一个外部中断专门用于唤醒平台,使脱离低功耗模式,上述代码只是一个简单示例,要使用到实际中需要很多考虑,但是这给我们提供了钩子函数的一种使用方式。
VxWorks提供的钩子函数注册和注销如下。
① 任务创建钩子函数注册和注销。
STATUS taskCreateHookAdd ( FUNCPTR createHook /* routine to be called when a task is created */ )
参数为注册的钩子函数,其将在任何一个新任务创建时被调用,它必须具有的定义形式如下:即以新创建任务的结构为参数。钩子函数可以这个代表新创建任务的结构做一些个性化的处理,如检查优先级使其限定在特定的范围等。
void createHook ( WIND_TCB *pNewTcb /* pointer to new task's TCB */ )
taskCreateHookDelete用以注销之前注册的钩子函数,其调用原型如下。
STATUS taskCreateHookDelete ( FUNCPTR createHook /* routine to be deleted from list */ )
② 任务调度钩子函数注册和注销。
STATUS taskSwitchHookAdd ( FUNCPTR switchHook /* routine to be called at every task switch */ )
taskSwitchHookAdd用于注册一个发生任务调度时调用的钩子函数,该钩子函数必须具有如下的定义形式。(其中,pOldTcb表示被调度出去的任务结构,而pNewTcb则表示被调度进来成为CPU新的使用者的任务结构。)
void switchHook ( WIND_TCB *pOldTcb, /* pointer to old task's WIND_TCB */ WIND_TCB *pNewTcb /* pointer to new task's WIND_TCB */ )
taskSwitchHookDelete用以注销通过taskSwitchHookAdd添加的钩子函数。其调用原型如下。
STATUS taskSwitchHookDelete ( FUNCPTR switchHook /* routine to be deleted from list */ )
③ 任务消亡(删除)钩子函数注册和注销。
STATUS taskDeleteHookAdd ( FUNCPTR deleteHook /* routine to be called when a task is deleted */ )
上述代码用以注册在任何一个任务消亡时被调用的钩子函数。钩子函数deleteHook必须具有如下的定义形式,参数是消亡的任务结构。
void deleteHook ( WIND_TCB *pTcb /* pointer to deleted task's WIND_TCB */ )
taskDeleteHookDelete用以注销之前通过taskDeleteHookAdd注册的钩子函数。其调用原型如下。
STATUS taskDeleteHookDelete ( FUNCPTR deleteHook /* routine to be deleted from list */ )
除了这些注册和注销函数外,VxWorks同时还提供了三个信息显示函数用于显示当前注册到系统中的所有钩子函数。这三个函数的调用原型如下。
void taskCreateHookShow (void)
显示当前注册到系统的所有在任务创建时被调用的钩子函数列表。
void taskDeleteHookShow (void)
显示当前注册到系统中所有在发生任务切换时被调用的钩子函数列表。
void taskSwitchHookShow (void)
显示当前注册到系统中的在任务消亡时被调用的钩子函数列表。
2.1.7 任务小结
至此,我们完成对VxWorks任务的说明,VxWorks下的任务即通用操作系统下所说的进程是内核的基本运行单元,也是内核调度算法的处理单元。纵观所有的操作系统,进程或者任务在底层上都由一个数据结构表示,这个数据结构一般称为任务(或者进程)控制块TCB(或者PCB),用以保存一个任务(或者CPU执行单元)的所有信息,这些信息中有些是保证一个任务可以被CPU运行的基本关键信息(如所有的寄存器值、内存映射),但很多是为管理需要而存在的“辅助”信息,这些“辅助”信息构成了某个操作系统的实现方式。
VxWorks任务给用户提供了极大的灵活性,如果深入到任务控制结构这一层面(即内核编程和驱动编程),那么可以对任务执行其任何需要的操作,即便让系统立刻崩溃也在所不惜。这也是底层编程提供的极大“娱乐性”。
VxWorks任务的几个重要方面总结如下:
● 任务具有优先级,是任务调度运行的依据和获取CPU资源的“竞争卡”,优先级越高,其获得CPU资源的可能性就越大,VxWorks以0~255的数字表示优先级,数值越大,优先级越小,0表示最高优先级。任务优先级可以在任务运行过程中动态地改变,可以改变一个任务到任意优先级。
● 任务具有任务栈。任务栈的容量在任务创建时就被确定,而且其后不可进行更改。任务栈与表示任务的内核结构作为一段连续的区域进行分配。任务栈是任务运行过程中各种局部函数变量的存放地和函数调用参数传递的渠道。由于VxWorks下任务栈容量不可动态改变,故在创建任务时必须指定一个足够大的容量,通常可以在函数调试阶段通过checkStack统计出一个任务的栈使用情况,进而指定一个合理的任务栈容量,避免浪费较多的内存空间。
● VxWorks提供了一种机制可以在涉及任务的三个关键状态变化出调用用户注册的钩子函数,这三个状态变化为任务初始创建时、任务被调度使用或放弃CPU时、任务消亡时。通过利用VxWorks提供的这种灵活性可以实现一些嵌入式平台下有意义的策略。
● 每个任务在操作系统底层的实现上都是由一个数据结构表示的,通过直接更改这个数据结构中的某些字段值,可以控制任务某些运行行为,这在内核层编程时十分有效。VxWorks任务结构定义在taskLib.h头文件中。感兴趣的用户可以查看VxWorks任务结构WIND_TCB的具体定义。