VxWorks入门系列教程0:介绍

关键词

SMP

多处理器系统的工作方式分为非对称多处理(asym-metrical mulit-processing)和对称多处理(symmetrical mulit-processing,SMP)两种。在对称多处理器系统中,所有处理器的地位都是相同的,所有的资源,特别是存储器、中断及I/O空间,都具有相同的可访问性,消除了结构上的障碍。

嵌入式操作系统VxWorks简介

VxWorks操作系统是美国WindRiver公司于1983年设计开发的一种嵌入式实时操作系统(RTOS),是嵌入式开发环境的关键组成部分。良好的持续能力、高性能的内核以及友好的用户开发环境,在嵌入式实时操作系统领域占据一席之地。它以其良好的可靠性和卓越的实时性被广泛地应用在通信、军事、航空、航天等高精尖技术及实时性要求极高的领域中,如卫星通讯、军事演习、弹道制导、飞机导航等。在美国的 F-16、FA-18 战斗机、B-2 隐形轰炸机和爱国者导弹上,甚至连1997年在火星表面登陆的火星探测器上也使用到了VxWorks。

实时操作系统和分时操作系统的区别

从操作系统能否满足实时性要求来区分,可把操作系统分成分时操作系统和实时操作系统。

分时操作系统按照相等的时间片调度进程*流运行,分时操作系统由调度程序自动计算进程的优先级,而不是由用户控制进程的优先级。这样的系统无法实时响应外部异步事件。

实时操作系统能够在限定的时间内执行完所规定的功能,并能在限定的时间内对外部的异步事件作出响应。 分时系统主要应用于科学计算和一般实时性要求不高的场合。实时性系统主要应用于过程控制、数据采集、通信、多媒体信息处理等对时间敏感的场合。

VxWorks的特点

可靠性

操作系统的用户希望在一个工作稳定,可以信赖的环境中工作,所以操作系统的可靠性是用户首先要考虑的问题。而稳定、可靠一直是VxWorks的一个突出优点。自从对中国的销售解禁以来,VxWorks以其良好的可靠性在中国赢得了越来越多的用户。

实时性

实时性是指能够在限定时间内执行完规定的功能并对外部的异步事件作出响应的能力。实时性的强弱是以完成规定功能和作出响应时间的长短来衡量的。

VxWorks 的实时性做得非常好,其系统本身的开销很小,进程调度、进程间通信、中断处理等系统公用程序精练而有效,它们造成的延迟很短。VxWorks 提供的多任务机制中对任务的控制采用了优先级抢占(Preemptive Priority Scheduling)和轮转调度(Round-Robin Scheduling)机制,也充分保证了可靠的实时性,使同样的硬件配置能满足更强的实时性要求,为应用的开发留下更大的余地。

可裁减性

用户在使用操作系统时,并不是操作系统中的每一个部件都要用到。例如图形显示、文件系统以及一些设备驱动在某些嵌入系统中往往并不使用。

VxWorks 由一个体积很小的内核及一些可以根据需要进行定制的系统模块组成。VxWorks 内核最小为 8kB,即便加上其它必要模块,所占用的空间也很小,且不失其实时、多任务的系统特征。由于它的高度灵活性,用户可以很容易地对这一操作系统进行定制或作适当开发,来满足自己的实际应用需要。

对一个实时内核的要求

一个实时操作系统内核需满足许多特定的实时环境所提出的基本要求,这些包括:

  • 多任务:由于真实世界的事件的异步性,能够运行许多并发进程或任务是很重要的。多任务提供了一个较好的对真实世界的匹配,因为它允许对应于许多外部事件的多线程执行。系统内核分配CPU给这些任务来获得并发性。
  • 抢占调度:真实世界的事件具有继承的优先级,在分配CPU的时候要注意到这些优先级。基于优先级的抢占调度,任务都被指定了优先级,在能够执行的任务(没有被挂起或正在等待资源)中,优先级最高的任务被分配CPU资源。换句话说,当一个高优先级的任务变为可执行态,它会立即抢占当前正在运行的较低优先级的任务。
  • 任务间的通讯与同步:在一个实时系统中,可能有许多任务作为一个应用的一部分执行。系统必须提供这些任务间的快速且功能强大的通信机制。内核也要提供为了有效地共享不可抢占的资源或临界区所需的同步机制。
  • 任务与中断之间的通信:尽管真实世界的事件通常作为中断方式到来,但为了提供有效的排队、优先化和减少中断延时,我们通常希望在任务级处理相应的工作。所以需要杂任务级和中断级之间存在通信。

系统编程方法

实时系统主要包括:多任务调度(采用优先级抢占方式),任务间的同步和进程间通信机制.

一个多任务环境允许实时应用程序以一套独立任务的方式构筑,每个任务拥有独立的执行线程和它自己的一套系统资源。进程间通信机制使得这些任务的行为同步、协调。 wind使用中断驱动和优先级的方式。它缩短了上下文转换的时间开销和中断的时延。在 VxWorks 中,任何例程都可以被启动为一个单独的任务,拥有它自己的上下文和堆栈。还有一些其它的任务机制可以使任务挂起、继续、删除、延时或改变优先级。

另一个重要内容是:硬件中断处理。硬件产生中断,统治系统调用相应的中断历程(ISR),位是系统得到尽快的响应,ISR在它自己独立的上下文和堆栈中运行.它的优先级高于任何任务优先级.

中断延迟(Interrupt Latency) 中断延迟是指从硬件中断发生到开始执行中断处理程序第一条指令之间的这段时间。

优先级驱动(Priority-Driven) 优先级驱动是指多任务系统中,当前运行任务总是具有最高优先级的就绪任务。

多任务调度

两种方式: 优先抢占和轮转调度(Preemptive Priority,Round-Robin Scheduling).

优先抢占(Preemptive Priority): 每一个任务都有一个优先级,系统核心保证优先级最高的任务运行于CPU.如果有任务优先级高于当前的任务优先级,系统立刻保存当前任务的上下文,切换到优先级高的上下文.

抢占(Preemptive): 抢占是指当系统处于核心态运行时, 允许任务的重新调度。换句话说就是指正在执行的任务可以被打断,让另一个任务运行。抢占提高了应用对异步事件的响应性能力。操作系统内核可抢占,并不是说任务调度在任何时候都可以发生。例如当一个任务正在通过一个系统调用访问共享数据时,重新调度和中断都被禁止.

任务上下文(Task Context): 任务上下文是指任务运行的环境。例如,针对x86的CPU,任务上下文可包括程序计数器、堆栈指针、通用寄存器的内容.

上下文切换(Context Switching): 多任务系统中,上下文切换是指CPU的控制权由运行任务转移到另外一个就绪任务时所发生的事件,当前运行任务转为就绪(或者挂起、删除)状态,另一个被选定的就绪任务成为当前任务。上下文切换包括保存当前任务的运行环境,恢复将要运行任务的运行环境。上下文的内容依赖于具体的CPU.

轮转调度(Round-Robin Scheduling):使所有相同优先级,状态为ready的任务公平分享CPU(分配一定的时间间隔,使个任务*流享有CPU).

系统由256个优先级,从0到255,0为最高,255为最低. 任务在被创建时设定了优先级.也可用taskPrioritySet ( ) 来改变任务优先级.

任务的主要状态: READY,PEND,DELAY,SUSPEND…

1
2
3
4
5
6
7
8
9
10
ready-------->pended -----------semTake()/msgQReceive()-其他任务 
ready-------->delayed-----------taskDelay()
ready-------->suspended---------taskSuspend()
pended------->ready-------------semaGive()/msgQSend()-其他任务
pended------->suspended---------taskSuspend()
delayed------>ready-------------expired delay
delayed------>suspended---------taskSuspend()
suspended---->ready-------------taskResume()/taskActivate()
suspended---->pended------------taskResume()
suspended---->delayed-----------taskResume()

轮转调度 (Round-Robin): 轮转调度可以扩充到优先抢占方式中,当多个任务优先级相同的情况下,轮转调度算法使任务按平等的时间片运行于CPU,共享CPU.避免一个任务长时间占用CPU,而导致其他任务不能运行.可以用 kernelTimeSlice( ) 来定义时间长度.

taskLock()和 taskUnlock()用来取消优先抢占方式 和恢复优先抢占方式.

注意: 一个任务可以调用taskDelete()删除另一个任务,但是如果一个当前正在运行的任务被删除后,该任务的内存没有释放,而其他任务不知道,依然在等待,结果导致系统stop.用 taskSafe()和 taskUnsafe() 来保证正在运行的任务不被删除.

用法如下:

1
2
3
4
5
6
7
taskSafe (); 
semTake (semId, WAIT_FOREVER);
/* Block until semaphore **ailable */
. . . . critical region .
semGive (semId); semGive (semId);  
/* Release semaphore */
taskUnsafe ();

任务间的同步和进程间协调

信号量作为任务间同步和互斥的机制。在 wind 核中有几种类型的信号量,它们分别针对不同的应用需求:二进制信号量、计数信号量、互斥信号量和 POSIX 信号量。所有的这些信号量是快速和高效的,它们除了被应用在开发设计过程中外,还被广泛地应用在VxWorks 高层应用系统中。对于进程间通信,wind 核也提供了诸如消息队列、管道、套接字和信号等机制。

任务间的同步和进程间协调的几种方式:

  • 内存共享(Shared Memory),对简单的数据共享而言.
  • 信号量(Semaphore),基本的互斥和同步.
  • 消息队列(Message queues)和管道(Pipe),单个CPU中,任务间的信息传递.
  • 套结字(Socket)和远程调用(Remote procedure calls),相对于网络任务间的通信.
  • 信号(Signals),出错处理(Exception handling).
  • 互斥(Mutual Exclusion)

互斥是用来控制多任务对共享数据进行串行访问的同步机制。在多任务应用中,当两个或多个任务同时访问共享数据时,可能会造成数据破坏。互斥使它们串行地访问数据,从而达到保护数据的目的.

解决互斥的几种方法:

  1. 关闭中断的方法(intLock): 能解决任务和中断ISR之间产生的互斥.
1
2
3
4
funcA () 
{ int lock = intLock();
. . critical region that cannot be interrupted .
intUnlock (lock); }

但在实时系统中采取这个办法会影响系统对外部中断及时响应和处理的能力.

  1. 关闭系统优先级(taskLock): 关闭系统优先级,这样在当前任务执行时,除了中断外,不会有其他优先级高的任务来抢占CPU,影响当前程序运行.
1
2
3
4
funcA () 
{ taskLock ();
. . critical region that cannot be interrupted .
taskUnlock (); }

这种方法阻止了高优先级的任务抢先运行,在实时系统中也是不适合的,除非关闭优先级的时间特别短.

  1. 信号量(Semaphore): 信号量是解决互斥和同步协调进程最好的方法

当一个Semaphore创建时,指定了任务队列的种类

semBCreat( SEM_Q_PRIORITY, SEM_FULL), SEM_Q_PRIORITY 指明处于等待状态的任务在等待队列中以优先级的顺序排列
semBCreat(SEM_Q_FIFO,SEM_FULL), SEM_Q_FIFO指明 处于等待状态的任务在等待队列中以先进先出的顺序排列

当一个Semaphore创建时,指定了这个semaphore是用在解决互斥还是用来同步任务
semBCreat( SEM_Q_FIFO, SEM_FULL) , SEM_FULL 指明用于任务间互斥.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
SEM_ID semMutex; 
semMutex = semBCreate (SEM_Q_PRIORITY, SEM_FULL);
.........
semTake (semMutex, WAIT_FOREVER);
. . critical region, only accessible by a single task at a time .
semGive (semMutex);
semBCreat(SEM_Q_FIFO,SEM_EMPTY), SEM_EMPTY 指明用于任务间同步.

