内核实现基本原理
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下输入的任何命令。以上只是推荐值,事实上,由于嵌入式系统下的特殊应用,对于某个任务优先级的设置需要根据该任务完成的具体工作而定,而不可一味地遵循推荐值。要谨记在任务达到某一特殊目的后,必须将任务优先级设置回正常(推荐)值。
无论何种操作系统,任务在设计中都由一个数据结构表示。这个数据结构包含一个任务运行时需要的所有信息,我们一般将这些信息称为任务上下文。具体的任务上下文(广义上)包括以下内容:
1)所在平台CPU 内部所有的寄存器值,特别是指令寄存器,这代表了任务当前的执行点。这一般是狭义上的任务上下文。除了寄存器值,每个任务有自己的内存映射空间、任务名称、任务优先级值、任务入口函数地址、打开文件句柄数组、信号量和用于各种目的的队列等。
2)任务运行时暂时存放函数变量以及函数调用时被传递参数的栈。从操作系统底层实现来看,很多操作系统将表示任务的数据结构和任务栈统一管理,如Linux 下在分配任务结构的同时分配任务的内核栈,这两者作为一个整体进行内存分配,通常将一页(如4KB)的开始部分作为任务结构,用以存储任务关键信息,而将页的末尾作为任务内核栈的顶部。如此,实际上用一页页面的大小(通常为4KB)减去任务信息占据的空间,剩下的空间都作为任务的内核栈在使用。VxWorks 与Linux 在栈的分配和管理上基本类似,不过VxWorks 区分于Linux 的一个最大不同是VxWorks 下所有的代码都运行在一个状态下,不区分内核态和用户
态。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)其他辅助信息。这些信息包括统计上的一些数据,如任务运行总时间、任务最终返回值等。