FreeRTos任务管理分析
(2011-08-05 16:22:01) 标签: freertos杂谈 | 分类: freertos入门学习 |
freertos是一个轻量级的rtos,它目前实现了一个微内核,并且port到arm7, avr, pic18, coldfire等众多处理器上;目前已经在rtos的市场上占有不少的份额。它当然不是一个与vxworks之类的rtos竞争的操作系统,它的目标在于低性能小RAM的处理器上。整个系统只有3个文件,外加上port的和处理器相关的两个文件,实现是很简洁的。
与ucosii不同,它是free的,ucosii不是free的,虽然它的代码是公开的。FreeRTOS提供的功能包括:任务管理、时间管理、信号量、消息队列、内存管理。FreeRTOS内核支持优先级调度算法,每个任务可根据重要程度的不同被赋予一定的优先级,CPU总是让处于就绪态的、优先级最高的任务先运行。FreeRT0S内核同时支持轮换调度算法,系统允许不同的任务使用相同的优先级,在没有更高优先级任务就绪的情况下,同一优先级的任务共享CPU的使用时间。这一点是和ucosii不同的。
另外一点不同是freertos既可以配置为可抢占内核也可以配置为不可抢占内核。当FreeRTOS被设置为可剥夺型内核时,处于就绪态的高优先级任务能剥夺低优先级任务的CPU使用权,这样可保证系统满足实时性的要求;当FreeRTOS被设置为不可剥夺型内核时,处于就绪态的高优先级任务只有等当前运行任务主动释放CPU的使用权后才能获得运行,这样可提高CPU的运行效率。
这篇文章是以freertos v5.0版本的代码为例子分析下它的任务管理方面的实现。时间关系可能没有太多时间写的很详细了。
1.链表管理
freertos里面的任务管理,queue,semaphore管理等都借助于双向链表,它定义了个通用的数据结构
struct xLIST_ITEM
{
};
这个数据结构定义了一个通用的链表节点;下面的数据结构定义了一个双向链表
typedef struct xLIST
{
} xList;
而下面这个数据结构用在xList中,只是为了标记一个链表的尾,是一个marker
struct xMINI_LIST_ITEM
{
};
typedef struct xMINI_LIST_ITEM xMiniListItem;
对于链表的操作也定义了一系列的函数和宏,在list.c文件中。如初始化个链表,吧一个节点插入链表等。
初始化链表:
void vListInitialise( xList *pxList )
{
}
把一个节点插入到链表尾部:
void vListInsertEnd( xList *pxList, xListItem *pxNewListItem )
{
volatile xListItem * pxIndex;
}
这些就不多说了。
2.任务控制块
typedef struct tskTaskControlBlock
{
} tskTCB;
其中uxBasePriority用于解决优先级反转,freertos采用优先级继承的办法解决这个问题,在继承时,将任务原先的优先级保存在这个成员中,将来再从这里恢复任务的优先级。
3.系统全局变量
freertos将任务根据他们的状态分成几个链表。所有就绪状态的任务根据任务优先级加到对应的就绪链表中。系统为每个优先级定义了一个xList。如下:
static xList pxReadyTasksLists[ configMAX_PRIORITIES ];
此外,所有延时的任务加入到两个延时链表之一。
static xList xDelayedTaskList1;
static xList xDelayedTaskList2;
还定义了两个指向延时链表的指针:
static xList * volatile pxDelayedTaskList;
static xList * volatile pxOverflowDelayedTaskLis
freertos弄出两个延时链表是因为它的延时任务管理的需要。freertos根据任务延时时间的长短按序将任务插入这两个链表之一。在插入前先把任务将要延时的xTicksToDelay数加上系统当前tick数,这样得到了一个任务延时due time(到期时间)的绝对数值。但是有可能这个相加操作会导致溢出,如果溢出则加入到pxOverflowDelayedTaskLis
freertos还定义了个pending链表:
static xList xPendingReadyList;
这个链表用在调度器被lock(就是禁止调度了)的时期,如果一个任务从非就绪状态变为就绪状态,它不直接加到就绪链表中,而是加到这个pending链表中。等调度器重新启动(unlock)的时候再检查这个链表,把里面的任务加到就绪链表中
static volatile xList xTasksWaitingTermination
static volatile unsigned portBASE_TYPE uxTasksDeleted = ( unsigned portBASE_TYPE ) 0;
一个任务被删除的时候加入到xTasksWaitingTermination
static xList xSuspendedTaskList;
这个链表记录着所有被xTaskSuspend挂起的任务,注意这不是那些等待信号量的任务。
static volatile unsigned portBASE_TYPE uxCurrentNumberOfTasks;记录了当前系统任务的数目
static volatile portTickType xTickCount;是自启动以来系统运行的ticks数
static unsigned portBASE_TYPE uxTopUsedPriority;记录当前系统中被使用的最高优先级,
static volatile unsigned portBASE_TYPE uxTopReadyPriority;记录当前系统中处于就绪状态的最高优先级。
static volatile signed portBASE_TYPE xSchedulerRunning
4.任务管理
freertos与ucosii不同,它的任务控制块并不是静态分配的,而是在创建任务的时候动态分配。另外,freertos的优先级是优先级数越大优先级越高,和ucosii正好相反。任务控制块中也没有任务状态的成员变量,这是因为freertos中的任务总是根据他们的状态连入对应的链表,没有必要在任务控制块中维护一个状态。此外freertos对任务的数量没有限制,而且同一个优先级可以有多个任务。
先看任务创建:
signed portBASE_TYPE xTaskCreate( pdTASK_CODE pvTaskCode, const signed portCHAR * const pcName, unsigned portSHORT usStackDepth, void *pvParameters, unsigned portBASE_TYPE uxPriority, xTaskHandle *pxCreatedTask )
{
signed portBASE_TYPE xReturn;
tskTCB * pxNewTCB;
#if ( configUSE_TRACE_FACILITY == 1 )
#endif
}
其中prvAllocateTCBAndStack分配tcb和stack内存,这个里面调用了pvportMalloc和pvPortFree函数来分配和释放内存,这两个函数对应于C标准库里面的malloc和free。但是标准库中的mallo和free存在以下缺点:并不是在所有的嵌入式系统中都可用,要占用不定的程序空间,可重人性欠缺以及执行时间具有不可确定性,而且多次反复调用可能导致严重的内存碎片。因此freertos在内存管理那块自己实现了这两个函数。
static tskTCB *prvAllocateTCBAndStack( unsigned portSHORT usStackDepth )
{
tskTCB *pxNewTCB;
}
再看任务删除。
freertos的任务删除分两步完成,第一步在vTaskDelete中完成,FreeRTOS先把要删除的任务从就绪任务链表和事件等待链表中删除,然后把此任务添加到任务删除链表(即那个xTasksWaitingTermination
void vTaskDelete( xTaskHandle pxTaskToDelete )
{
}
再看空闲任务做的第2步工作:
static portTASK_FUNCTION( prvIdleTask, pvParameters )
{
这里prvCheckTasksWaitingTerm
static void prvCheckTasksWaitingTerm
{
}
调度器的禁止和打开
这是一种同步机制,比关中断要温和点。禁止调度由vTaskSuspendAll实现,打开调度由xTaskResumeAll实现。
void vTaskSuspendAll( void )
{
}
这个很简单,系统维护一个计数uxSchedulerSuspended,当它大于0时候表示禁止调度,等于0则打开调度(允许调度)。
signed portBASE_TYPE xTaskResumeAll( void )
{
register tskTCB *pxTCB;
signed portBASE_TYPE xAlreadyYielded = pdFALSE;
}
任务挂起与唤醒
freertos的任务关起与ucosii也不大一样。它把所有挂起的任务加到xSuspendedTaskList中,而且一旦调用vTaskSuspend()函数挂起一个任务,该任务就将从所有它原先连入的链表中删除(包括就绪表,延时表和它等待的事件链表),也就是说,和 ucosii不同,一旦一个任务被挂起,它将取消先前它的延时和对事件的等待。ucosii中是不同的,在ucosii里面一个任务被挂起仅仅是把任务的状态或上一个OS_STAT_SUSPEND并从就绪表中删除,如果先前这个任务正在等待某事件,则并不取消等待。
//如果传进来的pxTaskToSuspend==NULL,则表示挂起当前任务
void vTaskSuspend( xTaskHandle pxTaskToSuspend )
{
相反的唤醒就是把任务从xSuspendedTaskList中删除,加到对应的就绪链表中(根据任务的优先级),然后如果唤醒的任务优先级高于当前任务优先级,则调度。
void vTaskResume( xTaskHandle pxTaskToResume )
任务调度
freertos支持多个任务具有相同的优先级,因此,当它被配置为可抢占内核时,调度算法既支持基于优先级的调度,也支持时间片轮流调度。任何时候调度器运行时它都选择处于就绪状态下的优先级最高的那个任务;如果有多个任务处于同一优先级,则freertos每个时钟节拍的中断服务程序中,将对这些任务应用换调度算法轮流执行这些任务。
系统用uxTopReadyPriority全局变量记录当前处于就绪态的任务的最高优先级。调度的时候就根据这个uxTopReadyPriority直接找到就绪链表中pxReadyTasksLists[ uxTopReadyPriority ]的任务,进行运行。
一个任务可以通过调用taskYIELD()让出cpu,从而调度令一个任务运行。它的实现如下:
#define taskYIELD()
而portYIELD()是一个体系结构相关的函数,对于不同的mcu需要实现这么一个函数完成调度。我拿atmel的atmega323 mcu为例子,说明下具体实现。
extern void vPortYield( void ) __attribute__ ( ( naked ) );
#define portYIELD()
void vPortYield( void ) __attribute__ ( ( naked ) );
void vPortYield( void )
{
}
portYIELD()就是vportYield(),它保存现场,然后调用vTaskSwitchContext()这个函数选择下一个运行的任务,然后portRESTORE_CONTEXT()完成任务切换。
void vTaskSwitchContext( void )
{
}
这里注意的是listGET_OWNER_OF_NEXT_ENTRY()宏并不是简单的从队列中取下第一个任务,而是walk through这个队列,比如上一次调度它从这个队列上取下的是第一个任务,那么这次调度选中的则是该队列中的第2个任务。这样就保证了同一优先级的多个任务之间公平的平分处理器时间。
选中任务后(用pxCurrentTCB指向它)。那么在portRESTORE_CONTEXT()中就完成最后的切换。因此这个地方有些有趣,函数vTaskSwitchContext()从名称看给人感觉是完成任务切换的,但是其实并不是这样,它只完成选择下一个运行的任务(也就是将要切换过去的任务),真正的切换时在portRESTORE_CONTEXT()中就完成的。
任务调度还可以发生在时钟节拍中断isr中,这个当然也是与cpu体系结构相关的。仍然以atmega323为例。它用的是定时器1的比较中断A作为时钟节拍产生器。其中断isr是:
void SIG_OUTPUT_COMPARE1A( void ) __attribute__ ( ( signal, naked ) );
而vPortYieldFromTick()就是完成调度。代码如下:
void vPortYieldFromTick( void ) __attribute__ ( ( naked ) );
void vPortYieldFromTick( void )
{