/* includes */
#include "vxWorks.h"
#include "semLib.h"
SEM_ID syncSem;
/* ID of sync semaphore */
init ( int someIntNum )
{ /* connect interrupt service routine */
intConnect (INUM_TO_IVEC (someIntNum), eventInterruptSvcRout, 0);
/* create semaphore */
syncSem = semBCreate (SEM_Q_FIFO, SEM_EMPTY);
/* spawn task used for synchronization. */
taskSpawn ("sample", 100, 0, 20000, task1, 0,0,0,0,0,0,0,0,0,0);
}
task1 (void)
{ ...
semTake (syncSem, WAIT_FOREVER);
/* wait for event to occur */
printf ("task 1 got the semaphore\n");
...
/* process event */
}
eventInterruptSvcRout (void)
{ ...
semGive (syncSem);
/* let task 1 process event */
...
}

函数介绍:

semTake(semID,time out)--------有Semaphore空闲,就Take, 如果没有,由time out 定,超时则向下执行

  1. 优先级反转(Priority Inversion)

优先级反转是指一个任务等待比它优先级低的任务释放资源而被阻塞,如果这时有中等优先级的就绪任务,阻塞会进一步恶化。优先级继承技术可用来解决优先级反转问题。

Priority inversion arises when a higher-priority task is forced to wait an indefinite period of time for a lower-priority task to complete.

  1. 优先级继承(Priority Inheritance)

优先级继承可用来解决优先级反转问题。当优先级反转发生时,优先级较低的任务被暂时地提高它的优先级,使得该任务能尽快执行,释放出优先级较高的任务所需要的资源。 The mutual-exclusion semaphore has the option SEM_INVERSION_SAFE, which enables a priority-inheritance algorithm. The priority-inheritance protocol assures that a task that owns a resource executes at the priority of the highest-priority task blocked on that resource. Once the task priority has been elevated, it remains at the higher level until all mutual-exclusion semaphores that the task owns are released; then the task returns to its normal, or standard, priority. Hence, the “inheriting” task is protected from preemption by any intermediate-priority tasks. This option must be used in conjunction with a priority queue (SEM_Q_PRIORITY).

VXWORKS内核分析

实时操作系统的结构

在计算的早期开发的操作系统的最原始的结构形式是一个统一的实体(monolithic)。在这样的系统中,提供的不同功能的模块,如处理器管理、内存管理、输入输出等,通常是独立的。然而他们在执行过程中并不考虑其他正在使用中的模块,各个模块都以相同的时间粒度运行。

由于现代实时环境需要许多不同的功能,以及在这样的环境中存在的并发活动所引起的异步性和非确定性,操作系统变得更加复杂。所以早期操作系统的统一结构的组织已经被更加精确的内部结构所淘汰。层次结构的起点----内核

操作系统的最好的内部结构模型是一个层次性的结构,最低层是内核。这些层次可以看成为一个倒置的金字塔,每一层都建立在较低层的功能之上。 内核仅包含一个操作系统执行的最重要的低层功能。正象一个统一结构的操作系统,内核提供了在高层软件与下层硬件之间的抽象层。然而,内核仅提供了构造操作系统其他部分所需的最小操作集。

对一个实时内核的要求

一个实时操作系统内核需满足许多特定的实时环境所提出的基本要求,这些包括: 多任务:由于真实世界的事件的异步性,能够运行许多并发进程或任务是很重要的。多任务提供了一个较好的对真实世界的匹配,因为它允许对应于许多外部事件的多线程执行。系统内核分配CPU给这些任务来获得并发性。

  • 抢占调度:真实世界的事件具有继承的优先级,在分配CPU的时候要注意到这些优先级。基于优先级的抢占调度,任务都被指定了优先级, 在能够执行的任务(没有被挂起或正在等待资源)中,优先级最高的任务被分配CPU资源。换句话说,当一个高优先级的任务变为可执行态,它会立即抢占当前正在运行的较低优先级的任务。

  • 快速灵活的任务间的通信与同步:在一个实时系统中,可能有许多任务作为一个应用的一部分执行。系统必须提供这些任务间的快速且功能强大的通信机制。内核也要提供为了有效地共享不可抢占的资源或临界区所需的同步机制。

  • 方便的任务与中断之间的通信:尽管真实世界的事件通常作为中断方式到来,但为了提供有效的排队、优先化和减少中断延时,我们通常希望在任务级处理相应的工作。所以需要杂任务级和中断级之间存在通信。

  • 性能边界:一个实时内核必须提供最坏情况的性能优化,而非针对吞吐量的性能优化。我们更期望一个系统能够始终以50微妙执行一个函数,而不期望系统平均以10微妙执行该函数,但偶尔会以75微妙执行它。

  • 特殊考虑:由于对实时内核的要求的增加,必须考虑对内核支持不断增加的复杂功能的要求。这包括多进程处理,Ada和对更新的、功能更强的处理器结构如RISC的支持。

拥有其它名字的内核

许多商用化的内核支持的功能远强于上面所列的要求。在这方面,他们不是真正的内核,而更象一个小的统一结构的操作系统。因为他们包含简单的内存分配、时钟管理、甚至一些输入输出系统调用的功能。

这种分类不仅仅是在语义上的争论,在这篇文章的后面章节将说明限制内核功能和油画这些功能的重要性。

VxWorks内核:Wind

VxWorks操作系统是一种功能最全的现在可以获得的独立于处理器的实时系统。然而,VxWorks是带有一个相当小的真正微内核的层次结构。内核仅提供多任务环境、进程间通信和同步功能。这些功能模块足够支持VxWorks在较高层次所提供的丰富的性能的要求。 通常内核操作对于用户是不可见的。应用程序为了实现需要内核参与的任务管理和同步使用一些系统调用,但这些调用的处理对于调用任务是不可见的。应用程序仅链接恰当的VxWorks例程(通常使用VxWorks的动态链接功能),就象调用子程序一样发出系统调用。这种接口不象有些系统需要一个笨拙的跳转表接口,用户需要通过一个整数来指定一个内核功能调用。

多任务

内核的基本功能是提供一个多任务环境。多任务使得许多程序在表面上表现为并发执行,而事实上内核是根据基本的调度算法使他们分段执行。每个明显独立的程序被成为一个任务。每个任务拥有自己的上下文,其中包含在内核调度使该任务执行的时候它所看到的CPU环境和系统资源。

任务状态

内核维护系统中的每个任务的当前状态。状态迁移发生在应用程序调用内核功能服务的时候。下面定义了wind内核状态:

  • 就绪态----一个任务当前除了CPU不等待任何资源
  • 阻塞态----一个任务由于某些资源不可获得而被阻塞
  • 延迟态----一个任务睡眠一段时间
  • 挂起态----主要用于调试的一个辅助状态,挂起禁止任务的执行

任务被创建以后进入挂起态,需要通过特定的操作使被创建的任务进入就绪态,这一操作执行速度很快,使应用程序能够提前创建任务,并以一种快捷的方式激活该任务。

调度控制

多任务需要一个调度算法分配CPU给就绪的任务。在VxWorks中默认的调度算法是基于优先级的抢占调度,但应用程序也可以选择使用时间片轮转调度。

  • 基于优先级抢占调度:基于优先级的抢占调度,每个任务被指定一个优先级,内核分配CPU给处于就绪态的优先级最高的任务。调度采用抢占的方式,是因为当一个优先级高于当前任务的任务变为就绪态时,内核将立即保存当前任务的上文,并切换到高优先级任务的上文。VxWorks有从0到255共256个优先级。在创建的时候任务被指定一个优先级,在任务运行的过程中可以动态地修改优先级以便跟踪真实世界的事件优先级。外部
    中断被指定优先于任何任务的优先级,这样能够在任何时候抢占一个任务。
  • 时间片轮转:基于优先级抢占调度可以扩充时间片轮转调度。时间片轮转调度允许在相同优先级的处于就绪态的任务公平地共享CPU。没有时间片轮转调度,当有多个任务在同一优先级共享处理器时,一个任务可能独占CPU,不会被阻塞直到被一个更高优先级的任务抢占,而不给同一优先级的其他任务运行的机会。如果时间片轮转被使能,执行任务的时间计数器在每个时钟滴答递增。当指定的时间片耗尽,计数器会被清零,该任务被放在同一优先级任务队列的队尾。加入特定优先级组的新任务被放在该组任务的队尾,并将运行计数器初始化为零。

基本的任务函数

用于状态控制的基本任务函数包括一个任务的创建、删除、挂起和唤醒。一个任务也可以使自己睡眠一个特定的时间间隔不去运行。许多其他任务例程提供由任务上下文获得的状态信息。这些例程包括访问一个任务当前处理器寄存器控制。

任务删除问题

wind内核提供防止任务被意外删除的机制。通常,一个执行在临界区或访问临界资源的任务要被特别保护。我们设想下面的情况:一个任务获得一些数据结构的互斥访问权,当它正在临界区内执行时被另一个任务删除。由于任务无法完成对临界区的操作,该数据结构可能还处于被破坏或不一致的状态。而且,假想任务没有机会释放该资源,那麽现在其他任何任务现在就不能获得该资源,资源被冻结了。

任何要删除或终止一个设定了删除保护的任务的任务将被阻塞。当被保护的任务完成临界区操作以后,它将取消删除保护以使自己可以被删除,从而解阻塞删除任务。

正如上面所展示的,任务删除保护通常伴有互斥操作。

这样,为了方便性和效率,互斥信号量包含了删除保护选项。(参见"互斥信号量")

任务间通信

为了提供完整的多任务系统的功能,wind内核提供了一套丰富的任务间通信与同步的机制。这些通信功能使一个应用中各个独立的任务协调他们的活动。

共享地址空间

wind内核的任务间通信机制的基础是所有任务所在的共享地址空间。通过共享地址空间,任务能够使用共享数据结构的指针自由地通信。管道不需要映射一块内存区到两个互相通信任务的寻址空间。

不幸的是,共享地址空间具有上述优点的同时,带来了未被保护内存的重入访问的危险。UNIX操作系统通过隔离进程提供这样的保护,但同时带来了对于实时操作系统来说巨大的性能损失。

互斥操作

当一个共享地址空间简化了数据交换,通过互斥访问避免资源竞争就变为必要的了。用来获得一个资源的互斥访问的许多机制仅在这些互斥所作用的范围上存在差别。实现互斥的方法包括禁止中断、禁止任务抢占和通过信号量进行资源锁定。

  • 中断禁止:最强的互斥方法是屏蔽中断。这样的锁定保证了对CPU的互斥访问。这种方法当然能够解决互斥的问题,但它对于实时是不恰当的,因为它在锁定期间阻止系统响应外部事件。长的中断延时对于要求有确定的响应时间的应用来说是不可接受的。
  • 抢占禁止:禁止抢占提供了强制性较弱的互斥方式。 当前任务运行的过程中不允许其他任务抢占,而中断服务程序可以执行。这也可能引起较差的实时响应,就象被禁止中断一样,被阻塞的任务会有相当长时间的抢占延时,就绪态的高优先级的任务可能会在能够执行前被强制等待一段不可接受的时间。为避免这种情况,在可能的情况下尽量使用信号量实现互斥。
  • 互斥信号量:信号量是用于锁定共享资源访问的基本方式。不象禁止中断或抢占,信号量限制了互斥操作仅作用于相关的资源。一个信号量被创建来保护资源。VxWorks的信号量遵循Dijkstra的P()和V()操作模式。
        
    当一个任务请求信号量,P(), 根据在发出调用时信号量的置位或清零的状态, 会发生两种情况。如果信号量处于置位态, 信号量会被清零,并且任务立即继续执行。如果信号量处于清零态,任务会被阻塞来等待信号量。
        
    当一个任务释放信号量,V(),会发生几种情况。如果信号量已经处于置位态,释放信号量不会产生任何影响。如果信号量处于清零态且没有任务等待该信号量,信号量只是被简单地置位。如果信号量处于清零态且有一个或多个任务等待该信号量,最高优先级的任务被解阻塞,信号量仍为清零态。
        
    通过将一些资源与信号量关联,能够实现互斥操作。当一个任务要操作资源,它必须首先获得信号量。只要任务拥有信号量,所有其他的任务由于请求该信号量而被阻塞。当一个任务使用完该资源,它释放信号量,允许等待该信号量的另一个任务访问该资源。
        
    Wind内核提供了二值信号量来解决互斥操作所引起的问题。 这些问题包括资源拥有者的删除保护,由资源竞争引起的优先级逆转。
        
    删除保护----互斥引起的一个问题会涉及到任务删除。在由信号量保护的临界区中,需要防止执行任务被意外地删除。删除一个在临界区执行的任务是灾难性的。资源会被破坏,保护资源的信号量会变为不可获得,从而该资源不可被访问。通常删除保护是与互斥操作共同提供的。由于这个原因,互斥信号量通常提供选项来隐含地提供前面提到的任务删除保护的机制。
        
    优先级逆转/优先级继承----优先级逆转发生在一个高优先级的任务被强制等待一段不确定的时间以便一个较低优先级的任务完成执行。考虑下面的假设:
        
    T1,T2和T3分别是高、中、低优先级的任务。T3通过拥有信号量而获得相关的资源。当T1抢占T3,为竞争使用该资源而请求相同的信号量的时候,它被阻塞。如果我们假设T1仅被阻塞到T3使用完该资源为止,情况并不是很糟。毕竟资源是不可被抢占的。然而,低优先级的任务并不能避免被中优先级的任务抢占,一个抢占的任务如T2将阻止T3完成对资源的操作。这种情况可能会持续阻塞T1等待一段不可确定的时间。这种情况成为优先级逆转,因为尽管系统是基于优先级的调度,但却使一个高优先级的任务等待一个低优先级的任务完成执行。
        
    互斥信号量有一个选项允许实现优先级继承的算法。优先级继承通过在T1被阻塞期间提升T3的优先级到T1解决了优先级逆转引起的问题。这防止了T3,间接地防止T1,被T2抢占。通俗地说,优先级继承协议使一个拥有资源的任务以等待该资源的任务中优先级最高的任务的优先级执行。当执行完成,任务释放该资源并返回到它正常的或标准的优先级。因此,继承优先级的任务避免了被任何中间优先级的任务抢占。

