FreeRTOS 内核中采用双向循环链表来进行任务调度,对任务总数没有限制,同一优先级的任务数也没有限制。相对于uC/OS-II 来说是一个大的优点。不过,有利必有弊。采用双向链表后代码相对来说要复杂一些。
FreeRTOS 中的链表和链表元素的定义如下:
typedef struct xLIST { volatile unsigned portBASE_TYPE uxNumberOfItems; // 链表中有多少元素 volatile xListItem * pxIndex; volatile xMiniListItem xListEnd; } xList; struct xLIST_ITEM { portTickType xItemValue; volatile struct xLIST_ITEM * pxNext; volatile struct xLIST_ITEM * pxPrevious; void * pvOwner; void * pvContainer; };typedef struct xLIST_ITEM xListItem;
先从链表元素开始分析,FreeRTOS 中的 xListItem 在其他代码中通常被称为 Node,也就是链表节点。
结构体中各个部分的含义如下:
xItemValue:存放链表节点的值,在 FreeRTOS 中,用这个值来存放时间,用于延时相关的功能。因此,这个变量的类型是 portTickType。
pxNext 和 pxPrevious: 是双向链表两个方向的指针。分别指向后一个元素和前一个节点。
pvOwner:xListItem 本身是包含在 tskTCB 结构中的(tskTCB结构存放任务的各种信息,一个tskTCB 对应的就是一个任务),pvOwner 指向所在的 tskTCB 结构。因此,通过xListItem 就知道对应的是哪个任务了。
pvContainer:pvContainer 指向它所在的xList,根据这个指针,就能知道任务是在就绪任务链表中还是阻塞任务链表中。当xListItem不在任何链表中时,pvContainer的值为 NULL。
链表节点只有初始化后采用使用,初始化函数是:void vListInitialiseItem( xListItem *pxItem ),初始化函数只做了一件事,就是将pvContainer 设为 NULL。
xList 表示对应的链表。各个元素的含义如下:
uxNumberOfItems:记录链表中有多少个节点(不包含xListEnd节点)。
pxIndex:是一个指针,用来遍历链表。
xListEnd:是链表中一个不能删除的节点。xListEnd 的后续节点指向的就是真正有用的链表节点。当没有其他链表元素时,xListEnd 的前后指针都指向自己。
链表初始化函数比较简单,下面列出代码:
void vListInitialise( xList *pxList ) { pxList->pxIndex = ( xListItem * ) &( pxList->xListEnd ); pxList->xListEnd.xItemValue = portMAX_DELAY; pxList->xListEnd.pxNext = ( xListItem * ) &( pxList->xListEnd ); pxList->xListEnd.pxPrevious = ( xListItem * ) &( pxList->xListEnd ); pxList->uxNumberOfItems = ( unsigned portBASE_TYPE ) 0U; }
链表操作主要有两大方面:将节点插入到链表,和从链表中删除节点。下面分别介绍。
FreeRTOS 将节点插入到链表又有两种操作。一个是将节点按照升序插入到链表的合适的位置。第二个是将节点插入到链表的结尾。
vListInsert 函数将节点按照升序插入到链表的合适的位置,前提是链表原本就是按照升序排序的。算法很简单,就是从第一个元素(xListEnd.pxNext)开始往后遍历,直到找到合适的位置,然后将节点插进去。
void vListInsert( xList *pxList, xListItem *pxNewListItem ) { volatile xListItem *pxIterator; portTickType xValueOfInsertion; xValueOfInsertion = pxNewListItem->xItemValue; if( xValueOfInsertion == portMAX_DELAY ) { pxIterator = pxList->xListEnd.pxPrevious; } else { for( pxIterator = ( xListItem * ) &( pxList->xListEnd ); pxIterator->pxNext->xItemValue <= xValueOfInsertion; pxIterator = pxIterator->pxNext ); } pxNewListItem->pxNext = pxIterator->pxNext; pxNewListItem->pxNext->pxPrevious = ( volatile xListItem * ) pxNewListItem; pxNewListItem->pxPrevious = pxIterator; pxIterator->pxNext = ( volatile xListItem * ) pxNewListItem; pxNewListItem->pvContainer = ( void * ) pxList; ( pxList->uxNumberOfItems )++; }
vListInsertEnd 函数将节点插入到链表的最后。所谓的最后,其实就是让 pxList->pxIndex 指向它就行了,因为遍历时是从 pxIndex->pxNext 的开始的,遍历一圈之后最后才能到 pxIndex。算法很简单,下面是代码。
void vListInsertEnd( xList *pxList, xListItem *pxNewListItem ) { volatile xListItem * pxIndex; pxIndex = pxList->pxIndex; pxNewListItem->pxNext = pxIndex->pxNext; pxNewListItem->pxPrevious = pxList->pxIndex; pxIndex->pxNext->pxPrevious = ( volatile xListItem * ) pxNewListItem; pxIndex->pxNext = ( volatile xListItem * ) pxNewListItem; pxList->pxIndex = ( volatile xListItem * ) pxNewListItem; pxNewListItem->pvContainer = ( void * ) pxList; ( pxList->uxNumberOfItems )++; }
从这两种将节点插入到链表的方法也能看出它们是不能混用的,将节点插入到链表最后是不能保证链表是升序排列的。FreeRTOS 的作者当然明白这个道理。在 FreeRTOS 中这两个函数各有自己的用途,当我们需要将一个任务延时一定时间时,使用vListInsert函数将其插入到xDelayedTaskList1 或xDelayedTaskList2。具体插入到那个链表要看延时值是否溢出了。而将一个任务设置为就绪状态时要将其按照优先级n 插入到pxReadyTasksLists[n]的最后,这时就要用vListInsertEnd函数了。插入到xPendingReadyList 和 xTasksWaitingTermination 链表时也要插入到最后,因此也用vListInsertEnd函数。总结一下,只有涉及到延时时才会用到vListInsert函数。
vListRemove 将节点从链表中删除,下面是代码。
void vListRemove( xListItem *pxItemToRemove ) { xList * pxList; pxItemToRemove->pxNext->pxPrevious = pxItemToRemove->pxPrevious; pxItemToRemove->pxPrevious->pxNext = pxItemToRemove->pxNext; pxList = ( xList * ) pxItemToRemove->pvContainer; if( pxList->pxIndex == pxItemToRemove ) { pxList->pxIndex = pxItemToRemove->pxPrevious; } pxItemToRemove->pvContainer = NULL; ( pxList->uxNumberOfItems )--; }
链表的另一个重要操作就是遍历链表,在 FreeRTOS 源码中用宏来实现的。算法很简单,就是将 pxIndex 指向的下一个节点的对应的 TCB 结构指针返回来,然后pxIndex向后挪移个位置。需要注意的就是pxIndex的下一个节点是 xListEnd 的时候要再向后挪一下。
#define listGET_OWNER_OF_NEXT_ENTRY( pxTCB, pxList ) \ { xList * const pxConstList = ( pxList ); \ ( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext; \ if( ( pxConstList )->pxIndex == ( xListItem * ) &( ( pxConstList )->xListEnd ) ) \ { \ ( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext; \ } \ ( pxTCB ) = ( pxConstList )->pxIndex->pvOwner; \ }
至此,链表有关的操作就都分析完了。实际上 FreeRTOS 源码中只用到了双向循环链表的最基本的功能。上面的分析也非常粗略,如果想更深入的学习链表的相关知识,可以找本算法或数据结构的教材来看看。不过,对于理解 FreeRTOS内核 ,上面这些已经够了。