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(),也可以将某一个异常向量映射到指定的处理函数。

任务调试模式下的多任务调试 在任务调试模式下,在一个集成环境中,在一个任务中调试,在另一个任务中设置断点,设置的断点不起作用。这是因为一个调试器只能处理一个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_MsgQ1id == NULL) {
        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的接收队列中。

系统调试模式下程序的调试

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。

在启动目标服务器时,要选择串口通信,并进行相应配置。

系统调试模式下多任务的调试:

调试使用的源代码与任务调试模式中使用的代码相同。但是,需要去掉为了能够在任务调试模式下进行多任务调试的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中的断点处停下。

中断服务程序的调试

中断服务程序只能在系统调试模式下调试,不能在任务调试模式下调试。因为中断服务程序是作为系统的一部分运行,不是以任务方式运行,因此不需要为它产生任务。

中断服务程序调试步骤:

  • 用-g选项编译源代码产生目标文件。
  • 下载产生的目标文件。
  • 在MultiTaskTestInit函数的开始设置断点。
  • 在Debugger命令窗口输入命令attach system进入系统调试模式。
  • 执行菜单命令Debug | Continue继续运行程序。
  • 如果产生相应的中断,程序就会在中断服务程序的断点处停下。进行需要的调试。

原文连接: VxWorks任务编程中常见异常分析