同步

信号量另一种通常的用法是用于任务间的同步机制。在这种情况下,信号量代表一个任务所等待的条件或事件。最初,信号量是在清零态。一个任务或中断通过置位该信号量来指示一个事件的发生。等待该信号量的任务将被阻塞直到事件发生、该信号量被置位。一旦被解阻塞,任务就执行恰当的事件处理程序。信号量在任务同步中的应用对于将中断服务程序从冗长的事件处理中解放出来以缩短中断响应时间是很有用的。

消息队列

消息队列提供了在任务与中断服务程序或其他任务间交换变长消息的一种较低层的机制。这种机制在功能上类似于管道,但有较少的开销。
管道、套接字、远程过程调用和更多

许多高层的VxWorks机制提供任务间通信的更高层的抽象,包括管道、TCP/IP套接字、远程过程调用和更多。为了保持裁减内核为仅包含足够支持高层功能的一个最小函数集的设计目标,这些特性都是基于上面描述的内核同步方式的。

内核设计的优点

wind内核的一个重要的设计特性是最小的抢占延时。其他的主要设计的优点包括史无前例的可配置性,对不可预见的应用需求的可扩展性,在各种微处理器应用开发中的移植性。

最小的抢占延时

正如前面所讨论的,禁止抢占是获得代码临界资源互斥操作的通常手段。这种技巧的不期望的负面影响是高的抢占延时,这可以通过尽量使用信号量实现互斥和保持临界区尽量紧凑被减小。但即使广泛地使用信号量也不能解决所有的可能导致抢占延时的根源。内核本身就是一个导致抢占延时的根源。为了理解其原因,我们必须更好地理解内核所需的互斥操作。

内核级和任务级

在任何多任务系统中,大量的应用是发生在一个或多个任务的上下文。然而,有些CPU时间片不在任何任务的上下文。这些时间片发生在内核改变内部队列或决定任务调度。在这些时间片中,CPU在内核级执行,而非任务级。

为了内核安全地操作它的内部的数据结构,必须有互斥操作。内核级没有相关的任务上下文,内核不能使用信号量保护内部链表。内核使用工作延期作为实现互斥的方式。当有内核参与时,中断服务程序调用的函数不是被直接激活,而是被放在内核的工作
队列中。内核完成这些请求的执行而清空内核工作队列。

当内核正在执行已经被请求服务时系统将不响应到达内核的函数调用。可以简单地认为内核状态类似于禁止抢占。如前面所讨论的,抢占延时在实时系统中是不期望有的,因为它增加了对于会引起应用任务重新调度的事件的响应时间.

管操作系统在内核级(此时禁止抢占)完全避免消耗时间是不可能的,但减少这些时间是很重要的。这是减少由内核执行的函数的数量的主要原因, 也是不采用统一结构的系统设计方式的原因。例如,有一种流行的实时操作系统的每个函数都是在内核级执行。这意味着当一个低优先级的任务在执行分配内存、获得任务信息的函数时所有高优先级的任务被禁止抢占。

一个最小的内核

已经说明了一个最小内核的优点和构造高层操作系统功能的必要功能,我们使用这些操作原语来执行一个传统的内核级功能,而在VxWorks中作为任务级功能执行,内存管理。 在这个例子中,考虑用户可调用的子例程malloc, 用于分配所请求大小的内存区并返回一个指向该内存区的指针。假定空闲内存区是通过搜索一个空闲内存块的队列找到的,一个信号量必须被用来保护这个非抢占多用户资源。分配内存的操作如下:

  • 获得互斥信号量
  • 搜索空闲内存块链表
  • 释放互斥信号量

值得注意的是搜索一个足够大的空闲内存块的可能的冗长的时间是发生在调用任务的上下文中。这是可以被高优先级的任务抢占的(除了信号量调用的这段执行时间)。

在一个标准的统一结构的实时内核中,内存分配例程操作如下:

  • 进入内核
  • 搜索空闲内存块链表
  • 退出内核

整个内存分配发生在内核级,任务抢占被禁止如果高优先级的任务在此时变为就绪态,它必须等待直到内核为低优先级的任务完成内存分配。有些操作系统甚至在这段市时间禁止中断。

任务级操作系统服务

Wind River System的实时操作系统,VxWorks,显示了这样设计的一个最小内核是能够满足需求的。VxWorks是现在能够获得的独立于任何处理器的、拥有相当小内核的、功能完全的层次结构的实时操作系统。

VxWorks在内核之上提供了大量的功能。它包括内存管理,一个完整的BSD4.3网络包,TCP/IP,网络文件系统(NFS),远程过程调用(RPC),UNIX兼容的链接加载模块,C语言的解释界面,各种类型的定时器,性能监测组件,调试工具,额外的通信工具如管道、信号和套接字,I/O和文件系统,和许多功能例程。这些都不是运行在内核级,所以不会禁止中断或任务抢占。

可配置性

实时应用有多种内核需求。没有哪个内核有一个用来满足每种需求的很好的设计折衷。然而,一个内核可以通过配置来调整特定的性能特性,裁减实时系统来最好地适应一个应用的要求。不可预见的内核配置性以用户可选择的内核排队算法的形式提供给应用。

排队策略

VxWorks中的排队库是独立于使用他们的内核队列功能而执行的,这样提供了将来增加新的排队方式的灵活性。

在VxWorks中有各种内核队列。就绪队列是一个按优先级索引的所有等待调度的任务队列。滴答队列用于定时功能。信号量是一个等待信号量的被阻塞任务的链表。活动队列是一个系统中所有任务的一个先进先出(FIFO)的链表。这些队列中的每个队列都需要一个不同的排队算法。这些算法不是被内嵌在内核中,而是被抽取到一个自治的、可转换的排队库中。这种灵活的组织形式是满足特殊的配置需求的基础。

可扩展性

支持不可预见的内核扩展的能力与以有功能的可配置性是同样重要的。简单的内核接口和互斥方法使内核级功能扩展相当容易; 在某些情况下,应用可以仅利用内核钩子函数来实现特定的扩展。

内部钩子函数

为了不修改内核而能够向系统增加额外的任务相关的功能,VxWorks提供了任务创建、切换和删除的钩子函数。这些允许在任务被创建、 上下文切换和任务被删除的时候额外的例程被调用执行。这些钩子函数可以利用任务上下文中的空闲区创建wind内核的任务特性。

未来考虑

有许多系统函数现在变得越来越重要,而且会影响到内核设计时的抢占延时。尽管涉及这些问题一个完整的讨论超出了本文的范围,但值得简单地提一下。
RISC/CISC

设计一个独立于CPU的操作系统一直是一个挑战。随着新的RSIC(精简指令集)处理器变得很流行,这些难度也加大了。为了在RISC环境下有效地执行,内核和操作系统需要有执行不同策略的灵活性。

例如,考虑在任务切换时内核执行的例程。在CISC(复杂指令集,如680x0或80x86)CPU,内核为每个任务存储一套完整的寄存器,在运行任务的时候将这些寄存器换入换出。在一个RISC机器上,这样是不合理的,因为涉及到太多的寄存器。所以内核需要一个更精密复杂的策略,如为任务缓存寄存器,允许应用指定一些寄存器给特殊的任务。

移植性

为了使wind内核在他们出现的结构上能够运行,需要有一个可移植的内核版本。这使移植是可行的,但不是最优化的。

多处理

支持紧耦合的多处理需求要求实时内核的内部功能包含,在理想情况下,在远端请求内核调用,如从一个处理器到另一个处理器。这就要涉及到信号量调用(为处理器间同步)和任务调用(为了控制另一个CPU上的任务)。这种复杂性无疑会增加内核级功能调用的开销,但是许多服务如对象标识可以在任务级执行。在多处理系统中保持一个最小内核的优点是处理器之间的互锁可以有较好的时间粒度。大的内核将在内核级消耗额外的时间,仅能获得粗糙的互锁时间粒度。

Ada

Ada语言为实时系统设计者提供了象聚会机制这样的任务原语。异常处理、任务终止、终止替换和聚会都将潜在地影响内核设计。这些操作可以由前面讨论的任务和同步机制构造,为了保持减小抢占延时的设计目标,许多工作能够在任务级执行。

实时内核的重要尺度

许多性能特性被用来比较以有的实时内核,这些包括:

  • 快速的任务上下文切换----由于实时系统的多任务的特性,系统能够快速地从一个任务切换到另一个任务是很重要的。在分时系统中,如UNIX,上下文切换是在ms级。Wind内核执行原始上下文切换只用17us。
  • 最小的同步开销----因为同步是实现资源互斥访问的基本方法,这些操作所引起的开销最小化是很重要的。在VxWorks中,请求和释放二值信号量仅用8us。
  • 最小的中断延时----因为外部世界来的事件通常以中断的形式到来,操作系统快速的处理这些中断是很重要的。内核在操作一些临界数据结构的时候必须禁止中断。为了减小中断延时,必须使这些时间最小化。Wind内核的中断延时小于10us。

抢占延时对性能指标的影响

当许多的实时解决方案被提交给应用工程师时, 性能指标对于评估供应商的产品变得越来越重要。不象上下文切换和中断延时,抢占延时很难测量。所以它很少在说明中被提及。但是考虑到当内核通常禁止上下文切换会长达数百微妙,而声称一个50us的固定长度(与任务个数无关)的上下文切换时间是毫无意义的。除了很难测量外,抢占延时可能会削弱许多性能指标的有效性。

Wind内核通过减小内核的大小来尽量减小抢占延时。 包含繁多功能的内核必将引起长的抢占延时。

结论

为了满足对实时内核日益增加的要求,如新的调度算法、多处理、Ada和RISC结构,wind内核始终以灵活性和可配置性为设计目标。它所提供的优秀的运行性能将成为实时应用需求新标准。

VxWorks及其选件介绍

VxWorks是WindRiver公司开发的具有工业领导地位的高性能实时操作系统内核,具有先进的网络功能。 VxWorks的开放式结构和对工业标准的支持,使得开发人员易于设计高效的嵌入式系统,并可以很小的工作量移植到其它不同的处理器上。

