VxWorks 6.9 内核编程指导之读书笔记 -- 多任务
- 概述
- VxWork系统任务
- 任务调度
- 任务创建和管理
- 任务的错误状态
- 任务异常处理
- 共享代码和重入
概述
现代实时操作系统是基于多任务和任务间通信的概念的。多任务环境运行一个实时进程RTP可以被作为一系列相互独立的任务集,每一个任务都有自己的执行线程和系统资源。任务是VxWorks调度的基本单元。所有任务,不管是在内核中,还是进程中,使用相同的调度(VxWorks进程本身不被调度)。
任务的概念与其他操作系统的线程概念比较类似。多任务为应用程序对多个孤立的实时事件的控制和反应对提供了基础。在单核处理器(UP)系统中,多任务呈现了多个任务同时执行的表象,实际上,内核在调度策略的基础上交替执行这些任务。
每个任务都有自己的上下文,即任务被内核调度运行时能访问的CPU环境和系统资源。上下文切换时,认为的上下文被保存在任务控制块中(TCB)。
TCB包含
- 执行的线程;即任务的程序计数器
- 任务的虚拟内存上下文(如果CPU支持且被包含)
- CPU寄存器和可选的协处理器的寄存器
- 动态变量和函数调用的栈
- 分配给标准输入、输出和错误的I/O
- 一个延时定时器
- 内核控制结构
- 信号处理器
- 任务的私有环境(环境变量)错误状态(errno)
- 调试和性能监视值
如果,VxWorks禁用RTP的支持(INCULDE_RTP),任务上下文不包含它的虚拟内存。所有任务仅运行在单一地址空间(内核)。
然而,如果VxWorks启用了RTP的支持,不管进程是否被激活,内核任务的上下文包含了虚拟内存上下文,因为系统有可能与内核之外的虚拟内存上下文打交道。即,系统可能有任务运行在几个不同的虚拟内存上下文中(内核和一个或多个进程)。
任务的状态和转变
内核维护在系统中的任务的当前状态。作为活动结果,任务将从一个状态转化为另一个状态,如被应用程序调用的某种函数(如在信号量无法使用时,试图获取信号量)和使用开发工具如调试器。
在就绪状态的最高优先级的任务将被执行。当任务用taskSpawn创建时,它们立即进入就绪状态。
当任务通过taskCreate或带有VX_TASK_NOACTIVATE选项参数的taskOpen函数创建时,它们被实例化为suspend状态。稍后可以调用taskActivate来使它们进入就绪状态。激活阶段非常快,使得应用程序能以及时的方式创建和激活它们。
任务状态
状态 | 描述 |
READY | 任务除了等待CPU没有等待其它任何资源。 |
PEND | 由于资源不可用,任务在等待资源。 |
DELAY | 任务在睡眠。 |
SUSPEND |
任务无法执行(不是PEND或DELAY)。这种状态主要用于调试。挂起不会禁止状态转变,只是执行。因此,pended-suspended任务仍然不会阻塞, delayed-suspended任务能够被唤醒。 |
STOP | 任务被调试器停止(也被错误检测和报告功能使用) |
DELAY + S | 任务既处于delayed,也处于suspended |
PEND + S | 任务既PENDED也SUSPENDED |
PEND + T | 任务被搁置带有超时值 |
STOP + P | 任务被搁置和停止(被调试器,错误检测和报告功能,或SIGSTOP信号) |
STOP + S | 任务被停止和挂起 |
STOP + T | 任务被延时和停止(被调试器,错误检测和报告功能,或SIGSTOP信号) |
PEND + S + T | 任务被搁置带有超时值并挂起 |
STOP + P + S | 任务被调试器搁置,挂起和停止 |
STOP + P + T | 任务被调试器搁置带有超时和停止 |
STOP + T + S | 任务被调试器挂起,延时和停止 |
ST + P + S + T | 任务被调试器搁置带有超时,挂起和停止 |
state + I | 任务被state指定(任何状态或上面状态的组合)加上内在的优先级。 |
STOP状态被调试器使用,当断点被执行时。也可以被错误检测和报告功能使用。
状态转变
VxWorks的系统任务
VxWorks在引导时启动的系统任务依赖于配置,有些总是运行。任务集与VxWorks的基本配置相关,很少的任务常用于可选的组件。
注意:别挂起、删除或改变任何系统任务的优先级。否则将导致不可预期的系统行为。
更多信息请看另外一篇随笔
任务调度
多任务要求任务调度器分配CPU给就绪的任务。VxWorks提供了以下调度选项:
- 传统的调度,提供基于优先级,抢占式及循环调度。
- VxWorks的POSIX线程调度,专门为RTP设计。
- 自定义调度框架,允许你开发自己的调度算法。
任务优先级
任务调度依赖于创建任务时指定的优先级。内核提供了256种优先级,0到255。0是最高优先级,255是最低优先级。在创建时指定了优先级,但是也可以在稍后编程修改它的优先级。
应用程序任务优先级
所有应用程序的任务优先级都在范围100到255.
注意:网络应用程序任务的优先级可以在100以内。
驱动任务优先级
相对与应用程序的任务优先级从100到255,它的任务优先级(与ISR相关的任务)可以是51到99。这些任务比较关键;举个例子,如果从芯片拷贝数据的任务失败,设备将丢失数据。驱动支持的任务包括tNet0,HDLC等。
tNet0的优先级是50,所以用户不应该分配的优先级低于该任务。如果低于,则将导致网络连接挂掉并阻止主机工具的调试功能。
VxWorks传统调度器
传统调度提供了基于优先级的抢占式调度及可选的可编程的循环调度。传统调度可以参考original或native调度。
传统调度默认被INCLUDE_VX_TRADITIONAL_SCHEDULER组件包含。
基于优先级的抢占式调度
当更高优先级的任务就绪时,当前任务被抢占。因此,CPU总是确保就绪的最高优先级的任务被执行。
这种调度策略的缺点是,多个相同优先级的任务必须共享处理器,如果单个任务永远不阻塞,它可能霸占CPU。因此其它相同优先级的任务将永远不会被执行。循环调度解决了这个办法。
调度和就绪队列
调度器维护了一个FIFO的队列,该队列包含了在系统中每个优先级的所有就绪的任务。当CPU可用时,排在前面的任务被调度。
任务在就绪队列中位置可能改变,取决于以下因素:
- 如果认为被抢占,调度器运行高优先级任务,但是被抢占的任务仍然保留在该优先级列表中的前面的位置。
- 如果认为被搁置,延时,挂起或停止,它将从就绪列表中被删除。当它随后再次就绪时,它被放在该优先级列表的尾部。
- 如果优先级通过taskPrioritySet被改变,它被放在心优先级列表的尾部。
- 如果认为的优先级是基于互斥信号量优先级而临时提高的,它执行之后将被放在它本来优先级的列表的尾部。
移动任务到优先级列表的尾端
taskRotate函数用于移动任务从优先级列表的前部到尾部。如taskRotate(100),它将把从100优先级列表中的前面的任务移到尾部。如果要移动本任务到尾部,则使用TASK_PRIORITY_SELF作为参数传递给taskRotate函数,而不是优先级的数值作为参数。该函数可用于替代循环调度。它可以控制相同优先级的就绪任务之间共享CPU,而不是让系统以相同的间隔来决定。
循环调度
循环调度是对于基于优先级调度的扩展。循环调度运行相同优先级的任务共享CPU。循环调度通过时间片来共享CPU。相同优先级的一组任务中的每一个任务在交出CPU之前,都执行一定的时间间隔,或时间片。因此,它们中没有哪个任务可以一直占有CPU。时间片用完则该任务将被放在该优先级列表中的尾部。
VxWorks的循环调度不像其它操作系统,它的任务是可以被更高优先级任务抢占的,更高优先级完成后,它继续执行未完成的任务。
启用循环调度
可以使用参数时间片来调用kernelTimeSlice启用循环调度。如果参数为0,则禁用循环调度。
时间片计数和抢占
每个任务可以执行相同的时间片。如果循环调度启用,抢占被执行的任务也启用,系统的tick处理器将增加时间片计数。当指定的时间片用完,系统tick处理器将清0任务的时间片计数,任务被放在该优先级列表的尾部。启用循环调度不影响任务上下文切换的性能,也不分配额外的内存。
如果任务被更高优先级任务抢占或阻塞,则它的时间片计数将被保存,并在继续执行时,执行剩余的时间。由于任务被抢占,任务实际执行的时间比分配给它的时间片多一点或少一点都是可能的。
任务状态错误码
ANSI C标准中有errno一个全局整数值来存放错误码。然而,errno也被定义成一个宏,对于VxWorks的所有函数来说,该宏定义为调用__errno()。而对于其它的函数来说,该宏定义为ANSI C标准中errno的地址,因此对于标准ANSI C的函数,该宏其实就是errno整数值。
在VxWorks,errno是一个预定义全局变量,可以被应用程序直接使用。然而,对于多任务的环境,每个任务必须访问自己的errno。因此errno被内核作为任务的上下文保存和恢复。
类似的,中断服务程序也有errno。
几乎所有的VxWorks函数都遵守一个约定:通过返回值来指示函数成功或失败。很多函数仅返回成功或失败;有些函数通常返回非负值表示成功,返回ERROR表示失败。返回指针的函数,通常返回NULL(0)表示失败。很多情况,返回错误的情况下也设置了errno值来指明发生什么错误。
全局的errno永远不会被VxWorks清除,因此,它的值总是指示最后发生的错误。当VxWorks函数调用别的函数发生错误时,它不会修改底层的errno,而是返回自己的错误,因此,你可以得到errno来查找底层发送的错误。建议你也使用这种机制。可以通过printErrno来显示错误信息,如果errno有相应的字符串常量在错误状态符号表中(statSymTbl)。
错误值的分配
errno使用最重要的2个自己来表示发生错误的模块,用最不重要的2个自己来表示发生的错误。所有VxWorks模块都在范围1到500。带有0模块号的errno用于源码兼容。
所有其他errno值(大于或等于(501<<16)正数和所有负数)都可以被应用程序使用。更多信息参考errnoLib类库。
异常处理
程序的代码和数据发生错误会引起异常条件如非法指令,总线或地址错误,被0整除等。VxWorks的异常处理包会处理这些异常。
如果为某个异常使用自己的异常处理,则通过signal会捕获到异常,但默认的异常处理将被禁用。用户自定义异常对于从灾难中恢复是非常有用的。通常setjmp和longjmp会被使用。
共享代码和重入
代码被几个任务同时调用而不会冲突,则该代码是可共享的,也是可重入的。大多数Vxworks的函数是可重入,但是也有一些是不可重入的。如果函数有相应的_r版本的函数,则该函数是不可重入的,而_r函数是可重入的,如ldiv()函数有相应的ldiv_r(),所以ldiv是不可重入的,而ldiv_r()是可重入的。
I/O和驱动是可重入,但是要求应用程序小心设计。对于带缓冲的I/O,风河公司建议使用基于每个任务的文件指针缓存。在驱动层,由于全局文件描述符表,很可能从不同的任务的流中加载到缓存。如报文驱动可能从不同的任务接收报文混合到流中,每个报文都有自己的目的地。
VxWorks使用了以下重入技术
- 动态栈变量 -- 没有自己的数据,数据都是作为参数传递进来。
- 用信号量保护的全局和静态变量
- 任务变量
注意:在有些情况无法重入。此时应该使用二进制信号量来保护,或者在ISR中使用intLock和intUnlock。初始化函数应该可以被调用多次,即使逻辑上应该只能被调用一次。作为规则,函数应该避免使用静态变量来保存状态信息。但是初始化函数是个例外;使用静态变量来返回初始化函数成功与否是合适的。