VxWorks任务调度
大家知道,CPU运行的基本单位其实是一条一条的指令,如今我们通过编译器就可以将代码生成为机器指令,也就是所谓的二进制文件,这些指令组成了程序。程序在装入内存中执行时被称之为任务,或者说是进程。随着处理器性能的日益强大,程序也越来越复杂,因此诞生了操作系统来帮助我们管理进程,更合理地分配处理器资源,这也就是任务调度的目标。而在VxWorks中,能够调度起这些任务,最核心的就是reschedule调度算法了。
不过在介绍它之前,还是要先了解下实时系统和分时系统的概念。我们现在接触最多的操作系统,像Windows、Linux这些都是分时操作系统,也就是把允许每个程序运行的时间划分为很短的时间片,按时间片轮流在处理器上运行,但是切换的速度非常快,这样微观上看是各程序轮流运行,宏观上看是进程们在并行工作。
而VxWorks属于实时系统,这也就要求做到及时响应和高可靠性,例如我们更期望一个系统能够始终以50微秒执行一个函数,而不期望系统平均以10微秒执行该函数,但偶尔会以75微秒执行它。这通常需要基于优先级的抢占调度,任务都被指定了优先级,在能够执行的任务(没有被挂起或正在等待资源)中,优先级最高的任务被分配CPU资源。这两种调度方式可以结合使用。
VxWorks默认采用基于优先级的抢占式调度(Priority-based preemptive scheduling)算法,同时也允许选用轮转(Round-robin)调度算法。
Wind内核有256个优先级,编号是0-255,0的优先级最高,255最低。有趣的是,这个优先级机制也并非完美。NASA的火星探路者号火星车就用的是VxWorks,在着陆后的第10天,它就开始无规律地重启,每次重启都造成了数据丢失。原因是当一个高优先级任务和低优先级任务共同需要某个资源时,就可能会导致优先级反转问题。
气象任务(低优先级)获得互斥量并写总线的时候,一个中断的发生导致了通信任务(中优先级)被调度并就绪,调度的时机正好是总线管理任务(高优先级)等待在总线访问互斥量上的时候。这种情况下,因为通信任务比气象任务优先级高,所以会抢占气象任务,当然,这也就更让总线管理任务失去了运行的机会。通信任务运行时间稍长,总线管理任务就会等待互斥量超时,返回错误,提示总线任务没有能够在一定的时间内完成总线操作,在探路者中,这种情况被当作严重错误处理,作为错误处理的结果就是整个系统被重启。
据报道,工程师们后来承认,在飞行前测试阶段就发生过一两起这样的重启,但是相当不易重现所以也没有得到解释,于是出于一种很自然的回避情绪认为这个问题不重要,并且也找到了一个托词——可能是硬件上的小毛病引起的。实际上为了防止这种高优先级任务反而被低优先级卡住的问题,VxWorks提供了优先级继承的手段,让进入临界区的任务临时继承临界区任务中的最高优先级,这时低优先级任务暂时获得了高优先级任务的优先级,系统优先级反转的情况就消失了。
内核调度器的目标是保证优先级最高的就绪任务处于运行状态,任务控制块(TCB)保存了任务的状态,下面几种是基本的。
当一个高优先级的任务由于资源释放变为就绪态,在reschedule调度后,它会立即抢占当前正在运行的较低优先级的任务。
就绪态(Ready):任务只等待系统分配CPU资源
挂起态(Pend):任务需等待某些目前不可利用的资源而被阻塞
休眠态(Sleep):如果某一个任务不需要工作,则这个任务处于休眠状态
延迟态(Delay):任务被延迟时所处的状态
根据任务的状态和类型,会把它们按顺序放到链表队列中,优先级高的任务排在队列前面,相同优先级的任务按照进入队列的先后顺序排列。这样调度的时候直接从队列头中取就好了。
readyQHead:存放就绪的任务,被抢占的任务排在同优先级任务的最前面队列头
tickQHead:存放由于时间而被延迟和休眠的任务队列头
pendQHead:存放由于资源而被挂起的任务队列头
activeQHead:活动队列包括了内核中创建的所有任务,也就包括了上面3种队列中的任务
workQ队列:环形的内核队列,存放内核产生的一些需要进行延时处理的操作
在资源释放后,任务的状态也会相应地改变。因此,进入重新调度的时机主要就是中断处理的执行退出、系统时钟更新(定时器中断)、对任务的操作(taskSpawn/Delete/Delay等)、资源的变化(如信号量semGive/Take、消息队列、内存空间)等。在这些操作触发进入reschedule后,它又干了哪些事呢?
/*******************************************************************************
*
* reschedule - portable version of the scheduler
*
* This routine determines the appropriate task to execute, then calls any
* switch and swap hooks, then loads its context. The complicating factor
* is that the kernel work queue must be checked before leaving. In the
* portable version the checking of the work queue and the loading of the
* task's context is done at interrupt lock out level to avoid races with
* ISRs. An optimized version ought to load as much of the context as is
* possible before locking interrupts and checking the work queue. This will
* reduce interrupt latency considerably.
*
* The _WRS_FUNC_NORETURN attribute is applied to reschedule() so that the
* compiler can dispense with generating the usual preamble that is only
* required for functions that actually return. The reschedule() function
* never returns since windLoadContext() is always used to effect a change in
* execution context. Likewise the windLoadContext() routine is marked as
* _WRS_FUNC_NORETURN so that the compiler can dispense with saving any
* volatile registers before invoking windLoadContext().
*
* \NOMANUAL
*/
_WRS_FUNC_NORETURN void reschedule (void)
{
FAST int ix; /* loop index */
pPrevTcb = _WRS_KERNEL_CPU_GLOBAL_GET (currentCpuIndex, taskIdCurrent);
pCurrentTcb = pPrevTcb;
if (!_WRS_KERNEL_CPU_GLOBAL_GET (currentCpuIndex, workQIsEmpty))
{
unlucky:
workQDoWork ();
}
从代码中可以看到,在对称多处理器核心的情况下,首先它会获取当前核心的上一次重新调度后执行的任务TCB,然后会检查当前内核workQ队列是否有还未执行的任务,如果没有的话就可以开始调度任务了。pCurrentTcb即指示当前正在运行的任务。
pCandidateTcb = READY_Q_TASK_GET (currentCpuIndex, &nextCpu);
if (pCandidateTcb != pCurrentTcb)
从readyQ中取出当前核心的待执行任务,同时将下一个需要调度的核心号放到nextCpu。然后判断当前运行的任务是否就是要执行的任务,如果不是的话就开始了切换流程。
{ pCandidateTcb->windSmpInfo.cpuIndex = currentCpuIndex;
设置将要执行任务的处理器信息为当前核心。
mask = pCandidateTcb->swapInMask | pCurrentTcb->swapOutMask;
for (ix = 0; mask != 0; ix++, mask = mask >> 1)
if (mask & 1)
(* taskSwapTable[ix]) (pCurrentTcb, pCandidateTcb);
然后进行任务的swap交换操作。此Hook提供给内核使用。
for (ix = 0;
(ix < VX_MAX_TASK_SWITCH_RTNS) && (taskSwitchTable[ix] != NULL);
++ix)
(* taskSwitchTable[ix]) (pCurrentTcb, pCandidateTcb);
进行任务的switch切换操作。具体Hook函数由用户任务自己指定。这是为了让用户在切换过程中能够执行一些自定义的逻辑,而无需修改内核代码。
pCurrentTcb->windSmpInfo.cpuIndex = NONE;
_WRS_KERNEL_CPU_GLOBAL_SET (currentCpuIndex, taskIdCurrent,
pCandidateTcb);
pCurrentTcb->windSmpInfo.stateRequest &= ~WIND_STATE_CHANGE;
pCurrentTcb = pCandidateTcb;
}
设置一些全局变量和状态标志,并确保pCurrentTcb始终指向切换后的任务。如果不切换的话那当前核自然不需要再进行上面的调度,这时nextCpu就派上用场了。
if (nextCpu >= 0)
{
CPUSET_ZERO (cpuSet);
CPUSET_SET (cpuSet, nextCpu);
vxIpiEmit (IPI_INTR_ID_SCHED, cpuSet);
nextCpu = SCHED_REQ_DONE;
}
在多核的环境下,为了维持时间的统一,减小不必要的开支,硬件定时器的中断往往只会由最先运行的主核进行响应,也就是说,其他核上其实是没有时间观念的,那么这些核上的任务在延时之后该怎么切换回来呢?这时就需要主核根据下一个CPU,向对应的核心发送核间中断,其他核心收到中断后,就会引起那个核的reschedule过程,这样每个核心就可以自己调度核上的任务了。
oldLevel = INT_CPU_LOCK ();
if (!_WRS_KERNEL_CPU_GLOBAL_GET (currentCpuIndex, workQIsEmpty))
{
INT_CPU_UNLOCK (oldLevel);
goto unlucky; /* take it from the top... */
}
最后一次检查workQ工作队列。为什么在reschedule()的末尾还要再次判断内核队列是否为空,如果非空重新执行调度器呢?这是因为,在执行内核切换和交换钩子函数期间,有可能会有更高优先级的任务刚好需要被唤醒,由于此时Wind内核正在被reschedule调度器互斥访问(即Wind内核处于内核态,kernelState=TRUE),将这些任务转为就绪态的工作都会被放到内核队列延迟运行。执行内核队列中的任务会使得更高优先级的任务可以进入就绪态,需要重新提供一次调度的机会,这样确保了只有优先级最高的任务占据CPU。
例如,某个高优先级任务被信号量semTake住。由于该任务处在pend队列中,刚才的调度选择的是ready队列中的某个中优先级任务。而就在同一时间,定时器中断触发,该信号量被释放,在semGive中,它先会判断当前处在中断上下文或内核锁不可用,信号量将被延迟释放,在semGiveDefer中将该类型信号量的Give方法加入到了workQ队列中。这时重新执行该workQ任务,就能让这个高优先级任务重回ready队列,赶上这次的调度。
_WRS_KERNEL_CPU_GLOBAL_SET (currentCpuIndex, reschedMode,WIND_NO_RESCHEDULE);
从这里开始,新任务已经被确定,再也没有什么能阻止任务上下文的加载了。顺便清除下CPU的重新调度标志。
KERNEL_LOCK_GIVE ();
windLoadContext ();
}
现在,加载新的任务上下文并开始执行它。
为了能更直观地了解这一过程,我们可以在SkyEye软件中对该函数过程进行一步一步地调试,这样就能真正理解每一步的作用。由迪捷软件自主开发的SkyEye全数字实时仿真软件(点击查看详情),是基于可视化建模的硬件行为级仿真平台。利用拖拽的方式快速搭建任意的虚拟硬件平台,保证虚拟嵌入式系统的可靠性和实时性,进行嵌入式软件的开发和调试。对标产品为美国风河公司的Simics。
SkyEye支持ARM、PowerPC、MIPS、DSP、SPARC等多种架构的处理器,以及定时器、中断控制器、串口、存储器、显示屏等多种硬件外设,能够运行Linux、VxWorks、FreeRTOS、RTEMS、ReWorks、SylixOS等多种嵌入式操作系统。它能够进行源代码和汇编指令级的调试,支持函数调用栈、多处理器、寄存器、变量、内存等的调试,是你在嵌入式学习道路上或业务中提高生产力的不二选择。