可裁剪微内核结构。

高效的任务管理:

  • 多任务,具有256个优先级。
  • 具有优先级排队和循环调度。
  • 快速的、确定性的上下文切换。

灵活的任务间通讯:

  • 三种信号灯:二进制、计数、有优先级继承特性的互斥信号灯。
  • 消息队列。
  • 套接字(Socket)。
  • 共享内存。
  • 信号(Signals)
  • 微秒级的中断处理。
  • 支持POSIX 1003.1b实时扩展标准。
  • 支持多种物理介质及标准的、完整的TCP/IP网络协议。
  • 灵活的引导方式。支持从ROM、flash、本地盘(软盘或硬盘)或网络引导。
  • 支持多处理器并行处理。
  • 快速灵活的I/O系统。
  • 支持MS-DOS和RT-11文件系统。
  • 支持本地盘,flash,CD-ROM的使用。
  • 完全符合ANSI C标准。
  • 多个系统调用。

VxWORKS板支持包(BSP)

VxWorks BSP包含了开发人员要在特定的目标机上运行VxWorks 所需要的一切支持:支持特定目标机的软件(如驱动程序等)和从主机通过网络引导VxWorks的Boot ROM。Wind River提供支持不同厂商的200多种商业体系结构和目标板的BSP。另外Wind River还提供一个BSP移植包,帮助用户移植VxWorks到客户化硬件板上(超过50%的VxWorks用户使用客户化目标机)。

操作系统的选件

Wind River提供广泛的、越来越多的选件产品以满足特定应用的需求。

BSP开发包(BSP Developers Kit)

BSP开发包帮助开发人员把VxWorks移植到客户化硬件平台上。BSP开发包的选项包括:测试工具,硬件设备的驱动程序库,BSP模板。用户可以根据需要选择不同的选项。Wind River还提供BSP测试验证等咨询服务。

VxVMI:

VxVMI是VxWorks的虚拟内存接口。在调试阶段和软件运行时都能提供强大的内存管理功能。它包括代码段和数据段保护功能,并包含对不同CPU结构的标准编程接口。

VxMP:

VxMP是VxWorks多处理器支持扩展包,它允许将任务分布在多个CPU上执行以提高效率。它透明的、高性能的设计使得在不同CPU上运行的任务可以通过现有的通信机制,如信号灯、消息队列等进行同步和数据交换。

Tornado移植包:

易于使用的Tornado移植包允许把基于VMEexec,pSOS,及其他嵌入式操作系统的应用程序移植到VxWorks上。

基于VxWorks的BSP概念与开发

BSP(Board Support Packet——板级支持包)是介于底层硬件和上层软件之间的底层软件开发包,它主要功能为屏蔽硬件,提供操作系统的驱动及硬件驱动,具体功能包括:

  • 单板硬件初始化,主要是CPU的初始化,为整个软件系统提供底层硬件支持;
  • 为操作系统提供设备驱动程序和系统中断服务程序;
  • 定制操作系统的功能,为软件系统提供一个实时多任务的运行环境;
  • 初始化操作系统,为操作系统的正常运行做好准备;

BSP文件在vxworks/target/config/all和vxworks/target/config/bspname文件夹里。其中,all文件夹里的文件是所有BSP的通用文件,bspname文件夹的文件是用户自己定制的BSP文件。

经过编译、链接,并在makefile和depend. bspname等文件的控制下,原程序最后将生成镜像。VxWorks的镜像可分为两类:可下载镜像和可引导镜像。

  • 可下载镜像(Loadable Image):实际包括两部分,一是vxWorks,二是boot ROM,两部分是独立创建的。其中boot ROM包括被压缩的boot ROM镜像(bootrom)、非压缩的boot ROM镜像(bootrom_uncmp)和驻留ROM的boot ROM镜像(bootrom_res)三种类型;
  • 可引导镜像(Bootable Image):是将引导程序和vxWorks融为一体的镜像,它常常是最终产品,包括不驻留ROM的镜像和驻留ROM的镜像两种类型。

VxWorks系统启动流程

一般来说,所有处理器的VxWorks系统启动流程都是相似的,但有些处理器会有一些特殊的步骤,而另一些处理器会跳过几个步骤。

下面以CPU为ARM7TDMI为例具体描述(见表1)。

对于可引导镜像在usrConfig.c文件里执行,对于可下载镜像在bootConfig.c文件里执行。

图2为压缩的可引导镜像的启动过程图示,其中所用到的地址的说明见表2。

BSP的开发过程

建立开发环境

主要是以目标板CPU的BSP文件为模板,在 ornado argetconfig目录下创建用户的BSP目录bspname,把 tornado target config all下的文件和BSP模板文件拷贝到该目录下,根据具体情况选择合适的VxWorks镜像类型。

修改模板程序

Makefile

Makefile文件控制镜像的创建,在Makefile文件里使用了将近135个宏,最简单的Makefile文件要包含以下的宏:

  • CPU:目标板CPU的类型;
  • TOOL:主机的make工具,为GNU;
  • TGT_DIR:target路径,默认为$(WIND_BASE)/target;
  • TARGET_DIR:BSP目录名;
  • VENDER:目标板生产商名;
  • BOARD:目标板名;
  • ROM_TEXT_ADRS:boot ROM的入口地址(以16进制表示,并且与config.h文件定义相同);
  • ROM_SIZE:ROM的大小;
  • RAM_LOW_ADRS:加载vxWorks的目标地址;
  • RAM_HIGH_ADRS:boot ROM拷贝到RAM的目标地址;
  • HEX_FLAGS:特殊结构的标记,用于产生S-记录文件;
  • MACH_EXTRA:扩展文件,用户可以加入自己的目标模块;
  • 除此以外,Makefile文件还需要包括以下文件:
  • $(TGT_DIR)/h/make/defs.bsp:Vxworks系统运行的标准变量定义;
  • (TGTDIR)/h/make/make.(TGT_DIR)/h/make/make.(CPU)$(TOOL):提供了特别的目标机结构和一套编译工具,如make. ARM7TDMI_Tgnu;
  • (TGTDIR)/h/make/defs.(TGT_DIR)/h/make/defs.(WIND_HOST_TYPE):提供了与主机系统有关的定义;
  • rules.bsp:在创建目标文件时所需要的规则;
  • rules. $(WIND_HOST_TYPE):指出创建目标文件时所需的从属文件表;
  • 如果没有用all目录下的文件而是拷贝到bspname下修改并使用,需要定义与这些文件有关的宏,如 BOOTINIT=bootInit.c 这样在创建镜像时就不会用all目录下的文件而使用bspname目录下的相应文件了;
  • 在Makefile文件里凡是16进制数前面都无需加“0x”;

bspname.h

根据具体目标板设置串行接口、时钟以及I/O设备等。在该文件中必须包含以下内容:

  • 中断向量/级别
  • I/O设备地址
  • 设备寄存器位的含义
  • 系统和附加时钟参数(最大和最小速率)

config.h

根据目标板的具体情况配置宏定义,注意ROM_TEXT_ADRS、ROM_SIZE、RAM_LOW_ADRS、RAM_HIGH_ADRS要与Makefile文件里定义的一致,LOCAL_MEM_LOCAL_ADRS和LOCAL_MEM_SIZE要正确。

romInit.s

这是系统上电后运行的第一个程序,根据具体目标板对寄存器和CPU进行设置;以CPU为ARM7TDMI为例,romInit.s 需要进行的工作有:

  • 保存启动方式:对于冷启动,如果CPU配置的是HIGH VECTORS,就设置入口地址为0xFFFF0000,否则设置入口地址为0x00000000;
  • 屏蔽中断:通过设置cpsr的I_BIT和F_BIT都为1来实现,还要设置中断寄存器为关模式,同时设定运行模式为SVC32模式;
  • 初始化堆栈指针pc和sp:堆栈指针sp指向STACK_ADRS,这个宏的定义为当镜像为驻留ROM时,该宏值为_sdata,当镜像为非驻留ROM时该宏值为_romInit,这两个地址经过地址映射后都指向被拷贝镜像在RAM的目标地址;
  • 初始化cache,屏蔽cache;
  • 根据具体目标板的需要初始化其他寄存器;
  • 指针跳转到romStart()函数并执行;

bootConfig.c

一般不需要用户修改,也可以根据具体情况做适当修改;

sysALib.s

与romInit.s文件实现的功能相似,但如果在romInit.s文件里对DRAM和内存控制器进行了初始化在这里不再进行这项工作;

创建VxWorks镜像

根据具体需要在命令行环境下利用Makefile创建各种镜像,也可以在Tornado的集成环境下Build菜单中选择Build Boot ROM来创建各种类型的Boot ROM;

除此以外,如果系统硬件包括串口,还要根据具体情况修改sysSerial.c文件;如果包含网络部分要修改configNet.h;如果包含NVRAM要修改bootnv.h文件。总之,BSP的开发要根据具体目标板的硬件进行。

结语

我们已经利用基于VxWorks的BSP开发包成功的进行了线缆调制解调器的底层软件开发。在开发过程中深感BSP在整个系统软件中的重要性——没有正确的BSP软件,整个系统的启动和运行也不会正确。另外需要强调的是:BSP的概念只是针对嵌入式操作系统而言的,而像DOS、WINDOWS、UNIX等BIOS操作系统是无BSP可言的。对同一目标板而言,不同操作系统的BSP在本质上是一样的,但是不同的操作系统将提供不同的支持库,另外在BSP结构上也可能会有所不同,在做BSP移植的时候应该注意。

VxWorks使用说明书

概述

VxWorks操作系统的集成环境叫Tornado。Tornado集成环境提供了高效明晰的图形化的实时应用开发平台,它包括一套完整的面向嵌入式系统的开发和调测工具。Tornado环境采用主机-目标机交叉开发模型,应用程序在主机的Windows环境下编译链接生成可执行文件,下载到目标机,通过主机上的目标服务器(Target Server)与目标机上的目标代理(Target Agent)的通信完成对应用程序的调试、分析。它主要由以下几部分组成:

  • VxWorks高性能的实时操作系统;
  • 应用编译工具;
  • 交互开发工具;

下面对Tornado集成环境的各组件功能分别介绍:

Tornado开发环境

Tornado是集成了编辑器、编译器、调试器于一体的高度集成的窗口环境,同样也可以从Shell窗口下发命令和浏览。

WindConfig:Tornado系统配置

通过WindConfig可选择需要的组件组成VxWorks实时环境,并生成板级支持包BSP的配置。通过修改config.h可以实现WindConfig的所有功能,并且,可以实现WindConfig不能实现的功能。

WindSh:Tornado外壳

WindSh是一个驻留在主机内的C语言解释器,通过它可运行下载到目标机上的所有函数,包括VxWorks和应用函数。Tornado外壳还能解释常规的工具命令语言TCL。
WindSh不仅可以解释几乎所有的C语言表达式,而且可以实现所有的调试功能。它主要有以下调试功能:下载软件模块;删除软件模块;产生任务;删除任务;设置断点;删除断点;运行、单步、继续执行程序;查看内存、寄存器、变量;修改内存、寄存器、变量;查看任务列表、内存使用情况、CPU利用率;查看特定的对象(任务、信号量、消息队列、内存分区、类);复位目标机。

浏览器

Tornado浏览器可查看内存分配情况、任务列表、CPU利用率、系统目标(如任务、消息队列、信号量等)。这些信息可周期性地进行更新。

CrossWind:源码级调试器

源码级调试器CrossWind提供了图形和命令行方式来调试,可进行指定任务或系统级断点设置、单步执行、异常处理。

有些功能如修改内存、寄存器、变量的值通过菜单操作是无法实现的,需要在WindSh中执行原语操作实现。

驻留主机的目标服务器

目标服务器管理主机与目标机的通信,所有与目标机的交互工具都通过目标服务器,它也管理主机上的目标机符号表,提供目标模块的加载和卸载。

Tornado注册器

