源码请在https://github.com/ifreecoding/MbedRtos.git下载
第3节 实时事件触发的实时抢占调度
在上节中我们成功的实现了任务的ready状态,并用tick中断实现了实时调度,但由于只有ready这一种状态,使得我们的例子只能不断的运行最高优先级任务。在这一节我们引入任务的另一个状态——delay状态,当前运行的任务可以通过调用MDS_TaskDelay函数进入delay状态,将CPU控制权交给其它任务,经过一段时间后,它可以再恢复为ready状态,重新参与任务调度。
本小节将使用ready状态和delay状态在多个任务之间按任务优先级实现任务交替运行,输出多个任务的打印结果。
ready表关联了处于ready状态的任务,处于delay状态的任务也需要使用一个delay表来关联,ready表需要使用任务优先级这个属性来调度其中的各个任务,而delay表则是以需要delay的时间这个属性来调度其中任务的,按照需要delay时间的长短,从短到长,将处于delay状态的各个任务节点挂接到delay表的根节点上。
delay表相对ready表来说要简单很多,它只有一个根节点。delay表初始化时,根节点被初始化为空。向delay表添加节点时,先从根节点找到第一个子节点,若该子节点剩余的delay时间小于等于需要新添加节点的delay时间,则继续查找下一个节点,直到找到节点的剩余delay时间大于新添加节点的delay时间时,将新节点添加到这个节点的前面,若链表中所有节点剩余的delay时间都小于等于新添加节点的delay时间,则将新添加的节点挂到delay表的最后。由于delay表是按照delay时间从短到长的顺序排列的,当从delay表拆除节点时,只需要从delay表的第一个节点开始向后找,找到最后一个剩余的delay时间不为0的节点为止,将这些剩余的delay时间为0的节点全部从delay表中拆除,挂入到ready表即可。
需要永久delay的任务不挂入delay表,因为它与时间不相关,不需要参与tick调度。
任务delay的时间是以tick为单位的,每次tick中断产生时,在函数MDS_TaskTick里会对guiTick全局变量做加1操作,操作系统就是以这个guiTick变量作为操作系统的系统时钟的。
任务调用MDS_TaskDelay(uiDelayTick)函数时,并不会真正延迟uiDelayTick个ticks的时间,这是因为任务是在2个ticks之间调用MDS_TaskDelay函数的,调用MDS_TaskDelay函数的时刻到下次tick中断的时刻之间的时间是不够1个tick时间的,因此,任务delay的全部时间只有(uiDelayTick-1)~uiDelayTick个ticks时间。如果你希望至少延迟N个ticks的话,那么就需要将MDS_TaskDelay的参数设置为(N+1),如果delay的时间为0,那么该任务并不会进入delay状态,操作系统只是将ready表中的任务重新调度了一次而已。
在图42中,任务调用MDS_TaskDelay函数时从running态转变为了delay态,那么操作系统就需要从ready表中找到一个最高优先级的任务将它转换成running态,但任务调度的过程是在tick中断中发生的,而此时并没有发生tick中断,因此,我们就需要增加一种新的任务调度触发方式,增加实时事件触发的任务调度。
在Mindows中,我们使用软中断来实现这个功能,在tick中断之外出现的实时事件需要触发任务调度时,它会触发软中断,继而运行软中断服务函数,在软中断服务函数里做一个任务调度,软中断服务函数里面所做的调度工作与tick中断服务函数里所做的调度工作非常相似。
处于delay状态的任务除了可以通过将需要delay的时间耗尽,从delay状态返回到ready状态,还可以通过其它任务调用MDS_TaskWake函数来唤醒该任务,使处于delay状态的任务立刻变为ready态,参与到ready表的任务调度中。
在前面章节的测试函数中,我们所使用的DEV_DelayMs函数会连续运行几秒钟时间,在这几秒时间内任务一直处于running态,一直占有CPU,用来模拟当前任务正在运行的业务。但在实际的项目中,这种情况是不可以存在的,每个任务连续运行的时间不会很长,每运行若干ms甚至us的时间就会让出CPU资源,由其它任务继续运行。
在一段时间内,与整个系统功能相关的指令运行所花费的时间,与这段时间的比值叫做CPU占有率,从这个定义可以看出,CPU占有率越高CPU剩下的可使用的处理能力越少。实时操作系统实时抢占的特点是具有随机性的,我们不知道什么时候就会有一个更高优先级的任务抢占了当前正在执行的任务,如果此时CPU占有率过高,那么突发的一些较高优先级任务就会使CPU占有率接近100%,而此时这些较高优先级任务中的一些任务则可能会由于得不到CPU资源而无法运行,这样系统就会丧失一些重要功能,从而影响整个系统的功能。当我们设计产品软硬件系统时,需要根据产品的功能、性能选择合适的处理器,在空闲状态时,要保证CPU占有率处于较低的水平,留有足够的CPU余量以备满业务负载或突发事件使用。
在系统运行过程中,很可能会出现所有的任务同时处于delay状态的情况,没有ready态的任务,那么操作系统也就无法从ready表中找出一个可以转换为running态的任务去执行,那么,这时候CPU应该执行什么代码呢?Mindows操作系统在初始化时,除了会创建root根任务,还会创建一个idle空闲任务,当所有其它任务都不处于ready态时,操作系统就运行idle任务。CPU占有率也可以认为是一段时间内非空闲任务运行的时间与这段时间的比值,运行idle任务时CPU虽然也是在执行指令,但这些指令是不算在CPU占有率中的,因此在idle任务中不要放入具有业务功能的代码,并且需要保证idle任务具有最低的优先级,使它不能影响其它任务的执行。在Mindows中,将最低优先级保留给idle任务,其它任务不能使用该优先级。root任务是系统最先运行的任务,它应该先于其它任务运行完毕,因此Mindows将最高优先级保留给root任务,其它任务不能使用该优先级。
idle任务不允许处于delay状态,如果idle任务也被delay了,那么操作系统就真的不知道该做什么了。Mindows对MDS_TaskDelay函数做了判断,如果用户在idle任务里误调用了MDS_TaskDelay则什么情况也不会发生,相当于是调用了空函数,这样可以防止操作系统崩溃。idle任务永远处于ready状态。
Mindows内核中有一些函数不可重入,在使用这些函数前需要先锁中断,使用之后再解锁中断,这样就可以防止多个任务并行运行这些函数发生重入。上节中的锁中断函数MDS_IntLock和解锁中断函数MDS_IntUnlock只针对tick中断做了处理,只能阻止tick中断产生的重入现象,却不能阻止在其它中断中调用这些函数产生的函数重入。因此从本节开始,以后的锁、解锁中断都修改为对所有中断的锁和解锁。
程序在锁中断、解锁中断时会发生嵌套的现象,看下面的例子:
00001 MDS_IntLock
00002 ……
00003 MDS_IntLock
00004 ……
00005 MDS_IntUnlock
00006 ……
00007 MDS_IntUnlock
1行和7行的锁中断和解锁中断是一对的,其目的是为了保护2~6行的代码。3行和5行的锁中断和解锁中断是一对的,其目的是为了保护4行的代码。但这样带来一个问题,在1行和3行连续锁了2次中断,4行的代码时已经处于锁中断状态,这是符合设计要求的,但在5行时解锁了一次中断,那么6行的代码就无法得到锁中断的保护了,这与设计是不符的。为了解决这个问题,在这两个函数里面做了计数统计,只有在未锁中断的状态下调用锁中断函数MDS_IntLock才会真正的去操作硬件寄存器,执行锁中断操作,在已锁中断的状态下调用锁中断函数MDS_IntLock只会增加其内部的变量计数,不对硬件寄存器做任何操作,做一个虚假的锁中断操作。同理,只有在已锁中断的状态下,并且变量计数为1的情况下调用解锁中断函数MDS_IntUnlock才会真正的去操作硬件寄存器,执行解锁中断操作,在其它情况下调用解锁中断函数MDS_IntLock只会减少其内部的变量计数,不对硬件做任何操作。
函数内部操作 | 函数内部变量值 | |
初始状态 | 0 | |
MDS_IntLock | 操作硬件寄存器,锁中断。变量加1。 | 1 |
MDS_IntLock | 变量加1。 | 2 |
MDS_IntUnlock | 变量减1。 | 1 |
MDS_IntLock | 变量加1。 | 2 |
MDS_IntUnlock | 变量减1。 | 1 |
MDS_IntUnlock | 操作硬件寄存器,解锁中断。变量减1。 | 0 |
锁中断函数MDS_IntLock和解锁中断函数MDS_IntUnlock需要成对使用,这里所说的成对,不是代码编写上的成对,而是代码运行时的成对。
本节增加了delay表,我们需要再改造一下TCB结构,为任务设计一个挂接到delay表的链表节点结构,并且需要增加一个变量用来保存任务delay的时间,来看看新的TCB结构:
typedef struct m_tcb
{
STACKREG strStackReg;
M_TCBQUE strTcbQue;
M_TCBQUE strDelayQue;
U32 uiTaskFlag;
U8 ucTaskPrio;
M_TASKOPT strTaskOpt;
U32 uiStillTick;
}M_TCB;
strDelayQue是任务挂入delay链表的队列结构。
任务是否在delay链表中的标志存放在uiTaskFlag变量中,uiTaskFlag变量在后面的章节中还会扩展,增加其它的标志。
TCB中还有一个M_TASKOPT结构,M_TASKOPT结构如下:
typedef struct m_taskopt
{
U8 ucTaskSta;
U32 uiDelayTick;
}M_TASKOPT;
从本节开始,在创建任务时可以由用户指定任务创建时的状态。创建任务时,用户将任务的状态存入M_TASKOPT结构体中的ucTaskSta变量中,若是delay状态,则还需要将delay的tick数值存入uiDelayTick变量中,创建任务时将装有初始化数据的M_TASKOPT结构的变量的指针作为入口参数传递给MDS_TaskCreate函数,就可以指定任务创建时的状态了。
TCB中的uiStillTick变量存放的是任务delay状态耗尽时的tick值,比如当前的tick是100,任务需要delay 5个ticks,那么该变量里保存的就是105,表明该任务的delay态持续到105 ticks。任务调度时就是根据该变量判断处于delay状态的任务是否需要转换为ready状态。
上面介绍了本小节新增的主要内容,比较零散,在介绍代码前,我们将这些内容串起来,梳理一下操作系统运行的过程。
整个系统上电后,在MDS_SystemVarInit函数里初始化操作系统的变量,然后创建最高优先级任务MDS_RootTask,创建最低优先级任务MDS_IdleTask,使用MDS_TaskStart函数从非操作系统状态切换到操作系统状态,开始运行MDS_RootTask任务。用户需要在MDS_RootTask任务里编写用户代码,对用户代码初始化,并初始化单板硬件,包括tick中断,创建用户任务。MDS_RootTask任务具有最高优先级,为了让用户任务能得以运行,本节在MDS_RootTask任务最后调用MDS_TaskDelay函数,并使用DELAYWAITFEV参数,使MDS_RootTask任务永远处于delay状态。
当用户完成tick中断初始化之后,Mindows的调度就正式开始了,此后每个tick周期就发生一次任务调度。若在MDS_RootTask任务运行过程中发生了tick调度,则由于MDS_RootTask任务具有最高优先级,tick调度后还会继续运行MDS_RootTask任务。用户在MDS_RootTask任务中创建的任务可能处于ready态或者delay态,在MDS_RootTask任务处于delay态之后,用户任务就会在tick中断中开始调度,tick中断调度发生时先查找delay表,从delay表中拆除delay时间耗尽的任务,将它们添加到ready表中,然后再在ready表中查找最高优先级的任务,切换到这个最高优先级任务继续运行。这个调度过程每个tick调度周期就会发生一次,由定时器周期触发。在任务运行过程中,如果调用了MDS_TaskDelay函数,任务就会进入delay态,被从ready表拆除,若不是永久delay则挂入delay表。MDS_TaskDelay函数最后会调用软中断调度函数MDS_TaskSwiSched产生一次软中断调度,在软中断调度中不需要调度delay表,直接从ready表中找出最高优先级任务,切换到这个最高优先级任务继续运行,这个调度过程由实时事件触发,不定周期。Mindows的调度方式就由这2种调度方式构成。在调度过程中,除idle任务之外的所有任务若都处于delay状态,操作系统则运行idle任务,当其它任务重新恢复到ready态时,它们就会抢占idle任务继续运行。
原理性的知识介绍完了,我们来看一下代码,先来看MDS_TaskCreate函数做了哪些改动:
00016 M_TCB* MDS_TaskCreate(VFUNC vfFuncPointer, U8* pucTaskStack, U32 uiStackSize,
00017 U8 ucTaskPrio, M_TASKOPT* pstrTaskOpt)
00018 {
00019 M_TCB* pstrTcb;
00020
00021
00022
00023
00024 if(NULL == vfFuncPointer)
00025 {
00026
00027 return (M_TCB*)NULL;
00028 }
00029
00030
00031 if((NULL == pucTaskStack) || (0 == uiStackSize))
00032 {
00033
00034 return (M_TCB*)NULL;
00035 }
00036
00037
00038 if(NULL != pstrTaskOpt)
00039 {
00040
00041 if(!((TASKREADY == pstrTaskOpt->ucTaskSta)
00042 || (TASKDELAY == pstrTaskOpt->ucTaskSta)))
00043 {
00044 return (M_TCB*)NULL;
00045 }
00046 }
00047
00048
00049 if(ucTaskPrio > LOWESTPRIO)
00050 {
00051 return (M_TCB*)NULL;
00052 }
00053
00054
00055 if((MDS_RootTask != vfFuncPointer) && (MDS_IdleTask != vfFuncPointer))
00056 {
00057
00058 if((HIGHESTPRIO == ucTaskPrio) || (ucTaskPrio >= LOWESTPRIO))
00059 {
00060 return (M_TCB*)NULL;
00061 }
00062 }
00063
00064
00065 if(MDS_RootTask == vfFuncPointer)
00066 {
00067
00068 if(NULL != gpstrRootTaskTcb)
00069 {
00070 return (M_TCB*)NULL;
00071 }
00072 }
00073
00074
00075 if(MDS_IdleTask == vfFuncPointer)
00076 {
00077
00078 if(NULL != gpstrIdleTaskTcb)
00079 {
00080 return (M_TCB*)NULL;
00081 }
00082 }
00083
00084
00085 pstrTcb = MDS_TaskTcbInit(vfFuncPointer, pucTaskStack, uiStackSize, ucTaskPrio,
00086 pstrTaskOpt);
00087
00088
00089 if((MDS_RootTask != vfFuncPointer) && (MDS_IdleTask != vfFuncPointer))
00090 {
00091
00092 MDS_TaskSwiSched();
00093 }
00094
00095 return pstrTcb;
00096 }
00017行,新增入口参数pstrTaskOpt指针,通过pstrTaskOpt指针可以配置任务刚建立时的状态。若没有使用pstrTaskOpt参数,任务则被默认为ready态,若使用了pstrTaskOpt参数,其中的ucTaskSta变量中保存的是任务创建时的状态,uiDelayTick变量中保存的是delay状态需要延迟的时间。
00038~00046行,对pstrTaskOpt入口参数进行检查,若使用了此参数,则只能配置任务的初始状态为ready或者delay状态,否则返回失败。
00048~00062行,对任务优先级进行检查,新创建任务的优先级不能低于任务最低优先级,若不是系统任务则不能使用最高和最低优先级。
00074~00082行,对是否重复创建idle任务进行检查。
00089~00093行,若创建非系统任务,则使用软中断调度一次任务,使新建立的这个任务也立刻参与到任务调度中。
MDS_TaskTcbInit函数也增加了pstrTaskOpt入口参数,做了一些修改:
00108 M_TCB* MDS_TaskTcbInit(VFUNC vfFuncPointer, U8* pucTaskStack, U32 uiStackSize,
00109 U8 ucTaskPrio, M_TASKOPT* pstrTaskOpt)
00110 {
00111 M_TCB* pstrTcb;
00112 M_CHAIN* pstrChain;
00113 M_CHAIN* pstrNode;
00114 M_PRIOFLAG* pstrPrioFlag;
00115 U8* pucStackBy4;
00116
00117
00118 pucStackBy4 = (U8*)(((U32)pucTaskStack + uiStackSize) & 0xFFFFFFFC);
00119
00120
00121 pstrTcb = (M_TCB*)(((U32)pucStackBy4 - sizeof(M_TCB)) & 0xFFFFFFFC);
00122
00123
00124 MDS_TaskStackInit(pstrTcb, vfFuncPointer);
00125
00126
00127 pstrTcb->uiTaskFlag = 0;
00128
00129
00130 pstrTcb->strTcbQue.pstrTcb = pstrTcb;
00131 pstrTcb->strDelayQue.pstrTcb = pstrTcb;
00132
00133
00134 pstrTcb->ucTaskPrio = ucTaskPrio;
00135
00136
00137 if(NULL == pstrTaskOpt)
00138 {
00139 pstrTcb->strTaskOpt.ucTaskSta = TASKREADY;
00140 }
00141 else
00142 {
00143 pstrTcb->strTaskOpt.ucTaskSta = pstrTaskOpt->ucTaskSta;
00144 pstrTcb->strTaskOpt.uiDelayTick = pstrTaskOpt->uiDelayTick;
00145 }
00146
00147
00148 (void)MDS_IntLock();
00149
00150
00151 if(TASKREADY == (TASKREADY & pstrTcb->strTaskOpt.ucTaskSta))
00152 {
00153 pstrChain = &gstrReadyTab.astrChain[ucTaskPrio];
00154 pstrNode = &pstrTcb->strTcbQue.strQueHead;
00155 pstrPrioFlag = &gstrReadyTab.strFlag;
00156
00157
00158 MDS_TaskAddToSchedTab(pstrChain, pstrNode, pstrPrioFlag, ucTaskPrio);
00159 }
00160
00161
00162 if(TASKDELAY == (TASKDELAY & pstrTcb->strTaskOpt.ucTaskSta))
00163 {
00164
00165 if(DELAYWAITFEV != pstrTaskOpt->uiDelayTick)
00166 {
00167
00168 pstrTcb->uiStillTick = guiTick + pstrTaskOpt->uiDelayTick;
00169
00170
00171 pstrNode = &pstrTcb->strDelayQue.strQueHead;
00172 MDS_TaskAddToDelayTab(pstrNode);
00173
00174
00175 pstrTcb->uiTaskFlag |= DELAYQUEFLAG;
00176 }
00177 }
00178
00179
00180 (void)MDS_IntUnlock();
00181
00182 return pstrTcb;
00183 }
00127行,初始化任务标志为空,即没有任何标志的状态,后续需要改变标志状态时再处理。
00131行,初始化strDelayQue结构中的TCB指针。
00137~00140行,创建任务时若没有使用pstrTaskOpt任务参数,则创建的任务默认为ready态。
00142~00145行,使用了pstrTaskOpt任务参数,将任务参数复制到TCB中。
00162行,判断新创建的任务是否为delay状态。
00165行,判断新创建的任务是否为非永久delay状态。
00167行,对于非永久delay任务,将需要delay的tick数值换算为delay时间耗尽时的tick数值。
00171行,获取任务TCB中可挂入delay链表的节点。
00172行,将任务挂入到delay链表。
00175行,在任务标志uiTaskFlag中设置任务已挂入到delay链表的标志。
MDS_TaskAddToDelayTab函数的功能是将任务节点添加到delay表中,添加的时候是按照任务剩余时delay时间从少到多排序的,这个函数最关键的部分在于为新加入的节点找到合适的节点位置,需要使用新加入节点的delay耗尽tick数值依次与delay表中节点的delay耗尽tick数值以及当前的tick数值做比较。当前tick变量guiTick会从0开始递增,当到达最大值232-1时又会重新回到0,形成一个回环的计数过程,因此,这3个需要比较的数值存在多种组合情况,情况比较复杂,MDS_TaskAddToDelayTab函数的细节不再详细介绍了。