2.2 VxWorks任务调度算法——基于优先级的抢占式调度

VxWorks是实时操作系统,进程调度时机对其而言至关重要。基本上所有的操作系统的进程调度时机都选择在同一个地方:从内核状态退出之时。从内核状态退出时主要有两个渠道:系统调用和中断。外部中断可有多个源,一般操作系统只是使用系统时钟中断作为固定的进程调度时刻点,而其他中断源以及系统调用由于其执行时机的不确定性,只是作为进程调度触发的辅助手段。实时操作系统与通用操作系统区别的关键点在于前者必须能够迅速地响应外部中断,所以,实时操作系统只是在响应速度上更快,而在响应时机上与通用操作系统并无区别。中断响应速度即中断产生到对应中断处理程序的调用之间的延迟时间。实时操作系统将这个延迟尽量减少到最小。

一个理解的误区是系统时钟频率对实时性起决定性影响,事实并非如此,例如,VxWorks系统时钟间隔1ms量级,而现在绝大多数通用操作系统的系统时钟间隔也是在1ms量级,并非时钟间隔越小越好。当时钟间隔过小时(如间隔为0.5ms时),操作系统将花费大量的时间用于处理进程的调度,这样会严重影响整个系统的工作性能。注意:系统时钟是进程调度的脉搏。

另一个误区是将系统时钟与CPU主频混为一谈。CPU主频是指CPU内硬件单元处理速度,一般以每秒内执行的指令数为指标参数。CPU主频越高,系统的实时性越好,因为可以在更快的时间内执行完过渡指令,从而使得中断响应的时间更少。所以,系统实时性一方面由操作系统决定,另一方面由操作系统运行的平台决定,或者更进一步,由平台上的CPU主频决定。

VxWorks操作系统与通用操作系统区别的一个显著特点是其不划分运行态的级别。应用层程序可以直接调用内核函数,而不需要通过软中断的方式从应用层进入到内核层。故VxWorks下系统调用的概念与通用操作系统(如Linux)有本质上的差别。下面的代码分别演示了在VxWorks和Linux下一个应用层函数(userRtn)是如何调用一个内核层函数(kernelRtn)的。

VxWorks下系统调用:

        void userRtn(){
              int param;
              … //prepare param
              //直接调用kernelRtn函数
              kernelRtn(param);
        }

Linux下系统调用(_NR_kernelRtn表示kernelRtn对应的系统调用号):

        void userRtn(){
              long _res;
              int param;
              … //prepare param
              //使用软中断进入内核态,进行kernelRtn函数的调用(以Intel x86体系为例)
              _asm_ volatile
        (
        "int $0x80" \
              :"=a"(_res) \
              :"0"(_NR_kernelRtn),"b"(param) \
        );
        }

VxWorks下所谓的内核态仅仅由一个内核布尔变量kernelState表示。当kernelState设置为TRUE时,表示此时代码运行在内核状态。VxWorks内核态的本质即保护内核数据结构,防止多处代码对内核数据结构同时进行访问,所以它不同于通用操作系统内核态的概念。通用操作系统内核态是为了保护内核数据结构不受应用层的影响。所以,VxWorks内核态更多的是一个信号量的概念。如下代码显示了kernelState的作用,从而可以看出内核态的基本含义:防止多处代码对内核数据结构的同时操作。

        if (kernelState)                                  /@ defer work if in kernel @/
        {
              workQAdd1 (windResume, tid);                /@ add work to kernel work q @/
              return (OK);
        }
        kernelState = TRUE;                               /@ KERNEL ENTER @/
        …                                                 /@ OPERATE ON KERNEL STRUCTRUES @/
        windExit ();                                      /@ KERNEL EXIT @/

当代码需要对内核数据结构进行操作时,其首先检查kernelState的状态,如果kernelState设置为TRUE,则表示当前已经有代码正在操作内核数据结构,故将当前代码要做的工作加入到内核工作队列中,稍候再完成。

可以说,内核工作队列以及任务队列构成了VxWorks内核的架构,基本上内核代码都是对各种队列的操作,所以,上文中所说的VxWorks内核数据结构主要就是指这些内核队列。VxWorks内核态的进入通过简单地设置kernelState变量值为TRUE完成,而退出内核态则是由windExit函数完成,该函数除了将kernelState设置为FALSE之外,完成的另外两个重要工作就是运行内核工作队列中延迟的工作以及进程调度。进程调度函数(reschedule)在windExit中被调用用以调度更高优先级的进程运行。而windExit函数被调用的时机则是从内核态退出,而中断和系统调用则是进入内核态的唯一手段,所以从这个意义上说,VxWorks进程调度的时机即在系统调用和中断发生之时。