所有目标服务器注册其提供的服务在注册器中。注册器映射用户定义的目标名到目标服务器网络地址。

VxWorks

Tornado集成了VxWorks操作系统。

目标代理程序

目标代理程序是一个驻留在目标机中的联系Tornado工具和目标机系统的组件。一般来说,目标代理程序往往是不可见的。

MPC750/MCPN750上bootrom的制作

MPC750/MCPN750上的 bootrom用于初始化MPC750/MCPN750的硬件,通过网口加载 VxWorks 的内核映象。因为MPC750/MCPN750的bootrom已经制作,如果没有特殊需要,不需重新制作bootrom。

制作MPC750/MCPN750上 bootrom的步骤:

  • 在Tornado集成环境下,使用菜单命令Project | Make MCP750/MCPN750 | Comman Targets | clean删除以前生成的bootrom文件,使用菜单命令Project | Make MCP750/MCPN750 | Comman Targets | bootrom.hex生成bootrom文件。生成的bootrom文件在相应的BSP目录中(MCP750为c:\tornado\target\config\mcp750,MCPN750为c:\tornado\target\config\mcpn750)
  • 使用elftobin < bootRom > mcp750.bin 生成mcp750.bin 文件
  • 启动TFTP服务器tftpd32.exe,设置下载文件(mcp750.bin)的目录
  • 用PPCBug启动目标系统
  • 运行并配置超级终端。配置:9600bps波特率、8位数据位、1位停止位、无校验位、无流量控制。
  • 在超级终端中,使用niot命令修改客户(目标机)IP地址和服务器(主机)IP地址
  • 使用niop命令设置加载的文件名(mcp750.bin)
  • 使用pflash 4000:FFF00 ff000100命令写FLASH B。

启动软盘制作

目标机启动软盘用于启动目标机,通过网口使用Ftp协议从主机下载VxWorks映象。在X86平台上,一般使用启动软盘来启动目标机。

启动盘的制作

在实时应用系统的开发调测阶段,往往采用以PC机作为目标机来调测程序。主机PC和目标机PC之间可采取串口或是网口进行联结。由于大多数目标已配有网卡,网络联结成为最简单快速的连接方式。串口联结虽通信速率不高,也有它自己的优点,系统级任务调试(如中断服务程序ISR)需使通信方式工作在Polled 模式,网口联结就不支持,因此可以裁剪掉系统中网络部分,以使VxWorks系统更小,满足目标板的内存约束。下面分别对这两种通信方式下目标机VxWorks系统启动盘的制作作一简要介绍(以PC机为目标系统)。

串口通信时目标机VxWorks系统启动盘的制作步骤:

(1)修改通用配置文件\Tornado\target\config\pc486\config.h.

在config.h文件中加入以下宏定义:

1
2
3
4
5
6
7
8
9
10
#define INCLUDE_WDB 
#define INCLUDE_WDB_TTY_TEST 
#undef WDB_COMM_TYPE 
#define WDB_COMM_TYPE WDB_COMM_SERIAL /*定义通信方式为串口联结*/ 
#define WDB_TTY_CHANNEL 1 /*通道号*/ 
#define WDB_TTY_BAUD 9600 /*串口速率,可设置至38400*/ 
#define WDB_TTY_DEV_NAME "tyCo/1" 
#define CONSOLE_TTY 0 
#define DEFAULT_BOOT_LINE \ 
"fd=0,0(0,0)hostname:/fd0/vxWorks h=主机ip e=目标机ip u=主机上的登录用户名"  

(2)在Tornado集成环境中执行菜单命令Project | Make PC486 | Common Targets | clean删除以前生成的文件,执行菜单命令Project | Make PC486 |  Boot Rom Targets   | bootrom_uncmp编译链接生成bootrom_uncmp ;再选择VxWorks Target,编译生成vxworks。

(3).拷贝\Tornado\target\config\pc486\bootrom_uncmp至\Tornado\host\bin下;

(4).重命名文件bootrom_uncmp为bootrom;

(5).准备一张已格式化的空盘插入软驱;

(6).在目录\Tornado\host\bin下执行命令 mkboot a: bootrom;

(7).拷贝\Tornado\target\config\pc486\VxWorks至软盘;

(8).将系统制作盘插入目标机软驱,加电启动目标机即载入VxWorkst系统。

网口通信时目标机VxWorks系统启动盘的制作步骤: 
1.配置目标机网卡,设置其中断号和输入输出范围(I/O地址); 
2.修改通用配置文件\Tornado\target\config\pc486\config.h. 
针对不同的网卡,其名称不同,如NE2000及其兼容网卡为ENE,3COM以太网卡为ELT,Intel网卡为EEX。 在config.h文件中修改相应网卡类型(如网卡为3COM网卡)的定义部分:

1
2
#define IO_ADRS_ELT 网卡I/O地址 
#define INT_LVL_ELT 网卡中断号 

并且修改#define DEFAULT_BOOT_LINE的定义:

1
2
#define DEFAULT_BOOT_LINE \ 
"elt(0,0)主机标识名:C:\\tornado\\target\\config\\pc486\\vxWorks h=主机IP e=目标机IP u=登录用户名 pw=口令 tn=目标机名"  

DEFAULT_BOOT_LINE宏定义,使缺省配置符合自己的调试环境

1
2
#define DEFAULT_BOOT_LINE \ 
"ene(0,0)host:c:/tornado/target/config/pc486/vxWorks h=129.9.75.39 e=129.9.49.7 u=x86 pw=x86 tn=x86" 

ene(0,0) /* 启动设备为网卡 
host /
 主机标识,可以任意填写,不影响启动过程 
c:\tornado\target\config\pc486\vxWorks  /
 需要从主机加载的映象文件 
h=129.9.75.39  /
 主机的IP地址 
e=129.9.49.7 /
 目标机的IP地址 
u=x86  /
 用户名,主机的Ftp服务器必须有相应的同名用户 
pw=x86 /
 密码,必须与主机的Ftp服务器相应的同名用户的密码相同*/ 
tn=x86  /目标名,可以任意设置,不影响启动过程/

3.主机信息的确定(可无)

主机操作系统Win95安装目录下有一文件hosts.sam,向其中加入: 
主机IP 主机名 
目标机IP 目标机名

4.在Tornado集成环境中点取Project菜单,选取Make PC486,选择Common Target,先进行clean操作;再选择Boot Rom Target,进行bootrom_uncmp操作;再选择VxWorks Target,进行vxworks操作。 
5.拷贝\Tornado\target\config\pc486\bootrom_uncmp至\Tornado\host\bin下; 
6.重命名文件bootrom_uncmp为bootrom;
7.准备一张已格式化的空盘插入软驱; 
8.在目录\Tornado\host\bin下执行命令 mkboot a: bootrom ; 
9.启动Tornado组件FTP Server,在WFTPD窗口中选择菜单Security中的User/right…,在其弹出窗口中选择New User…,根据提示信息输入登录用户名和口令,并且要指定下载文件vxWorks所在根目录;还必选取主菜单Logging中Log options,使Enable Logging、Gets 、Logins 、Commands 、Warnings能。 
10.将系统制作盘插入目标机软驱,加电启动目标机即通过FTP方式从主机下载VxWorkst系统。

2)主机Tornado环境配置

串口联结时主机Tornado开发环境的目标服务器配置操作如下: 
1.在Tornado集成环境中点取Tools菜单,选取Target Server,选择config…; 
2.在Configure Target Servers窗口中先给目标服务器命名; 
3.在配置目标服务器窗口中的"Change Property"窗口中选择Back End,在"Available Back"窗口中选择wdbserial,再在"Serial Port"窗口中选择主机与目标机连接所占用的串口号(COM1,COM2),再在"Speed(bps)"窗口中选择主机与目标机间串口速率。 
4. 在配置目标服务器窗口中的"Change Property"窗口中选择Core File and Symbols, 
选择File为BSP目标文件所在目录(本例为PC486目录)的VxWorks.st,并选取为All Symbols. 
5.在配置目标服务器窗口中的"Change Property"窗口中的其它各项可根据需要选择。

网口联结时主机Tornado开发环境的目标服务器配置操作如下: 
1.在Tornado集成环境中点取Tools菜单,选取Target Server,选择config…; 
2.在Configure Target Servers窗口中先给目标服务器命名; 
3.在配置目标服务器窗口中的"Change Property"窗口中选择Back End,在"Available Back"窗口中选择wdbrpc,在"Target IP/Address"窗口中输入目标机IP。 
4. 在配置目标服务器窗口中的"Change Property"窗口中选择Core File and Symbols, 
选择File为BSP目标文件所在目录(本例为PC486目录)的VxWorks,并选取为All Symbols. 
5.在配置目标服务器窗口中的"Change Property"窗口中的其它各项可根据需要选择。

以上的串口和网口联结配置完成后,可按以下步骤和目标机建立连接: 
1.点击Launch按钮,连接主机和目标机,全部出现successed后即可进入应用程序调试。 
2.点击图形按钮中下拉框,选择和主机相连的目标机。 
3.选择Debugger菜单项中Download…,下载应用程序到目标板。 
4. 选择Debugger菜单项中Run…,调测应用程序中某一任务或功能函数。

VxWorks系统目标机启动盘的制作步骤: 
 配置目标机网卡,设置其中断号和输入输出范围(I/O地址); 
 修改配置文件C:\Tornado\target\config\pc486\config.h。 
不同的网卡,其名称不同,如NE2000及其兼容网卡为ENE,3COM以太网卡为ELT,Intel网卡为EEX。根据目标机的网卡类型、中断号、I/O地址修改config.h文件中的相应宏定义。以下以NE2000网卡(中断号为10,I/O地址为0X300)为例说明配置过程。 
#define IO_ADRS_ENE 0x300 /网卡I/O地址
#define INT_LVL_ENE 0x0a /网卡中断号
* 因为用软盘启动目标机无法保存修改的信息,修改config.h文件中的DEFAULT_BOOT_LINE宏定义,使缺省配置符合自己的调试环境 
#define DEFAULT_BOOT_LINE \ 
“ene(0,0)host:c:/tornado/target/config/pc486/vxWorks h=129.9.75.39 e=129.9.49.7 u=x86 pw=x86 tn=x86” 
ene(0,0) /* 启动设备为网卡 
host /
 主机标识,可以任意填写,不影响启动过程 
c:\tornado\target\config\pc486\vxWorks  /
 需要从主机加载的映象文件 
h=129.9.75.39  /
 主机的IP地址 
e=129.9.49.7 /
 目标机的IP地址 
u=x86  /
 用户名,主机的Ftp服务器必须有相应的同名用户 
pw=x86 /
 密码,必须与主机的Ftp服务器相应的同名用户的密码相同*/ 
