1、第1章 wind 内 核1.1 内 核 概 述VxWorks 操作系统内核称为 wind 内核,下面从实时性能、核结构、调度特点等方面初步探讨。1.1.1 实时内核“实时”表示控制系统能够及时处理系统中发生的要求控制的外部事件。从事件发生到系统产生响应的反应时间称为延迟(Latency) 。对于实时系统,一个最重要的条件就是延迟有确定的上界(这样的系统属于确定性系统) 。满足这个条件后,根据这个上界大小再区分不同实时系统的性能。这里,“系统”是从系统论的观点讲的一个功能完整的设计,能够独立和外部世界交互,实现预期功能,包括实时硬件系统设计、实时操作系统设计、实时多任务设计 3 部分。后两者可以
2、概括为实时软件系统设计。实现实时系统是这 3 部分有机结合的结果。从另外一个角度,即实时程度看,可以把系统分为硬实时系统和软实时系统。硬实时系统是这样一种系统,它的时间要求有一个确定的底线(Deadline) ,超出底线的响应属于错误的结果,系统将会崩溃,上面所说的实时系统属于硬实时系统。对于软实时系统来说, “实时性”是个程度概念,在提交诸如中断、计时和调度的操作系统服务时,系统定义一个时间范围内的延迟。在该范围内,越早给出响应越有价值,只要不超出范围,晚点给出的结果价值下降,但可以容忍。1实时硬件系统设计实时硬件系统设计是其他两部分的基础。实时硬件系统设计要求满足在软件系统充分高效的前提下
3、,必须提供足够的处理能力。例如,硬件系统提供的中断处理逻辑能同时响应的外部事件数量、硬件反应时间、内存大小、处理器计算能力、总线能力等,以保证最坏情况下所有计算仍然得以完成。多处理的硬件系统还包括内部通信速率设计。当硬件系统不能保证达到实时要求时,可以确信整个系统不是实时的。目前,各种硬件速度不断提高,先进技术大量涌现,硬件在大多数应用中已经不是实时系统的瓶颈。因而,实时系统的关键集中在实时软件系统设计,这方面也成了实时性研究的主要内容,也是最复杂的部分。许多场合甚至对实时系统和实时操作系统不加区分。2实时操作系统设计先来看实时操作系统性能评价的几个主要指标: 中断延迟时间:从接收中断信号操作
4、系统做出响应,并完成进入中断服务程序的时间; 任务切换时间:多任务之间进行切换所花费的时间; 系统响应时间:系统在发出处理要求到系统给出应答信号的时间。系统响应时间从整体上评价操作系统,综合了前面两个指标。从实时性角度看,操作系统经历了前后台系统、分时操作系统和实时操作系统 3 个 阶段。前后台系统其实没有操作系统,系统中只运行一个无限主循环,没有多任务的概念,但是通过中断服务程序响应外部事件。在前后台系统中,对外部事件的实时响应特性从两方面看。 (1)中断延迟:主循环一般保持中断开放状态,因此前后台系统中断响应非常快,并且通常允许嵌套;(2)系统响应时间:需要经历一次主循环才能对中断服务程序
5、中采集的外部请求进行处理,因此系统响应时间决定于主循环周期。分时操作系统将系统计算能力分成时间片,按照一定的策略分配给各个任务,通常在分配过程中追求某种意义上的公平。分时操作系统不保证实时性。实时操作系统(Real Time OS,RTOS)的目的是实现对外部事件的实时响应,即根据前面对实时性的定义,实时操作系统必须在确定的时间内给出响应。实时操作系统必须满足下面几个条件: 可抢占的内核; 可抢占的优先级调度; 中断优先级; 中断可嵌套; 系统服务的优先级由请求该服务的任务的优先级确定; 优先级保护(优先级翻转保护) ; 前述实时操作系统性能评价指标具有固定上界。满足上面的必要条件后,内核内部
6、具体的实现机制就决定了其实时性的优劣。VxWorks 的 wind 是一个真正的实时微内核,满足上述条件。同时,wind 采取单一实时地址空间,任务切换开销非常低,相当于在 UNIX 这样的主机上切换到相同进程内的另一个线程,并且没有系统调用开销。高效的实时设计使 wind 在从工业现场控制到国防、航空等众多领域中表现出优秀的实时性。3实时多任务设计具备前面两个实时条件后,实时系统的最终实现就取决于实时多任务设计。这部分是最能体现系统设计者艺术的部分,也是最有挑战性的部分。这部分设计内容涵盖了一个完整的软件工程过程:需求分析(需求建模) 、概要设计(设计建模) 、模块设计、模块实现、调试、发布
7、。和一般软件工程过程不同的是上述每一步中都需要考虑对实时性的满足,因此更加复杂、也许需要专门通过一本书的篇幅对此进行探讨。我们只简单分析设计过程中实时多任务设计需要面临的关键问题:多任务划分、多任务分配、多任务调度。 “关键”是因为它们是决定系统实时性的主要因素,具体设计时还存在其他一些问题,如任务间通信机制选择,中断服务程序设计等,都对系统实时性产生重大影响,但不属于本质的问题。对多任务划分、多任务分配和多任务调度的设计对应软件工程过程的概要设计和模块设计阶段。多任务划分即如何将整个系统功能设计为不同的任务来实现,任务之间采取怎样的耦合关系,划分的粒度如何等。这里, “任务”也包括中断,因此
8、也包括将什么功能放在中断中实现,什么功能放在常规的任务中处理。在根据数据流划分任务时,影响划分的要素包括数据流之间的并行和串行关系;根据控制流划分任务时,考虑的要素是控制的因果关系。多任务划分影响着多任务分配和多任务调度。多任务分配决定任务放在哪个处理器上完成(存在多个处理器时) ,以及网络环境下任务如何分配。多任务划分目的的实现需要多任务调度,多任务调度的设计目的是关键任务得到实时响应,同时整体上所有任务的设计内容都在允许的时间内完成。多任务调度的内容包括系统调度策略的选择,任务优先级的确定,以及任务间竞争和合作的设计。在划分多任务时,已经考虑了各任务所担任职责的轻重缓急,多任务调度时需要根
9、据这种紧急程度分配合理的优先级;调度还必须使不同优先级的协作任务有效地同步。多任务划分、多任务分配和多任务调度三者是有机的整体,而多任务划分则是其中决定性的部分。任何一个因素设计不合理都将影响整个系统的实时性。1.1.2 微内核传统上,一个操作系统分为核心态和用户态。内核在核心态运行,为用户态的应用程序服务。内核是操作系统的灵魂和中心,决定了操作系统的效率和应用领域。在设计操作系统时,内核包含哪些功能以及内核功能采取何种组织结构,都是由设计者决定的。我们从内核功能和结构特点看,具有整体式内核、层次式内核、微内核三种不同形式。整体式内核结构的操作系统实质上“无结构” 。操作系统功能由一系列模块堆
10、砌而成,任何模块之间可进行任意调用。整体式内核结构的操作系统不进行任何的数据封装和隐藏,在具有较高效率的同时,存在着难以扩展和升级的缺点。CP/M 和 MS-DOS 属于此类结构的操作系统。层次式内核结构的操作系统将模块功能划分为不同层次,下层模块封装内部细节,上层模块调用下层模块提供的接口。UNIX ,LINUX ,VAX/VMS,MULTICS 等属于层次结构操作系统。层次化使操作系统结构简单,易于调试和扩展。两种操作系统的内核结构如图 1-1 所示。不管整体式结构,还是层次式结构,它们的操作系统都包括了许多将其用于各种可能领域时需要的功能,故被称为宏内核操作系统,以至可以认为该内核本身便
11、是一个完整的操作系统。以 UNIX 为例,其内核包括了进程管理、文件系统、设备管理、网络通信等功能,用户层仅仅提供一个操作系统外壳和一些实用工具程序。(a)整体式结构 (b)层次式结构图 1-1 内核结构嵌入式操作系统大多采用微内核结构。微内核操作系统是近二十年新发展起来的技术,内核非常小但效率高,从数十 KB 到数百 KB 字节,适合于资源相对有限的嵌入式应用。微内核将很多通用操作的功能从内核中分离出来(如文件系统,设备驱动,网络协议栈等) ,只保留最基本的内容。一般认为微内核操作系统具有如下优点: 统一的接口,在用户态和核心态之间无需进程识别; 可伸缩性好,易于扩充,能适应硬件更新和应用变
12、化; 可移植性好,操作系统要移植到不同的硬件平台上,只需修改微内核中极少代码即可; 实时性好,内核响应速度快,可以方便地支持实时处理; 安全可靠性高,微内核将安全性作为系统内部特性来进行设计,对外仅使用少量应用编程接口; 适合分布式计算环境。内核为进程传递消息的方式天然适合 RPC 这一计算模式。由于操作系统核心常驻内存,而微内核结构精简了操作系统的核心功能,内核规模比较小,一些功能都移到了外存上,所以微内核结构十分适合嵌入式的专用系统,如图 1-2 所示的 wind 微内核结构。微内核的不同实现模式从宏内核系统到微内核,操作系统结构发生了根本性的变化,导致的影响是多方面的。除了上面说明的微内
13、核的优越性外,微内核的批评者也指出了微内核的缺陷:在微内核操作系统中,由于许多传统的操作系统功能改为用户任务实现,一般采取客户/服务器模型,即传统操作系统中许多系统功能作为微内核结构下的服务器任务,这样使任务间的数据交换量更大,需要在内核与任务之间进行大量的数据复制,因此影响了系统性能。有研究人员指出著名的微内核操作系统 Mach 性能远不如宏内核的BSD UNIX(当然这里与应用类型有关,例如用嵌入式微内核的系统实现一个文件服务器显然比 UNIX效率低) 。图 1-2 wind 微内核结构为了提高微内核效率,有两种实现模式:受保护的虚地址空间模式和无保护的单一实地址空间模式。前者在宏内核操作
14、系统(如 UNIX)和某些微内核操作系统(如 QNX)中采用。这种模式的优点是显而易见的:任务独立运行,不受其他任务错误影响,系统可靠性高。VxWorks 的 wind 微内核采取单一实地址空间模式,所有任务在同一地址空间运行,不区分核心态和用户态。其优势在于: 任务切换时不需要进行虚拟地址空间切换; 任务间可以直接共享变量,不需要通过内核在不同的地址空间复制数据; 系统调用时不需要在核心态和用户态之间切换,相当于直接的函数调用。 系统调用时需要从用户态切换到核心态,以 执行用 户态下不能执行的操作,在 许多处理器上这是通过一个 trap 中断处理完成的。VxWorks 中不存在这样的切换,因
15、此系 统调用和一般函数调用没有什么差别。但是我 们仍然沿用一般说法。对于两种模式孰优孰劣,各自的支持者们进行了大量的争论。比较各有所长的东西往往非常困难。我们倾向于认为,对于嵌入式实时应用,单一实地址模式要合适一些。许多实践也证明,依靠虚地址保护来提高可靠性总存在局限性,毕竟程序运行出了错误。有时虚地址保护只是使已经出现的错误经过一个延迟、积累和放大的过程,用过 Windows 就会有这种感触。而经过大量关键应用检验的VxWorks 操作系统,则被充分证明是高度可靠的(当然,可靠的系统由可靠的操作系统和可靠的应用系统组成) 。对于从“单片机汇编语言”成长起来的开发人员,可能更喜欢单一实地址空间
16、的系统。1.1.3 任务调度实时系统和分时系统的一个显著差异体现在调度策略上。实时系统调度关心的是对实时事件的相应延迟,而传统的分时系统调度时要考虑的目标是多方面的:公平、效率、利用率、吞吐量等。因此实时系统通常采用优先级调度,即操作系统总是从就绪任务队列中选择最高优先级运行。优先级调度根据调度的时机又分为“不可抢占式调度”和“可抢占式调度” 。1优先级调度如果一个线程一旦获得处理器就独占处理器运行,除非它因某种原因决定放弃处理器,系统才会调度别的线程,这种调度方式称为“不可抢占式调度” (Non-Preemptive Scheduling) ,也即只有任务主动让出处理器后系统才重新根据优先级
17、调度选择任务,如图 1-3 所示。图 1-3 不可抢占优先级调度在图 1-3 中,高优先级任务 t2 和 t3 就绪后(图中斜线阴影部分) ,并没有立即被调度,而是低优先级让出处理器时,才根据优先级高低选择任务运行。高优先级任务和低优先级任务之间是紧密合作的关系,低优先级任务必须设计为不使高优先级任务长时间等待。当注重实时响应时,应该采用“可抢占式调度” (Preemptive Scheduling) 。只要有更高优先级任务就绪,系统立即中断当前任务来调度高优先级任务,确保任意时刻最高优先级任务得到处理器,如图1-4 所示。在图 1-4 中,抢占式优先级调度使任务 t2 和 t3 就绪时总能立
18、即得到处理器,其实时响应特性优于不可抢占调度。对于区分核心态和用户态的系统,优先级调度还是微内核实现的基础。由于可抢占式内核中的核心态任务也可以随时让位给比其优先级高的任务,使得系统可以把一些实时设备的操作放在内核之外,通过“任务”完成,从而简化了内核设计。在传统的操作系统(如 UNIX)中,必须将所有对时间要求较高的操作放在内核中实现,导致庞大的内核。wind 内核支持 256 级优先级:0255。优先级 0 为最高优先级,优先级 255 为最低优先级。任务优先级在创建时确定,并允许程序运行中动态修改。但是对于内核而言,从就绪队列中选择一个任务调度时优先级是确定的,换言之,内核不会动态计算每
19、个任务的优先级,因此这种调度策略属于静态调度策略;相对地,动态调度策略调度时需要根据某个目标(例如任务完成时间底线)动态确定任务优先级并调度。静态调度策略效率显著高于动态调度策略,并且足够满足应用需要,因而被普遍采用,包括 POSIX 1003.1b 标准对任务调度的定义。图 1-4 抢占式优先级调度2Round-Robin 调度前面介绍的优先级调度存在这样的问题:如果没有被更高优先级任务抢占,或者因阻塞等原因让出处理器,任务将一直运行下去,在此情况下,同优先级任务将得不到运行。Round-Robin 调度基于这样的哲学:在更高优先级任务调度依然优先运行的前提下(这一点和前面一样) ,同优先级
20、任务之间调度时追求一定意义上的公平。Round-Robin 调度将任务运行划分为时间片,当任务运行一个时间片后,内核将其调出处理器并放在同优先级就绪任务队列尾部;调度时选择最高优先级就绪队列首部任务。Round-Robin 调度的效果是将每个任务运行一个时间片后“让出”处理器给下一个任务,如轮转一样,也称轮转调度。可见,Round-Robin 调度并没有改变“基于优先级”和“可抢占 ”这两个实时调度的特征。说在 VxWorks 中,Round-Robin 调度是基于优先级可抢占调度的一个“附加特征 ”,如图 1-5 所示。图 1-5 Round-Robin 调度在 POSIX 1003.1b
21、中,基于优先级的可抢占调度定义为 SCHED_FIFO;Round-Robin 调度定义为SCHED_RR。 3.5 节“任务调度”部分深入介绍任务调度细节。3优先级翻转问题实时系统中,如果任务调度采用基于优先级的方式,则传统的资源共享访问机制在系统运行时很容易造成优先级翻转问题,即当一个高优先级任务访问共享资源时,该资源已被一低优先级任务占有,而这个低优先级任务在访问共享资源时可能又被其他一些低优先级的任务抢先,因此造成高优先级任务被许多较低优先级的任务阻塞,高优先级任务在低优先级任务之后运行,看起来像低优先级任务抢占了高优先级任务,即发生了优先级翻转(Priority Inversion)
22、 。一个最为著名的优先级翻转问题的例子是“火星探路者” (Pathfinder) ,它采用了 VxWorks 实时内核设计。1997 年 7 月 4 日, “探路者”弹出气囊在火星表明登陆,放出“漫游者”探测车,收集传回地球的大量数据。在开始的几天里, “探路者”被誉为完美无缺的作品,引起巨大轰动。但是几天后,“探路者”开始出现系统复位、数据丢失的现象。当时解释为“软件小故障” 、 “同时处理的事情太多”等。为了找出故障,研究人员不断模拟“探路者”在火星上工作的条件和过程,终于在实验室中再现了系统复位的现象,并记录下来。根据分析,有如下两个任务需要互斥访问共享“信息总线”:(1)总线管理任务,
23、具有最高优先级,运行频繁,进行总线数据 I/O;(2)气象数据收集任务,优先级低,运行较少,收集数据并通过互斥信号量将数据发布到“总线” 。如果数据收集任务持有信号量期间,总线管理任务就绪并且也申请获取信号量,则总线管理任务阻塞,直到收集任务释放信号量。看起来工作得很好,因为收集任务很快就会完成,高优先级的总线管理任务会很快得到运行。但是,另有一个需要较长时间运行的通信任务,其优先级比总线管理任务低,但是比数据收集任务高。在很少的情况下,如果通信被中断激活,并刚好在总线管理任务等待数据收集任务完成期间就绪,它将被系统调度,从而比它优先级低的数据收集任务得不到运行,并因此使最高优先级的总线管理任
24、务无法运行。在经历较长时间后,看门狗观测到“总线”没有活动,将此解释为严重错误并使整个系统复位。很显然需要防止优先级翻转以确保系统的实时响应。常见的解决办法之一是使用优先级继承协议(Priority Inheritance Protocol) 。当高优先级任务需要低优先级任务占用资源时,将低优先级任务的优先级别提高到和高优先级同样的级别,即相当于低优先级任务继承高优先级任务的优先级级别。VxWorks 实现了对优先级继承的支持,但是默认情况下该功能被关闭,也就是说有可能发生优先级翻转问题,也就导致了“探路者”中问题的发生。不过,研究人员及时查出了问题并给在火星上的系统打上了补丁,使“探路者”终
25、于成功完成使命。美国航空航天局 JPL(喷气推进实验室)决定第二代火星探测器项目仍然与风河系统进行合作,被引为 VxWorks 功能强大和可靠性的例证。防止优先级翻转的另外一种协议是优先级天花板(Prioceiling) ,其设计策略是对优先级翻转采取“预防” ,而不是“补救” 。也就是说:不论是否阻塞了高优先级任务,持有该协议的任务在执行期间都被赋予优先级天花板看作的优先级,以使任务尽快完成操作,因此可以把任务优先级天花板看作是更“积极”的一种保护优先级的方式。VxWorks 对 POSIX 信号量的支持实现了该协议。我们后面将对优先级翻转问题做更具体的描述。1.2 任务属性VxWorks
26、任务具有两个显著不同于主机操作系统的特点:(1)VxWorks 任务和内核具有相同的权限,都能够执行处理器所支持的全部指令;(2)所有任务(包括内核)共享同一实地址空间(不进行虚拟内存管理) ,不同任务的数据没有任何保护机制。这两个特性一方面使 VxWorks 具有很高的效率,同时,由于没有 VxWorks 任务执行指令和没有访问内存任何约束和保护,使得某个任务的错误容易造成更严重的影响,因而对代码质量提出了更高的要求。1.2.1 任务控制块(WIND_TCB)多任务设计能随时打断正在执行着的任务,对内部和外部发生的事件在确定的时间里作出响应。VxWorks 实时 Wind 内核提供了基本的多
27、任务环境。从表面上来看,多个任务正在同时执行,实际上,系统内核根据某一调度策略让它们交替运行。系统调度器使用任务控制块的数据结构(TCB)来管理任务调度功能。任务控制块用来描述一个任务,每一任务都与一个 TCB 关联。TCB 包括了任务的当前状态、优先级、要等待的事件或资源、任务程序码的起始地址、初始堆栈指针等信息。调度器在任务最初被激活时以及从休眠态重新被激活时,要用到这些信息,TCB 使多个任务得以独立运行,如表 1-1所示任务控制块 TCB。表 1-1 任务控制块 TCB项 目 内容和含义任务名称 指针指向表示任务名称的字符串上下文程序计数器,表示任务被中止时的位置CPU 状态,包括各种
28、处理器特定的寄存器栈,用于动态变量,函数调用,信号处理等标准输入,标准输出,标准错误输出定义延迟定时器,用于任务延迟计数时间片定时器,用于 Round-Robin 调度 内核控制结构(Kernel Control Structures)信号处理设置信息,包括阻塞信号集,信号处理方式,信号状态等错误状态,每个任务都有一个独立的全局 errno 的副本调试和性能监视状态任务变量(可选)浮点上下文(可选)异常信息 因处理器体系不同而有差异,用于异常处理退出码 exitCode 作调试之用为了便于调试,每个任务都有一个独一无二的字符串表示的名称,在任务被创建时由用户程序指定或者系统默认生成。几乎所有的
29、任务控制函数都采用任务 ID(等于 TCB 地址)表示一个任务。VxWorks 提供任务名称和任务 ID 之间的转换函数。TCB 的一个重要内容就是任务上下文(ContExT) ,代表了任务运行状态。VxWorks 的任务切换就是将当前任务(被换出 CPU)的上下文保存到该任务的 TCB,然后从调度程序(Scheduler )选择新任务(被换入 CPU)的 TCB 中恢复上下文。上下文切换分两种情况: 同步上下文切换,引起的原因是当前运行的任务执行下列操作:(1)进行阻塞、延迟、挂起的调用;(2)使更高优先级任务就绪而发生优先级抢占;(3)降低自身优先级或者退出; 异步上下文切换,通常由 IS
30、R 使更高优先级任务就绪引起。异步上下文切换比同步上下文切换需要更多时间。调度程序开销主要取决于保存和恢复上下文需要复制的寄存器数,要求该过程非常快。对于特定体系的处理器而言,该数值是固定的。为提高调度程序效率,在默认情况下,VxWorks 上下文不包括浮点寄存器(即假设任务不使用浮点寄存器,在许多情况下的确如此) 。任务创建时指定浮点寄存器是否属于上下文的一部分(标志 VX_FP_TASK) 。VxWorks 中,内存地址空间不是任务上下文的一部分。所有的代码运行在同一地址空间。如每一任务需各自的内存空间,需可选产品 VxVMI 的支持。1.2.2 任务栈每个任务都有独立的栈空间,栈用于任务
31、的函数调用,分配自动变量和函数返回值。任务控制块WIND_TCB 记录了位置和大小等栈信息。WIND_TCB 本身放在任务栈开始 部分。任务栈大小的设置必须合理,太大会浪费内存空间,太小时可能引起栈溢出。在 VxWorks 中,所有任务在同一地址空间运行,任务之间没有任何地址保护机制,因此栈溢出会引起连锁反应,可能导致系统崩溃,或者出现难以调试的意外结果。栈大小设置没有可以套用的公式,一般凭经验设置一个较大的值,以存储空间换取可靠性。分析程序所有可能的分支和调用,从而计算出需要的栈大小的方法,从理论上给出了最佳的栈设置方案,但是目前好像还没有这样的自动化分析工具,靠程序员手工计算似乎不大可行。
32、好在存储器越来越便宜,因此许多应用中,人们更趋向于使用大存储器解决问题。对可靠性要求高的应用,仍然需要充分分析和测试栈大小设置是否足够。栈大小在 taskSpawn( )创建任务时指定。1中断只要体系和 BSP 支持,VxWorks 支持独立的中断栈。 “独立”是对任务而言,对所有的 ISR 使用相同的中断栈。中断栈在系统启动时根据配置参数设置位置、大小和填充。如果体系不支持,中断栈属于被中断任务栈的一部分。此时任务栈必须足够大,保证最坏情况下中断嵌套也不会溢出。对独立中断栈的支持:MC680x0(MC68060 除外) ,ARM,PPC,MIPS。VxWorks for Pentium 支持
33、程序在任务栈和专用中断栈两种方式之间动态选择,如表 1-2 所示。表 1-2 不同处理器的中断栈处理器体系 中断栈 SHELL 根任务 WDBMC680x0 1000 10000 10000 0x1000COLDFIRE 1000 10000 10000 0x1000SPARC 10000 50000 10000 0x2000I960 1000 40000 20000 0x2000MIPS 5000 20000 20000 0x2000PPC 5000 20000 24000 0x2000I80x86 1000 10000 10000 0x1000SH 1000 10000 10000 0x1
34、000AM29xxx 10000 40000 10000 0x2000ARM 决定于 BSP 中断结构0x10000 0x4000 0x20002栈溢出检测当某个任务栈溢出后,系统行为将难以预料。在程序开发过程中,根据经验在可能发生较深嵌套调用的地方加入一些栈检查调用 checkStack( )来检查任务栈使用情况,例如:NAME ENTRY TID SIZE CUR HIGH MARGIN- - - - - - -tShell _shell 23e1c78 9208 832 3632 5576checkStack( )显示了单个指定任务或者所有任务的栈使用情况,包括: 栈大小(SIZE) ;
35、 栈当前使用数(CUR) ; 历史使用峰值(HIGH) ; 最大可能空余数(MARGIN=SIZE-HIGH) 。如果上述结果是程序在足够长的测试运行过程中充分考虑了边界条件以及各种可能引发最深嵌套调用的情况之后得到的输出,那么可以根据该结果调整任务栈的设置,比如设置栈大小为峰值的120%:SIZE=HIGH*1.2 如果 ISR 使用任务栈,则还要加上一个经验常数:SIZE=HIGH*1.2+CC 根据处理器体系特点、ISR 复杂程度和内存大小确定。 VxWorks 要求栈大小为偶数值。VxWorks 没 有 直 接 为 checkStack()记 录 使 用 峰 值 , 而 是 生 成 任
36、 务 时 将 栈 空 间 以 值 0xee 进 行 填 充 ,checkStack()据 此 检 查 栈 使 用 情 况 。 如 果 在 生 成 任 务 时 指 定 VX_NO_STACK_FILL, 则 任 务 栈 不 会 被 填 充 ,因 此 用 checkStack()得 到 的 峰 值 是 没 有 意 义 的 。1.2.3 出错状态ANSI C 标准定义了一个全局整型变量 errno,用来使应用程序知道底层函数出错的详细情况。使用errno 应该注意:errno 总是定义为该任务(对其他主机操作系统而言为进程)最后一次调用出错时的错误值,任何系统调用/函数执行成功都不应该清除 errn
37、o,即不应该执行 errno=0。当某个函数调用返回错误的结果(如返回状态值为 ERROR 或者返回指针为 NULL)时,程序可以立即检查 errno 得知错误细节,该 errno 一直保持,直到随后某次调用中出现新的错误将其覆盖。在 VxWorks 中,每个任务 TCB 中都记录有一个全局 errno 副本,属于上下文的一部分,由调度程序在上下文切换时保存和恢复,因此各个任务的出错状态相互不会影响。与此类似,ISR 也使用独立的errno,但是 ISR 没有 TCB,内核为 ISR 在中断栈中保存和恢复 errno。从编写程序角度来看,无论属于常规任务,还是属于 ISR 的代码,程序都可以使
38、用不受其他任务或者 ISR 影响的 errno。程序引用 errno 时实际上通过一个函数得到全局 errno 地址( errno.h):#include “errno.h“#define errno ( *_errno( )有经验的程序员习惯充分利用 errno,这样,程序具有很好的逻辑结构和用户接口。当发生嵌套调用时,VxWorks 定义的出错状态在错误最初发生的位置定义,例如:STATUS funcA ( void )errno = S_xxx_NOT_IMPLEMENTED;return ERROR;STATUS funcB ( void ). if ( funcA( ) != OK
39、) return ERROR;. return OK;函数 funcA()功能尚未实现,简单的设置 errno 为 S_xxx_NOT_IMPLEMENTED 并返回 ERROR;函数 funcB()调用 funcA()得到 ERROR 时,funcB()返回 ERROR 给调用者但不修改 errno,因为 funcA()已经定义。当然,如果错误源于 funcB(),则 funcB()应该设置 errno。VxWorks 库函数都遵循上述约定。VxWorks 允许用户程序中不仅使用 VxWorks 库函数中对 errno 的定义,还允许用户代码中定义新的 errno,但是应该避免和 VxWor
40、ks 的定义重复。VxWorks 定义的 errno 分两 部分:bit31bit16 错误产生的模块编号bit15bit0 错误编号高 16 位模块编号小于 501 的部分已经被 VxWorks 库使用,用户程序可以使用从 501 开始的模块编号,低 16 位为错误编号可以任意指定。库 errnoLib 提供 VxWorks 库函数中定义的 errno 错误信息字符串。可以通过在 shell 执行printErrno()打印错误信息。使用库 errnoLib 需要定义 INCLUDE_STAT_SYM_TBL。例如可以在 shell 下执行:- i (查看任务状态,ERRNO 列出十六进制表
41、示的错误码)NAME ENTRY TID PRI STATUS PC SP ERRNO DELAY- - - - - - - -.t16 _pRun 7641dc 100 READY 17d1c 763de4 30 0- printErrno(0x30) S_errno_EADDRINUSE该输出结果可以极大地方便程序调试。但是一个缺点是库 errnoLib 以目标码形式发布,必须将其构建到执行映像中才能使用。无法直接通过文本编辑器查看。这需要消耗大约 24KB 代码空间。实际上,不使用库 errnoLib 也可以知道错误信息细节。VxWorks 将模块号定义在头文件targethvwModN
42、um.h 中,可以通过得到的错误信息中的模块号找到对应的错误信息,这样可以节约代码空间。以 errno 为 OX410001 的错误为例,我们得到模块号为十进制数 65(高 16 位) ,然后在vwModNum.h 找到对应模块的定义:#define M_msgQLib (65 targethmsg QLib.h 中了解到:#define S_msgQLib_INVALID_MSG_LENGTH (M_msgQLib | 1)从而得知 errno 为 0x410001 表示 msgQLib 模块由于发送消息超出长度而出错。除了各个模块定义的错误号,VxWorks 还定义了一系列 ANSI C
43、标准错误,如EINTR,EINVAL,EIO,EAGAIN 等,本书后面将会涉及。1.2.4 钩子函数VxWorks 允许任务创建 “钩子函数 ”,安装钩子函数后,在规定事件发生时内核自动调用。具体来说有 3 类钩子函数:(1)任务创建钩子:在任务被创建时调用;(2)任务调度钩子:在任务被调度时调用;(3)任务删除钩子:在任务被删除时调用,任务“删除”包括以任何方式调用函数 taskDelete()、taskDeleteForce()和 exit()引起的系统动作。可以指定多个钩子函数,VxWorks 确保钩子函数具有一致的调用顺序。当多个钩子函数之间存在某种依赖时,如下两点就显得很重要: 任
44、务创建钩子函数和任务调度钩子函数按照其安装的顺序调用; 任务删除钩子函数按照与其安装顺序相反的顺序调用。1任务创建(删除)钩子任务创建钩子或任务删除钩子必须具有如下原型定义:void xxxHook ( WIND_TCB *pTcb );参数 pTcb 指向被创建(删除)任务的 TCB,xxxHook 为任务创建钩子或为任务删除钩子函数名称。钩子函数可以随时被安装/卸载,用于安装和卸载的函数为:#include “taskHookLib.h“STATUS taskCreateHookAdd (FUNCPTR createHook); /* 安装任务创建钩子 */STATUS taskCreat
45、eHookDelete (FUNCPTR createHook); /* 卸载任务创建钩子 */STATUS taskDeleteHookAdd (FUNCPTR deleteHook); /* 安装任务删除钩子 */STATUS taskDeleteHookDelete (FUNCPTR deleteHook); /* 卸载任务删除钩子 */虽然钩子函数在某个特定任务中被创建,但是创建后的钩子却被所有任务共享:在安装钩子后创建任何任务或者删除任何任务时调用钩子函数,除非将安装的钩子函数卸载。注意, (1)任务创建钩子在任务入口函数之前被调用;(2)任务删除钩子在任务退出后被调用。即:任务创建
46、钩子 任务入口函数 任务结束 任务删除钩子如果有多个钩子,其调用顺序前面已经说明。任务删除钩子常常用于进行任务清理工作,如释放内存和关闭打开的文件。由于每次删除任务都会引起任务删除钩子调用,因此必须注意钩子函数重入性。任务创建钩子同样需要考虑代码重入。这些考虑包括使用可扩展的数据结构定义以及避免重复释放同一块内存等。2任务调度钩子任务调度钩子函数原型必须定义如下:void switchHook ( WIND_TCB *pOldTcb, WIND_TCB *pNewTcb );参数 pOldTcb 指向被调出处理器的任务的 TCB,pNewTcb 执行被调入处理器的任务的 TCB。任务调度钩子函
47、数由下列函数动态安装和卸载:#include “taskHookLib.h“STATUS taskSwitchHookAdd ( FUNCPTR switchHook );STATUS taskSwitchHookDelete ( FUNCPTR switchHook );任务调度钩子函数在每次 VxWorks 调度程序选择新的任务运行时被调用。与任务创建(删除)钩子不同的是,任务调度钩子在内核上下文中运行,而任务创建(删除)钩子在入参 pTcb 所指示的任务的上下文中运行。这一差异使得任务调度钩子不能和任务创建钩子及任务删除钩子函数那样进行所有的系统调用。如表 1-3 所示列出了任务调度钩子
48、所能调用的 VxWorks 函数。表 1-3 任务调度钩子可以调用的系统函数库 允许调用的函数bLib 全部函数fppArchLib fppSave( ),fppRestore( )intLib intContext( ),intCount( ),intVecSet( ),intVecGet( )lstLib 全部函数mathALib 如果使用 fppSave( )和 fppRestore( ),则全部函数可以使用rngLib 除 rngCreate( )外的函数vxLib vxTas( )taskLib taskIdVerify( ),taskIdDefault( ),taskIsReady
49、( ),taskIsSuspended( ),taskTcb( )任务调度钩子可以用来检查任务调度时两个任务的状态,有时可以利用这一特点为调试提供帮助。例如,可以在钩子函数中向某端口输出一个波形,然后通过高速的逻辑分析仪捕获信号来计算某个任务执行时间。如果系统调度策略为默认的抢占优先级调度,还可以通过比较被调入调出的任务优先级判断是否由于阻塞引起的调度,或者由于优先级抢占引起的调度。如下面这段代码,当旧任务优先级比新任务优先级高时,说明高优先级任务被阻塞(也可是挂起或延迟) ,这时可以通过 pOldTcb 检查任务的阻塞队列。把任务调度钩子设计为:void switchHook ( WIND_TCB *pOldTcb, WIND_TCB *pNewTcb )if( pNewTcb-priority priority ). /*阻塞,挂起等引起