更进一步,中断一定会引起进程调度,系统调用在绝大多数情况下也会引起进程调度。系统调用并不总是能够引起进程调度,因为可能调用的内核函数较为简单,不需要对内核共享数据结构进行操作,此时就不会执行windExit函数。

中断可以分为时钟中断和其他硬件中断。时钟中断即以固定时间间隔产生中断,这个中断专门用于系统定时器和进程调度。任务状态的改变主要由时钟中断触发。其他硬件中断则是嵌入式系统的关键组成部分,用以控制特定设备。对这些硬件中断的实时响应可以说是VxWorks嵌入式系统的主要功能。一个嵌入式系统可以没有任务(当然除了Idle任务外,这是VxWorks操作系统自带的后台进程),但是不可以没有硬件中断。

下面以ARM处理器为例详细介绍时钟中断的注册和执行流程,这个中断是系统脉搏,了解它对于整体掌握VxWorks操作系统将有很大帮助,这对于在嵌入式平台下实现一些有意义的策略也很有启示。在后文“中断处理”一节中,我们还将对这些内容进行更详细的讲解。

时钟中断的注册在VxWorks操作系统初始化过程中完成,这主要由在userRoot中调用的如下三个函数完成。

        sysClkConnect ((FUNCPTR) usrClock, 0);  /* connect clock ISR */
        sysClkRateSet (SYS_CLK_RATE);  /* set system clock rate */
        sysClkEnable ();

其中,sysClkConnect完成时钟中断程序的注册。事实上,传递给sysClkConnect函数的userClock函数并非是直接中断响应函数。ARM处理器支持两类中断:普通中断irq和快速中断fiq。VxWorks操作系统只使用了irq。

对于一个嵌入式平台而言,单个中断源显然不够用,故实际上各种中断都是以irq的形式存在的。VxWorks内核维护一个irq中断的总入口中断处理程序,再由这个总入口中断处理函数根据中断向量号进一步调用对应中断处理程序。

时钟中断向量号一般设置为0。时钟中断对应的中断响应函数为sysClkInt,这是在sysHwInit2 函数中完成注册的。事实上,sysHwInit2 是在sysClkConnect中被调用的。sysClkConnect调用sysHwInit2注册sysClkInt作为时钟中断响应函数,基本代码如下:

        void sysHwInit2(){
              …
              (void)intConnect ( INUM_TO_IVEC(INT_TINT0), sysClkInt, 0);
              intEnable ( INT_TINT0 );
              …
        }

而后,sysClkConnect将userClock(sysClkConnect的参数)作为二次函数调用注册到VxWorks内核中。所谓二次函数调用,即当发生一个时钟中断时,sysClkInt作为时钟中断响应函数将首先被调用,而sysClkInt又进一步调用userClock函数。userClock函数定义在userConfig.c中,其代码如下:

        void usrClock ()
        {
              tickAnnounce (); /* announce system tick to kernel */
        }

从以上代码可以看出,userClock直接调用tickAnnounce函数作为响应。

tickAnnounce函数主要完成如下工作:

① 对vxTick变量做加1运算。vxTick表示系统自启动之时到现在的tick数,所以用vxTick乘以系统时钟间隔就是开机时间。

② 对处于等待状态的任务进行检查,将超时任务(那些调用taskDelay延迟的任务)重新设置为ready状态,并转移到调度队列中。

③ 遍历内核工作队列,对延迟的内核工作进行执行。

④ 进程调度。选择最高优先级任务作为当前任务运行。

时钟中断注册过程总结如下:

        userConfig.c:userRoot()→arm_timer.c:sysClkConnect()→sysLib.c:sysHwInit2()

其中,arm_timer.c为ARM平台BSP中的定时器驱动文件。如前文所述,sysClkConnect函数调用sysHwInit2,将sysClkInt函数注册为时钟中断处理函数,同时将作为参数传入的userClock函数作为二次函数调用注册到VxWorks内核中。(注:二次函数调用即被sysClkInt调用执行。)

时钟中断执行流程总结为:时钟中断产生→VxWorks内核IRQ总入口中断处理函数→时钟中断处理函数(sysClkInt)→userClock函数→tickAnnounce函数。