tn=x86  /目标名,可以任意设置,不影响启动过程
焈 在Tornado集成环境中执行菜单命令Project | Make PC486 | Common Targets | clean删除以前生成的文件,执行菜单命令Project | Make PC486 |  Boot Rom Targets | bootrom_uncmp编译链接生成bootrom_uncmp。

  • 拷贝c:\Tornado\target\config\pc486\bootrom_uncmp至c:\Tornado\host\bin下;
  • 重命名文件bootrom_uncmp为bootrom;
  • 准备一张已格式化的空盘插入软驱;
  • 在目录c:\Tornado\host\bin下执行命令 mkboot    a:     bootrom ;
  • 启动盘制作完成
    2VxWorks使用说明书(2)
    使用步骤 
    使用Tornado集成环境一般需要经过以下步骤: 
    运行TCP/IP端口管理器Portmapper(portmap.exe) 
    运行注册器Tornado Registry(wtxregd.exe)。如果使用的是试用版,注意是否注册成功,是否修改了日期 
    运行并配置Ftp Server(wtfpd32.exe)。点击菜单命令Security | Users | rights弹出配置窗口,点击New User,在New User弹出窗口加入需要加入的用户名(注意:要与目标机bootrom或启动软盘设置一致),在改变密码弹出窗口设置该用户的密码(注意:要与目标机bootrom或启动软盘设置一致),在Home Directory中设置相应的目录,MCP750为c:\tornado\target\config\mcp750,MCPN750为c:\tornado\target \config\mcpn750,X86为c:\tornado\target\config\pc486 ,配置结束。 
    目标板上电或复位 
    在控制台(MCP750/MCPN750为超级终端,X86为目标机显示器)上可以看到启动信息。如果需要修改,在等待用户配置时,按c键,进行相应修改。(注意:配置信息要与主机配置、Ftp服务器配置一致),修改结束后,按@键重新启动目标机。 
    运行Tornado(tornado.exe) 
    执行菜单命令Tools | Target Server | Configure,弹出目标服务器设置对话框,点击New产生一个新的配置。设置Description域(可以任意设置);设置Target Server域(可以任意设置);在Change Property域选取Back End项(该项设置主机与目标机如何连接,缺省为网口连接,如果使用串口连接,需要修改configall.h文件,重新编译链接VxWorks映象),如果使用网口调试,选择wdbrpc,在目标IP名或地址域中给出目标机的IP名或地址(建议给出IP名,因为这样会快得多),如果给出的是IP名,需要在HOSTS文件中给出IP名与IP地址的对应关系,如果用串口调试,选择wdbserial,选择相应的串口和波特率(注意:此处的串口是指主机的串口不是目标机的串口);在Change Property域选取Core File and Symbols项,选中File项输入相应的文件(MCP750c:\tornado\target\config\mcp750\vxWorks,MCPN750为c:\tornado\target  \config\mcpn750\vxWorks,在X86平台上为c:\tornado\target\config\pc486\vxWorks),点击Launch,运行目标服务器。 
    执行菜单命令File | New 创建一个新的文件,并打开编辑器Editor(该编辑器功能不是很强大,可以使用其它编辑器如Source Insight)。 
    单独编译生成的源文件,生成目标文件(.o),编译连接过程的详细介绍请见后面。 
    选取相应的目标服务器。 
    执行菜单命令Tools | Debugger运行调试器。 
    执行菜单命令Debug | Download下载要调试的目标文件(.o) 
    在Editor窗口设置断点。 
    执行菜单命令Debug | Run弹出对话框,要求输入调试入口函数,输入要调试的函数。 
    进行源码级调试 
    执行菜单命令Tools | Shell运行Shell。可以在Shell窗口查看/修改全局变量、内存,查看任务列表、各模块使用内存的情况、对象(如任务、队列、信号量、定时器、内存分区)等信息。 
    执行菜单命令Tools | Browser运行Browser。在Browser中可以查看任务列表、各模块使用内存的情况、对象(如任务、队列、信号量、定时器、内存分区)等信息。 
    6、编译链接 
    VxWorks的开发调试环境可以把VxWorks内核和应用分开,分别加载。 VxWorks内核在目标机启动过程中通过ftp协议加载到目标机中运行,应用模块在调试中动态下载,目标代理把下载的应用模块动态链接到系统中,应用模块的调试是通过在用户执行运行命令时提供入口函数实现的。这样做的好处是需要调试哪个模块就下载那个模块调试,不需下载其它模块,前期调试一般使用这种编译方式。  
    VxWorks的开发调试环境也提供把应用模块与系统内核链接在一起,通过ftp协议加载执行。这需要经过两个步骤:把应用模块的入口代码加到usrConfig.c文件中的usrRoot函数的尾部;把应用模块编译链接到VxWorks内核中,这种编译链接方式一般用于后期调试。 
    下面分类对编译链接进行介绍 
    6.1 单个应用模块的编译 
    单个应用模块的编译可以通过使用菜单命令Project | Make Current Source File进行编译,要编译的源文件必须已经用Editor打开并且为当前窗口。如果要编译的源文件所在目录没有makefile文件,系统会提示创建一个新的makefile文件,确定。在弹出的创建缺省makefile窗口的CPU域选择相应的项(MCP750/MCPN750选择PPC604,X86选择I80486),在ADDED_FLAGS域输入-g,确定。系统对源文件进行编译,生成目标文件(.o)。生成的目标文件在Debugger环境中动态加载,与内核动态链接到一起。 
    6.2 系统内核vxWorks的编译链接 
    系统内核vxWorks是调试中使用最多的内核映象。它被通过Ftp协议从主机加载到目标机中。它的作用通常是进行软硬件初始化,等待加载应用模块,进行程序调试。 
    在Project菜单下,选择相应硬件平台的生成vxWorks的命令,进行编译链接。在编译链接之前先使用clean命令删除以前生成的文件。 
    6.3 应用模块与系统内核一起编译链接 
    VxWorks的开发调试环境也提供把应用模块与系统内核链接在一起,通过Ftp协议加载,vxWorks内核自动执行应用模块。这需要经过两个步骤:把应用模块的入口代码加到usrConfig.c文件中的usrRoot函数的尾部;在makefile中把待生成的应用模块的目标文件名加到宏定义MACH_EXTRA中,再把相应的编译规则加到makefile中。编译链接生成vxWorks映象。 
    6.4 Project菜单下其它编译链接命令介绍
  • vxWorks_rom :可以写到ROM的、没有带符号表和Shell的、没有压缩的vxWorks。
  • vxWorks.st :带有符号表的vxWorks。
  • vxWorks.st_rom:可以写到ROM的、带有符号表和Shell的、压缩的vxWorks。
  • vxWorks.res_rom :可以写到ROM的、带有符号表和Shell的、只有数据段拷贝到内存的、没有压缩的vxWorks。
  • vxWorks.res_rom_nosym:可以写到ROM的、只有数据段拷贝到内存的、没有压缩的vxWorks。
  • bootrom:压缩的 bootrom
  • bootrom_uncmp:没有压缩的bootrom  
    7、调试时的常用方法 
    下面是一些调试手段在调试器中的相应命令(操作) 
    调试手段  相应操作 
    设置断点  菜单命令Debug|Toggle BreakPoint 
    删除断点  菜单命令Debug|Toggle BreakPoint 
    运行  菜单命令Debug|Run 
    单步执行(进入函数)  菜单命令Debug|Step 
    单步执行(不进入函数)  菜单命令Debug|Next 
    继续执行(停下后的程序) 菜单命令Debug|Continue 
    执行完当前的函数,停在调用它的函数的下一条语句  菜单命令Debug|Finish 
    查看变量的值  菜单命令Debug|Inspect 
    查看当前函数的所有局部变量  菜单命令Debug|Locals 
    查看内存  菜单命令Debug|Memory 
    查看寄存器  菜单命令Debug|Registers 
    修改内存  Shell命令m 
    修改寄存器  Shell命令mRegs 
    修改变量  在Shell中直接给该变量赋值(局部变量无法用此方法修改) 
    卸载一个加载的模块  Shell命令unld 
    删除任务  Shell命令td 
    复位目标机  Shell命令reboot 
    (用该命令的好处:目标服务器自动与目标代理重新链接,Shell自动重启 )
    查看任务  在Browser对象信息窗口输入待查看的任务名或ID 
    查看信号量  在Browser对象信息窗口输入待查看的信号量名或ID 
    查看消息队列  在Browser对象信息窗口输入待查看的消息队列命或ID 
    内存分区  在Browser对象信息窗口输入待查看的内存分区ID 
    看门狗  在Browser对象信息窗口输入待查看的看门狗ID 
    类(class)  在Browser对象信息窗口输入待查看的类的ID 
    查看内存使用(模块使用内存的情况)  Browser的内存使用窗口 
    查看任务列表(系统里的所有任务)  Browser的任务列表窗口 
    查看CPU占用率  Browser的Spy窗口 
    查看堆栈使用情况  Browser的堆栈检查窗口 
    注:
  • Shell可以通过菜单命令Debug | Shell启动
  • Shell的原语可以通过在Shell中输入help列出
  • Browser可以通过菜单命令Debug | Browser启动
  • Debugger命令窗口的命令可以通过在命令窗口输入help列出 
    8、任务调试模式下的多任务调试 
    在任务调试模式下,在一个集成环境中,在一个任务中调试,在另一个任务中设置断点,设置的断点不起作用。这是因为一个调试器只能处理一个TCB(任务控制块),每个任务都有一个TCB,因此一个调试器只能调试一个任务,要调试几个任务就要启动几个调试器。一个集成环境只能启动一个调试器,所以要调试几个任务就要启动几个集成环境。另外,需要在被调试的任务的待调试的第一条语句前加入taskSuspend(0)语句,挂起该任务,否则任务就可能会在调试前被执行。 
    下面是多任务调试的测试用例的源代码 
    /* VxWorks includes */ 
    #include        “vxWorks.h” 
    #include        “taskLib.h” 
    #include        “stdio.h” 
    #include        “msgQLib.h” 
    int g_lTaskATid; 
    int g_lTaskBTid; 
    MSG_Q_ID g_MsgQ1id; 
    MSG_Q_ID g_MsgQ2id;

void MultiTaskTestTaskA(void)  

    char cMsgToTaskB[100]; 
    char cMsgFromTaskB[100];

