浅析FreeRTOS_v4.5.0的任务切换原理和栈结构
文章来源:http://gliethttp.cublog.cn[转载请声明出处]
FreeRTOS的更新速度很快,基本2、3个月就会出现一个新版本,看来一直在完善和各种功能增加中,截止 2007/09/27日为止,FreeRTOS_v4.5.0是最新版本,下面研究一下FreeRTOS_v4.5.0在at91sam7s64处理器 上的任务切换代码. 当有更高优先级的task任务就绪或者当前task任务因为vTaskDelay()延时或者消息事件等待而主动让出cpu 时,FreeRTOS_v4.5.0会在适当的位置执行taskYIELD();进行task进程调度,让更应该持有cpu的task获得 持有权. 1.触发软中断执行处理器对应的swi处理函数 //在/source/include/Task.h中有如下定义: #define taskENTER_CRITICAL() portENTER_CRITICAL() //在/source/portable/iar/atmelsam7s64/Portmacro.h中有如下定义: //只是借壳下蛋,中断号0,其值本身没有意义,因为swi处理函数中,并不会使用到中断号值, //这和linux的系统调用有些出入,linux的系统调用,中断号值对应了相应系统调用处理函数 //的索引值,可以参看《浅析arm-linux系统调用的流程 》 //文章地址:http://blog.chinaunix.net/u1/38994/showart_331915.html #define portYIELD() asm ( "SWI 0" ) 因此可以看出来FreeRTOS_v4.5.0使用软中断触发task的调度函数,来看看软中断入口函数: //在/demo/arm7_at91sma7s64_iar/srciar/Cstratup.s79中有中断向量处理代码: B InitReset ; 0x00 Reset handler undefvec: B undefvec ; 0x04 Undefined Instruction swivec://很明显portYIELD()调用之后将直接导致cpu去进一步执行vPortYieldProcessor B vPortYieldProcessor ; 0x08 Software Interrupt pabtvec: B pabtvec ; 0x0C Prefetch Abort dabtvec: B dabtvec ; 0x10 Data Abort rsvdvec: B rsvdvec ; 0x14 reserved irqvec: LDR PC, [PC, #-0xF20] ; Jump directly to the address given by the AIC //在/source/portable/iar/atmelsam7s64/Portasm.s79中定义了vPortYieldProcessor处理函数 vPortYieldProcessor: //lr+4是为了调整lr的值,使其和IRQ中断进入的模式值一致 ADD LR, LR, #4 //接下来就可以安全的认为是由于IRQ中断引起的任务进、出栈(gliethttp) portSAVE_CONTEXT//保存上、下文 LDR R0, =vTaskSwitchContext mov lr, pc BX R0//计算出优先级最高的任务 //恢复vTaskSwitchContext函数计算出的优先级最高任务的上、下文,进而在cpu中运行优先级最高任务. portRESTORE_CONTEXT 2.FreeRTOS_v4.5.0在at91sam7s64上的任务切换出、入栈汇编代码分析 ///source/portable/iar/atmelsam7s64/ISR_Support.h中定义了这两个汇编子函数 <2.1>portSAVE_CONTEXT入栈宏 在进行分析下面这段代码之前,首先必须清楚FreeRTOS_v4.5.0在at91sam7s64上使用的模式情况 首先,上电之后sam64工作在svc模式,当完成所有sam64自身的初始化工作以及xTaskCreate()之后, 仍然处于svc模式,只有当调用vTaskStartScheduler()->xPortStartScheduler()->vPortStartFirstTask() 之后sam64就随着portRESTORE_CONTEXT()的执行使得task进入sys模式下执行了, 所以其实最后任务将在sys模式下执行,这也是pxPortInitialiseStack()所定义的spsr的模式值 portSAVE_CONTEXT MACRO STMDB SP!,{R0}//R0推入swi专用栈 STMDB SP, {SP}^//将sys模式下的sp值推入当前swi模式下的sp中 NOP SUB SP, SP, #4//swi堆栈调整到执行STMDB SP!, {SP}^之后的值 LDMIA SP!,{R0}//把swi栈中存放的sys模式下的sp值转存到r0中 STMDB R0!,{LR}//将swi模式中的lr推入sys模式下的堆栈(gliethttp) MOV LR, R0//将sys模式下的堆栈值转用lr临时存储 LDMIA SP!,{R0}//现在sys模式下的sp堆栈已经使用swi下的lr代替,所以r0已经不再被使用, //swi模式下的sp已经释放,r0恢复到了sys模式下的r0值 STMDB LR, {R0-LR}^//把sys下的r0-lr的数据推入sys模式下的sp堆栈中 NOP SUB LR, LR, #60//调整lr,使其等于sys模式下的sp堆栈位置 MRS R0, SPSR//r0已经入栈,所以可以使用;spsr中含有IRQ和FIQ的中断时能否标志以及sys模式标志 STMDB LR!,{R0}//将spsr入栈 LDR R0, =ulCriticalNesting LDR R0, [R0] STMDB LR!,{R0} //将ulCriticalNesting入栈 LDR R1, =pxCurrentTCB LDR R0, [R1] STR LR, [R0]//pxCurrentTCB为task进程的上下文存储单元,其中第一项为pxTopOfStack本task的栈顶值 ENDM 所以入栈后sys栈中的数据为: pc //task被抢占执行到的地址值 r14 ... r0 spsr ulCriticalNesting pxCurrentTCB->pxTopOfStack 一共18项 <2.2>portRESTORE_CONTEXT出栈宏 portRESTORE_CONTEXT MACRO LDR R1, =pxCurrentTCB//pxCurrentTCB对应当前切换出去之后需要执行的task的上下文 //它的数据结构体偏移0处就是task的pxTopOfStack堆栈顶值 LDR R0, [R1] LDR LR, [R0]//task的sp存放到swi的lr LDR R0, =ulCriticalNesting LDMFD LR!,{R1} STR R1, [R0]//恢复新task栈中的ulCriticalNesting到全局量ulCriticalNesting LDMFD LR!,{R0} MSR SPSR_cxsf, R0//将新task的spsr恢复到SPSR_cxsf中 LDMFD LR, {R0-R14}^//恢复新task的r0-r14寄存器 NOP LDR LR, [LR, #+60]//调整sp指针,并读取pc值 SUBS PC, LR, #4//因为是在ISR模式下的lr,所以需要+4调整,执行新task,同时恢复SPSR_cxsf到CPSR ENDM 以上汇编设计的很精巧,短短几行就完成了数据的保存与恢复,还有一个设计的很精巧的地方就是第一次 启动FreeRTOS的启动函数vTaskStartScheduler()->xPortStartScheduler()->vPortStartFirstTask() //在source/portable/iar/atmelsam7s64/Portasm.s79中定义 vPortStartFirstTask: portRESTORE_CONTEXT//很巧妙,先将task从TCB中恢复,之后r0~lr、spsr、ulCriticalNesting //和pxCurrentTCB->pxTopOfStack就真的是那个task执行起来的环境值了
//因为xTaskCreate()->pxPortInitialiseStack()初始化task堆栈时,将task的入口地址
//进行也和下面swi一样的调整来模拟IRQ环境,调整代码为:
//*pxTopOfStack = ( portSTACK_TYPE ) pxCode + portINSTRUCTION_SIZE;
//其中pxCode为task的程序入口地址,portINSTRUCTION_SIZE = 4;很巧妙! vPortYieldProcessor: ADD LR, LR, #4 portSAVE_CONTEXT LDR R0, =vTaskSwitchContext mov lr, pc BX R0 portRESTORE_CONTEXT 3.计算高优先级任务,以及同优先级任务轮转调度函数vTaskSwitchContext() //位于source/Tasks.c文件 <3.1>vTaskSwitchContext()函数 void vTaskSwitchContext( void ) { if( uxSchedulerSuspended != ( unsigned portBASE_TYPE ) pdFALSE ) {//2007-09-27 gliethttp //调度器被上锁,那么标示xMissedYield为需要调度,也就是先登记上,在随后合适的时候会执行登记的了实际调度 xMissedYield = pdTRUE; return;//保持当前task继续运行 }
while( listLIST_IS_EMPTY( &( pxReadyTasksLists[ uxTopReadyPriority ] ) ) ) {//2007-09-27 gliethttp //可能位于最高优先级数组中的双向链表上的最高优先级任务自己已经主动放弃cpu使用权 //所以需要执行次高优先级任务,依此类推,直到次高优先级数组中有task任务挂在上面为止 --uxTopReadyPriority; } //2007-09-27 gliethttp //将pxCurrentTCB恢复到当前最高优先级任务中,任务双向链表最靠前面的那个task, //也正因为这样实现了同优先级多任务tasks们的轮转调度(注:不是基于时间的轮转调度) listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopReadyPriority ] ) ); vWriteTraceToBuffer();//没设么用,一个freerots自己调试自己时用到的接口而已 } <3.2>listGET_OWNER_OF_NEXT_ENTRY()宏 来看看抢占计算和轮转调度综合在一起的宏函数 #define listGET_OWNER_OF_NEXT_ENTRY( pxTCB, pxList ) \ { \ xList * const pxConstList = pxList; \ /* Increment the index to the next item and return the item, ensuring */ \ /* we don't return the marker used at the end of the list. */ \ ( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext; \ if( ( pxConstList )->pxIndex == ( xListItem * ) &( ( pxConstList )->xListEnd ) ) \ { \ ( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext; \ } \ pxTCB = ( pxConstList )->pxIndex->pvOwner; \ } 其实原理也很简单,但是很巧妙,pxIndex指向的永远都是当前正在运行的task所在双向链表上的taskTCB链表别名, 所以这也就决定了这个task所处的双向链表位置将来是最老的位置,也就是最后的位置, 所以pxIndex从task运行起来之后的角度说,pxIndex指向的是某个优先级数组上挂接的所有task中最老的一个(gliethttp).
|