sprintf(cMsgToTaskB,“To TaskB \n”); 
    printf(" Hello from MultiTaskTestTaskA \n"); 
    /start point of debugging for  MultiTaskTestTaskA
    taskSuspend(0);      
    for(;😉 
    {     
        printf(" Hello from MultiTaskTestTaskA \n"); 
        /Send message to MultiTaskTestTaskB/              
       msgQSend(g_MsgQ1id,cMsgToTaskB,sizeof(cMsgToTaskB),WAIT_FOREVER,MSG_PRI_NORMAL);
        /Receive message from MultiTaskTestTaskB
        msgQReceive(g_MsgQ2id,cMsgFromTaskB,100,WAIT_FOREVER); 
        printf("%s",cMsgFromTaskB); 
    } 

void MultiTaskTestTaskB(void)   

    char cMsgToTaskA[100]; 
    char cMsgFromTaskA[100]; 
    sprintf(cMsgToTaskA,“To TaskA \n”); 
    printf(" Hello from MultiTaskTestTaskB \n"); 
    /start point of debugging for  MultiTaskTestTaskA
    taskSuspend(0); 
    for(;😉 
    { 
        printf(" Hello from MultiTaskTestTaskB \n");          
        /Send message to MultiTaskTestTaskA
msgQSend(g_MsgQ2id,cMsgToTaskA,sizeof(cMsgToTaskA),WAIT_FOREVER,MSG_PRI_NORMAL); 
        /Receive message from MultiTaskTestTaskA
        msgQReceive(g_MsgQ1id,cMsgFromTaskA,100,WAIT_FOREVER); 
        printf("%s",cMsgFromTaskA); 
    } 

/This function spawns MultiTaskTestTaskA and MultiTaskTestTaskB , creates g_MsgQ1id and g_MsgQ2id ,  is entry for debugging.
void  MultiTaskTestInit(void) 

    printf(" Hello from MultiTaskTestInit \n"); 
    g_MsgQ1id=msgQCreate(20,100,MSG_Q_FIFO); 
    if(g_MsgQ1idNULL) 
    { 
        printf(" ERROR: create g_MsgQ1 error \n"); 
    } 
    g_MsgQ2id=msgQCreate(20,100,MSG_Q_FIFO); 
    if(g_MsgQ1id
NULL) 
    { 
        printf(" ERROR: create g_MsgQ2 error \n"); 
    } 
    printf(" Spawning a new task called MultiTaskTestTaskA \n\n"); 
    g_lTaskATid = taskSpawn(“MultiTaskTestTaskA”,100,0,10000,(FUNCPTR)MultiTaskTestTaskA, 0,0,0,0,0,0,0,0,0,0); 
    if(g_lTaskATid == ERROR) 
    {  
        printf(" ERROR: task did not spawn \n"); 
        exit(1); 
    } 
    printf(" Spawning a new task called MultiTaskTestTaskB\n"); 
    g_lTaskBTid = taskSpawn(“MultiTaskTestTaskB”, 100,0,10000,(FUNCPTR)MultiTaskTestTaskB, 0,0,0,0,0,0,0,0,0,0); 
   if(g_lTaskBTid == ERROR) 
    {  
        printf(" ERROR: task did not spawn \n"); 
        exit(1); 
    }   
  exit(0); 

多任务调试步骤:

  • 用-g选项编译源代码产生目标文件
  • 下载产生的目标文件
  • 在MultiTaskTestInit函数的开始设置断点
  • 把MultiTaskTestInit设置为调试任务的人口函数
  • 单步执行产生MultiTaskTestTaskA任务的语句后可以在串口(超级终端)上看到字符串Hello from MultiTaskTestTaskA,用Browser查看任务,可以看到任务MultiTaskTestTaskA出于挂起态(suspended),表明程序执行了taskSuspend(0)语句。
  • 运行另一个Tornado集成环境
  • Attach任务MultiTaskTestTaskA,
  • 在语句msgQReceive(g_MsgQ2id,cMsgFromTaskB,100,WAIT_FOREVER)的下一条语句处设置断点
  • 运行任务MultiTaskTestTaskA。可以看到没有执行到断点处,用Browser查看任务状态,MultiTaskTestTaskA出于阻塞态(pended),因为它在等待消息。
  • 单步执行MultiTaskTestInit到产生MultiTaskTestTaskB任务的下一条语句,可以看到MultiTaskTestTaskB任务处于挂起态
  • 再运行另一个Tornado集成环境
  • Attach任务MultiTaskTestTaskB,
  • 在语句msgQReceive(g_MsgQ1id,cMsgFromTaskA,100,WAIT_FOREVER)下一条语句处设置断点
  • 运行任务MultiTaskTestTaskB。可以看到执行到断点处停下。这是因为MultiTaskTestTaskA任务已经发送一条消息到MultiTaskTestTaskB的接收队列中。
  • 此时,可以看到MultiTaskTestTaskA任务也运行到断点处,因为为MultiTaskTestTaskB任务已经发送一条消息到MultiTaskTestTaskA的接收队列中。  
    9、系统调试模式下程序的调试 
    Tornado集成环境提供两种调试模式:任务调试模式和系统调试模式。在任务调试模式下,在一个集成环境下一个时间内只能调试一个任务。调试只影响当前被调试的任务,其它任务正常运行。在系统调试模式下,可以同时调试多个任务、中断服务程序(ISR),调试影响整个系统。 
    Tornado1.0集成环境下,在系统模式下进行程序调试,主机与目标机之间必须使用串口通信。Tornado2.0集成环境提供了通过网口进行系统模式调试的功能。 
    系统缺省使用网口通信,如果需要使用串口通信,需要修改文件C: \ Tornado \ target \ config  
    } all \ configAll.h的一些宏定义,修改为: 
    #define WDB_COMM_TYPE        WDB_COMM_SERIAL   /使用串口通信
    #define WDB_TTY_CHANNEL    0                                         /使用第一个串口
    #define WDB_TTY_BAUD          38400                                 /波特率:38400bps/

重新编译链接vxWorks。 
在启动目标服务器时,要选择串口通信,并进行相应配置。 
9.1 系统调试模式下多任务的调试: 
调试使用的源代码与任务调试模式中使用的代码相同。但是,需要去掉为了能够在任务调试模式下进行多任务调试的MultiTaskTestTaskA和MultiTaskTestTaskB中的语句taskSuspend(0);

多任务调试步骤:

  • 用-g选项编译源代码产生目标文件。
  • 下载产生的目标文件。
  • 在MultiTaskTestInit函数的开始设置断点。
  • 在Debugger命令窗口输入命令attach system进入系统调试模式。
  • 在Shell窗口输入命令sp MultiTaskTestInit产生一个以MultiTaskTestInit为入口函数的任务,因为整个系统都停下了,新产生的任务还没有执行,这可以通过在Debugger命令窗口输入命令info threads显示当前系统中的任务列表看出来。
  • 执行菜单命令Debug | Continue继续运行程序。
  • 系统在设置的断点处停下。
  • 在函数MultiTaskTestTaskA中的语句msgQReceive(g_MsgQ2id,cMsgFromTaskB, 100,WAIT_FOREVER)的下一条语句处设置断点。
  • 在函数MultiTaskTestTaskB中的语句msgQReceive(g_MsgQ1id,cMsgFromTaskA, 100,WAIT_FOREVER)的下一条语句处设置断点。
  • 执行菜单命令Debug | Continue继续运行程序。
  • 程序在任务MultiTaskTestTaskB中的断点处停下(为什么不是在任务MultiTaskTestTaskA中停下?请考虑)。
  • 执行菜单命令Debug | Continue继续运行程序。
  • 程序在任务MultiTaskTestTaskA中的断点处停下。
  • 执行菜单命令Debug | Continue继续运行程序。
  • 程序又一次在任务MultiTaskTestTaskA中的断点处停下(为什么停两次?请考虑)。
  • 执行菜单命令Debug | Continue继续运行程序。
  • 程序在任务MultiTaskTestTaskB中的断点处停下。 
    9.2 中断服务程序的调试 
    中断服务程序只能在系统调试模式下调试,不能在任务调试模式下调试。因为中断服务程序是作为系统的一部分运行,不是以任务方式运行,因此不需要为它产生任务。 
    中断服务程序调试步骤:
  • 用-g选项编译源代码产生目标文件。
  • 下载产生的目标文件。
  • 在MultiTaskTestInit函数的开始设置断点。
  • 在Debugger命令窗口输入命令attach system进入系统调试模式。
  • 执行菜单命令Debug | Continue继续运行程序。
  • 如果产生相应的中断,程序就会在中断服务程序的断点处停下。进行需要的调试。

VxWorks操作系统指南__任务管理

任务管理

任务是代码运行的一个映象,从系统的角度看,任务是竞争系统资源的最小运行单元。任务可以使用或等待CPU、I/O设备及内存空间等系统资源,并独立于其它任务,与它们一起并发运行(宏观上如此)。VxWorks内核使任务能快速共享系统的绝大部分资源,同时有独立的上下文来控制个别线程的执行。

任务结构

多任务设计能随时打断正在执行着的任务,对内部和外部发生的事件在确定的时间里作出响应。VxWorks实时内核Wind提供了基本的多任务环境。从表面上来看,多个任务正在同时执行,实际上,系统内核根据某一调度策略让它们交替运行。系统调度器使用任务控制块的数据结构(简记为TCB)来管理任务调度功能。任务控制块用来描述一个任务,每一任务都与一个TCB关联。TCB包括了任务的当前状态、优先级、要等待的事件或资源、任务程序码的起始地址、初始堆栈指针等信息。调度器在任务最初被激活时以及从休眠态重新被激活时,要用到这些信息。

此外,TCB还被用来存放任务的"上下文"(context)。任务的上下文就是当一个执行中的任务被停止时,fPW"教T供\aI管h;`所要保存的所有信息。在任务被重新执行时,必须要恢复上下文。通常,上下文就是计算机当前的状态,也即各个寄存器的内容。如同在发生中断所要保存的内容一样。当发生任务切换时,当前运行的任务的上下文被存入TCB,将要被执行的任务的上下文从它的TCB中取出,放入各个寄存器中。于是转而执行这个任务,执行的起点是前次它在运行时被中止的位置。

VxWorks中,内存地址空间不是任务上下文的一部分。所有的代码运行在同一地址空间。如每一任务需各自的内存空间,需可选产品VxVMI的支持。

任务状态和状态迁移

实时系统的一个任务可有多种状态,其中最基本的状态有四种:

  • 就绪态:任务只等待系统分配CPU资源;
  • 悬置态:任务需等待某些不可利用的资源而被阻塞;
  • 休眠态:如果系统不需要某一个任务工作,则这个任务处于休眠状态;
  • 延迟态:任务被延迟时所处状态;

当系统函数对某一任务进行操作时,任务从一种状态迁移到另一状态。处于任一状态的任务都可被删除。

状态迁移调用

1
2
3
4
5
6
7
8
9
10
就绪态 ----> 悬置态semTake()/msgQReceive() 
就绪态 ----> 延迟态taskDelay()
就绪态 ----> 休眠态taskSuspend()
悬置态 ----> 就绪态semGive()/msgQSend()
悬置态 ----> 休眠态taskSuspend()
延迟态 ----> 就绪态expireddelay
延迟态 ----> 休眠态taskSuspend()
休眠态 ----> 就绪态taskResume()/taskActivate()
休眠态 ----> 悬置态taskResume()
休眠态 ----> 延迟态taskResume()

任务调度策略

多任务调度须采用一种调度算法来分配CPU给就绪态任务。Wind内核采用基于优先级的抢占式调度法作为它的缺省策略,同时它也提供了轮转调度法。

基于优先级的抢占式调度,它具有很多优点。这种调度方法为每个任务指定不同的优先级。没有处于悬置或休眠态的最高优先级任务将一直运行下去。当更高优先级的任务由就绪态进入运行时,系统内核立即保存当前任务的上下文,切换到更高优先级的任务。

多任务调度须采用一种调度算法来分配CPU给就绪态任务。Wind内核采用基于优先级的抢占式调度法作为它的缺省策略,同时它也提供了轮转调度法。

基于优先级的抢占式调度,它具有很多优点。这种调度方法为每个任务指定不同的优先级。没有处于悬置或休眠态的最高优先级任务将一直运行下去。当更高优先级的任务由就绪态进入运行时,系统内核立即保存当前任务的上下文,切换到更高优先级的任务。

Wind内核划分优先级为256 级(0~255)。优先级0为最高优先级,优先级255为最低。当任务被创建时,系统根据给定值分配任务优先级。然而,优先级也可以是动态的,它们能在系统运行时被用户使用系统调用taskPrioritySet()来加以改变,但不能在运行时被操作系统所改变。

轮转调度法分配给处于就绪态的每个同优先级的任务一个相同的执行时间片。时间片的长度可由系统调用KernelTimeSlice()通过输入参数值来指定。很明显,每个任务都有一运行时间计数器,任务运行时每一时间滴答加1。一个任务用完时间片之后,就进行任务切换,停止执行当前运行的任务,将它放入队列尾部,对运行时间计数器置零,并开始执行就绪队列中的下一个任务。当运行任务被更高优先级的任务抢占时,此任务的运行时间计数器被保存,直到该任务下次运行时。

抢占禁止

Wind内核可通过调用taskLock()和taskUnlock()来使调度器起作用和失效。当一个任务调用taskLock()使调度器失效,任务运行时没有基于优先级的抢占发生。然而,如果任务被阻塞或是悬置时,调度器从就绪队列中取出最高优先级的任务运行。当设置抢占禁止的任务解除阻塞,再次开始运行时,抢占又被禁止。这种抢占禁止防止任务的切换,但对中断处理不起作用。

异常处理

程序代码和数据的出错,如非法命令、总线或地址错误、被零除等。VxWorks异常处理包,一般是将引起异常的任务休眠,保存任务在异常出错处的状态值。内核和其它任务继续执行。用户可借助Tornado开发工具,查看当前任务状态,从而确定被休眠的任务。

任务管理

VxWorks内核的任务管理提供了动态创建、删除和控制任务的功能,具体实现通过如下一些系统调用:

  • taskSpawn()创建(产生并激活)新任务
  • taskInit() 初始化一个新任务
  • taskActivate() 激活一个已初始化的任务
  • taskName() 由任务ID号得到任务名
  • taskNameToId()由任务名得到任务ID号
  • taskPriorityGet()获得任务的优先级
  • taskIsSuspended()检查任务是否被悬置
  • taskIsReady()检查任务是否准备运行
  • taskTcb()得到一个任务控制块的指针
  • taskDelete() 中止指定任务并自由内存(仅任务堆栈和控制块)
  • taskSafe() 保护被调用任务
  • taskSuspend()悬置一个任务
  • taskResume() 恢复一个任务
  • taskRestart()重启一个任务
  • taskDelay()延迟一个任务

VxWorks中的多任务通讯机制

通常,在一个实时系统中,存在着多个并发的任务来协同实现系统的功能,操作系统必须为这些任务提供快速且功能强大的通信机制。在VxWorks系统中,有信号量(semaphore)、消息队列(message queue)、管道(pipe)、事件(event)等通信机制,对一个系统开发人员来说,如何合理地使用这些通信机制,是系统能够长期高效、可靠、安全运行的关键。

信号量(semaphore)

在VxWorks种,信号量是提供任务间同步和互斥的最快速、开销最小的机制,VxWorks有三种不同类型的信号量:

  • 二进制信号量:可用于2个任务之间的同步工作。如任务A必须在任务B完成特定的动作以后才能进行,在这种情况下,任务A可以获取信号量而处于阻塞(pend)状态,任务B在完成特定的动作后释放该信号量。一般来说二进制信号量适用于一对一的任务之间的同步。
  • 互斥信号量:主要用于任务之间共享数据区的互斥保护,具有优先级反转、安全删除、递归等特性。在有2个或2个以上的任务共享一个数据区的时候,必须使用互斥机制进行保护。
  • 计数器信号量:类似于二进制信号量,但是对信号量的释放、获取有计数功能,而二进制信号量则只有0和1两种状态。

VxWorks提供了一组管理信号量的函数接口供开发者使用,包括创建、删除、获取、释放等。

虽然信号量具有快速、开销小的优点,但也有它的局限性,首先它无法提供额外的信息,其次对于一个任务必须与多个任务进行同步的情况,信号量也无能为力。因此在许多场合,信号量必须与其它通信机制配合使用来完成任务之间的通信。

消息队列(message queue)

消息队列是VxWorks提供的单个CPU中的任务之间通信的主要机制之一。消息队列允许基于FIFO或基于任务优先级方式排队消息,一个消息队列的消息数目和消息长度可以由开发者在创建消息队列时指定。在理论上,VxWorks允许多个任务向同一个消息队列发送消息,或者从同一个消息队列接收消息;而在实际应用中,一般来说只有一个任务从消息队列接收消息,有一个或多个任务发送消息,即这个消息队列有多个生产者,而只有一个消费者。消息队列时单向的,对于需要进行双向通信的两个任务,必须使用两个消息队列。消息队列非常适合于Client-Server结构的任务之间的通信,如图一,任务Client1和Client2都需要任务Server的服务,它们通过消息队列“Request Queue”向任务Server发送请求和参数,任务Server处理请求后分别通过“Reqpy Queue 1”和“Reqpy Queue 2”向这两个任务返回结果。

在VxWorks中,消息队列是一种代价比较高的一种通信机制,因此在使用时应该使消息的长度尽量短,而且应避免在需要十分频繁通信的场合使用消息队列。另外消息队列中的消息是排队的,即使是完全相同的消息,后面的消息也不会覆盖前面的消息。

管道(pipe)

在VxWorks中,管道是一种通过虚拟的I/O设备来实现的消息队列通信机制。使用函数pipeDevCreate()和pipeDevDelete()来生成和删除管道,管道一经生成后,任务之间就可以使用标准I/O操作主要是read()和write()进行通信。管道的优点在于它是一个I/O设备,与标准的VxWorks I/O一样,可以使用select机制,而有了select机制,一个任务很方便地使用多个异步I/O设备,如任务要处理同时从串口、管道、socket接收到的数据,就可以使用select。

事件(event)

在5.5版本之前,VxWorks并没有事件这一通信机制。事件(event) 最早出现在pSOS实时操作系统中,在风河公司收购了pSOS之后,从VxWorks 5.5之后,加入了事件机制,并在pSOS事件的基础上做了增强和改进。事件可用于任务和中断服务程序ISR之间、任务和任务之间、任务和VxWorks资源之间进行通信。任务用函数eventReceive()来接收它关心的事件,用eventSend()来向另一个任务发送事件。

VxWorks资源主要是指信号量和消息队列,一个任务要想从VxWorks资源接收到事件,必须先进行注册(register),那么当资源处于FREE状态时,会向注册过的任务发送一个事件。对于每一个VxWorks资源,最多只允许有一个任务注册。如对于消息队列,任务可以使用函数msgQEvStart()来进行注册,那么当有消息到达这个消息队列而又没有任务等待这个消息队列时,会向这个任务发送一个事件,表明消息队列可用。而对于信号量,可以用函数semEvStart()来进行注册。但必须注意的是,一个任务接收到资源发送的事件后,并不能保证这个任务能获取该资源,如获取信号量、从消息队列接收到消息。

在VxWorks中,每一个任务都有一个32位事件寄存器,其中高8位由VxWorks系统保留,开发者可以使用低24位,其每一位表示一种事件,而事件的意义则完全有任务来定义,因此对于不同的任务,相同的位可能有不同的意义。而VxWorks并不对事件进行计数,而只表示该事件发生过,这与消息队列不同,因此接收事件的任务并不能知道接受到的事件发生的次数。

事件非常适合于一个任务必须与多个任务进行通信的场合,如任务A必须同时与任务B、任务C、任务D进行通信,其中任务B通过消息队列向任务A发送数据,其发送频率较低,它要求任务A必须及时进行处理;而任务C则只是向任务A指示一种状态,但频率很高;而任务D用来通知任务A释放动态申请的资源,并停止运行。在这种场合,事件机制能很好地解决问题。

总结

在VxWorks中,任务之间高效、经济地通信对整个系统的性能有很大的影响。一般来说并不能使用一种单一的通信机制就能解决问题,而是需要同时使用多种通信机制。另外,对任务的合理划分,又能简化任务之间的通信。总之,开发者必须通过足够的实践,才能充分利用VxWorks的各种通信机制,设计出高效、可靠的实时系统

VxWorks任务编程中常见异常分析

在任务运行过程中,会出现一些异常的情况,导致任务不能正常运行或者对操作系统造成影响。一般来说,这些异常是由程序的逻辑错误造成的,防止这些异常情况的出现和出现后进行补救就有格外重要的意义。

代码重入与共享

在应用中,可能会出现多个任务调用同一段代码的情况,由于任务占用CPU是串行的,不会出现代码资源使用冲突。但是,不同优先级的任务同时调用同一段代码,则可能出现低优先级任务执行某一函数时被执行该函数的高优先级任务打断的情况,如果函数中要改写全局变量而没有使用互斥,就有可能导致错误的存取。例如在中断中调用内存分配或者释放函数,如果某个任务正在调用内存分配函数或者是内存释放函数,打断该任务时会造成异常,可能导致内存泄漏,甚至有可能会因在中断中异常而reboot。另外,如果多个任务共用的代码中有全局变量且使用目的不同,或者多个任务的代码中有全局变量同名的情况,则有可能造成变量使用中的错误。VxWorks提供了任务变量(taskVar)的方法来解决这个问题,任务可以将使用的全局变量作为任务变量独立使用,添加的任务变量保存在任务的上下文中,任务切换时保存当前内容。

符号表的使用

VxWorks中有模块(module)的概念。装载模块完成目标代码文件在内存中的链接,并可以将目标代码文件中的函数与全局变量加入符号表。符号表中的符号对C语言编写的函数以原来名字命名,对于C++语言的函数则是在后面加上形参的数据类型作为符号名。如f1( )的符号名为f1__Fv,最后的v表示void类型;f2(int)符号名为f2__Fi,f3(int,int)为f3__Fii,依此类推。代码的编译过程中并不对要使用的函数和变量进行检查。例如调用一个并不存在的函数编译并不报错,编译器认为此函数可能在操作系统内核中或者已经下载的目标文件中,但在目标文件下载时会找不到要调用的函数。如果符号表中的符号出现了重名,譬如两次下载的目标文件中有函数重名,则要作散列处理,之后对该函数的调用是最后加入符号表的函数,而之前已经装载的模块则不会受到影响。如果应用程序中使用了与操作系统内核同名的符号,则对操作系统某些API函数的调用将会失败。

特殊的任务保护

在VxWorks中,当一个任务被删除,其它任务不会得到通知,而且由于任务间的独立性,每一个任务可以无限制地删除其它任务。在应用中,我们可能会把需要保护任务误删除。VxWorks 提供的两个函数taskSafe( )和taskUnsafe( )将通知意外删除任务而引起的问题。当任务调用taskSafe( )时,从调用的那一刻起,该任务就被保护起来而不会被其它任务删除。如果任务1试图删除已经调用taskSafe( )的任务2,则任务1将被阻塞,直到任务2调用taskUnsafe( )。保护只能由任务自己实现,一个任务不能safe或unsafe另外一个任务。taskSafe( )和taskUnsafe( )支持嵌套模式。如果有嵌套发生,一个计数器将开始工作,每有一个taskSafe( )被调用,则计数器加1;调用1个taskUnsafe( ),则计数器减1。只有当计数器为0时,才能删除该任务。

有时为了执行效率等原因,任务的运行需要禁止基于优先级的抢占,这可以通过调用taskLock( )实现。如果任务1调用taskLock( )禁止了高优先级任务对它的抢占,当任务1被阻塞或被暂停,核心将调度下一个具有最高优先级的就绪任务运行。如果这时任务1又就绪且被调度运行,抢占又被禁止。但是,禁止基于优先级的抢占可以阻止任务切换,却并不会屏蔽中断。调用taskUnLock( )可以解除优先级抢占的禁止,通过调用taskLock( )和taskUnLock( )可以实现对临界资源的互斥访问。

任务调度中CPU的占用

如前所述,不同优先级的任务是通过抢占获得CPU使用权的,如果不选时间片轮转,相同优先级的任务之间也是抢占CPU的。任务就绪队列中正在运行的任务如果不主动放弃CPU,则其它同优先级的任务不会得到运行,这样就有可能看到几个同优先级的任务状态同为READY,但实际上只有一个任务在运行的现象。比如在一个任务中用taskSpawn()函数创建一个同优先级或低优先级的任务,如果原任务一直占用CPU,新任务就不会开始运行。调用函数taskDelay()可以使任务放弃CPU一定的时间,从而实现任务间时间上的同步;也可以放弃CPU零时间,将任务移至同优先级就绪队列的末尾,这样就可以实现多个同优先级的任务并发运行。另外,由于中断能够打断任务的运行,中断处理函数中执行的代码就要尽可能少地占用CPU,并且中断中不能有获取信号量的操作。一旦处于等待之中,所有的任务均得不到运行,用户可能会有CPU不响应的错觉。

堆栈越界

如前所述,每一个任务都有自己的堆栈,任务创建时进行初始化。每个堆栈的大小是固定,但是任务运行过程中并不对堆栈的使用进行限制。由于VxWorks不对内存访问作限制,栈顶超越了原定的值后出现越界,这样操作系统中该任务堆栈以外的内存区域就可能被改写,会造成难以预料的结果,甚至可能造成任务的上下文区域被改写而任务消失。造成越界的原因主要是在函数中定义了比较大的数组,以致进栈时越界。这样在编写程序时,就要求在堆栈许可的范围内定义数组。如果确实需要比较大的内存空间,可以使用操作系统的内存分配函数来获得内存。由于堆栈越界后有可能使任务的控制信息被破坏,使得对堆栈越界的检测比较困难,例如可以在栈底写入一串特殊字符,用另外一个任务或者中断服务程序经常来检查是否被改写来判断越界。

CPU异常

在VxWorks中,当任务的指令执行中出现了指令非法、地址寻址错误、总线错、除数为0等情况时,就会出现CPU异常。比较常见的情况是,指针地址非法或者数组下标越界就有可能存取有效地址空间以外的地址而造成CPU异常。VxWorks提供一个异常处理句柄(handler)和一个名为tExcTask的任务来处理异常。异常出现后任务成为挂起状态(suspend),并且不能转变为其它状态。在VxWorks中,有一个异常向量表来对应各种异常,外部中断也作为一种特殊的异常。VxWorks的做法是把多种异常的处理映射到同一个异常处理函数进行处理,并且VxWorks提供了向这个异常处理函数中钩挂用户的异常处理函数的接口excHookAdd(),也可以将某一个异常向量映射到指定的处理函数。