目录
FreeRTOS内核调度使用了大量的列表(list)和列表项(listitem)数据结构。它的源码中涉及到很多列表的操作,对于FreeRTOS来说,列表就是它最基础的一部分,列表被用作FreeRTOS调度器使用,用于跟踪任务,处于就绪,挂起,延时的任务都会被挂接到各自的列表中,用户程序如果有需要,也可以使用列表,其中就连FreeRTOS的任务调度其实核心也涉及到列表。
FreeRTOS列表使用指针指向列表项。一个列表(list)下面可能有很多个列表项(list item),每个列表项都有一个指针指向列表。如图所示
列表和列表项
列表项有两种形式,全功能版的列表项xLIST_ITEM和迷你版的列表项xMINI_LIST_ITEM。我们来看一下它们具体的定义,先看全功能版。
struct xLIST_ITEM
{
listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE /*用于检测列表项数据是否完整*/
configLIST_VOLATILE TickType_t xItemValue; /*列表项值*/
struct xLIST_ITEM * configLIST_VOLATILE pxNext; /*指向列表中下一个列表项*/
struct xLIST_ITEM * configLIST_VOLATILE pxPrevious; /*指向列表中上一个列表项*/
void * pvOwner; /*指向一个任务TCB*/
void * configLIST_VOLATILE pvContainer; /*指向包含该列表项的列表 */
listSECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE /*用于检测列表项数据是否完整*/
};
typedef struct xLIST_ITEM ListItem_t;
宏listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE和listSECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE用于检查列表项数据是否完整,在projdefs.h中,如果将宏configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES设置为1,则使能列表项数据完整性检查,则宏listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE和listSECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE会被两个已知的数值代替。
xItemValue是列表项值,通常是一个被跟踪的任务优先级或是一个调度事件的计数器值。如果任务因为等待从队列取数据而进入阻塞状态,则任务的事件列表项的列表项值保存任务优先级有关信息,状态列表项的列表项值保存阻塞时间有关的信息。这个变量被configLIST_VOLATILE修饰,configLIST_VOLATILE被映射成C语言关键字volatile,表明这个变量是"易变的",告诉编译器不得对这个变量进行代码优化,因为列表项的成员可能会在中断服务程序中被更新。
pxNext和pxPrevious是列表项类型指针,用来指向列表中下一个和上一个列表项,通过这两个指针,列表项之间可以形成类似双向链表结构。
指针pvOwner通常指向一个任务TCB。
指针pvContainer指向包含该列表项的列表。
迷你版的列表项xMINI_LIST_ITEM是全功能版列表项xLIST_ITEM的一个子集,定义如下所示:
cpp
struct xMINI_LIST_ITEM
{
listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE /*用于检测列表项数据是否完整*/
configLIST_VOLATILE TickType_t xItemValue;
struct xLIST_ITEM * configLIST_VOLATILE pxNext;
struct xLIST_ITEM * configLIST_VOLATILE pxPrevious;
};
typedef struct xMINI_LIST_ITEM MiniListItem_t;
既然有了全功能版的列表项,为什么还要声明迷你版的列表项呢?这是因为列表结构体需要一个列表项成员,但又不需要列表项中的所有字段,所以才有了迷你版列表项。迷你列表项起到的主要作用就是标识列表的末尾,所以它的值也设置成了最大值,列表结构体定义为:
cpp
typedef struct xLIST
{
listFIRST_LIST_INTEGRITY_CHECK_VALUE /*用于检测列表项数据是否完整*/
configLIST_VOLATILE UBaseType_t uxNumberOfItems;
ListItem_t * configLIST_VOLATILE pxIndex; /*用于遍历列表*/
MiniListItem_t xListEnd; /*列表项*/
listSECOND_LIST_INTEGRITY_CHECK_VALUE /*用于检测列表项数据是否完整*/
}List_t;
和列表项定义相同,宏listFIRST_LIST_INTEGRITY_CHECK_VALUE和listSECOND_LIST_INTEGRITY_CHECK_VALUE用于检查列表项数据是否完整,在projdefs.h中,如果将宏configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES设置为1,则使能列表项数据完整性检查,则宏listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE和listSECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE会被两个已知的数值代替。
uxNumberOfItems表示该列表中挂接的列表项数目,0表示列表为空。
列表项类型指针用于遍历列表,列表初始化后,这个指针指向&xListEnd。通过宏listGET_OWNER_OF_NEXT_ENTRY()来获取列表中的下一个列表项。
列表项xListEnd用于标记列表结束。xListEnd.xItemValue被初始化为一个常数,其值与硬件架构相关,为0xFFFF(16位架构)或者0xFFFFFFFF(32位架构)。
关于列表的一些操作
初始化列表
列表结构体中包含一个列表项成员,主要用于标记列表结束。初始化列表就是把这个列表项插入到列表中。
cpp
void vListInitialise( List_t * const pxList )
{
/*列表索引指向列表项*/
pxList->pxIndex = ( ListItem_t * )&( pxList->xListEnd );
/* 设置为最大可能值 */
pxList->xListEnd.xItemValue =portMAX_DELAY;
/* 列表项xListEnd的pxNext和pxPrevious指针指向了它自己 */
pxList->xListEnd.pxNext = (ListItem_t * ) &( pxList->xListEnd );
pxList->xListEnd.pxPrevious= ( ListItem_t * ) &( pxList->xListEnd );
pxList->uxNumberOfItems = ( UBaseType_t) 0U;
/* 设置为已知值,用于检测列表数据是否完整*/
listSET_LIST_INTEGRITY_CHECK_1_VALUE(pxList );
listSET_LIST_INTEGRITY_CHECK_2_VALUE(pxList );
}
如果宏configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES设置为1,则使能列表项数据完整性检查,则宏listSET_LIST_INTEGRITY_CHECK_1_VALUE()和listSET_LIST_INTEGRITY_CHECK_2_VALUE被一个已知值代替,默认为0x5a5a(16位架构)或者0x5a5a5a5a(32位架构)。
假设禁止列表数据完整性检查,初始化后的列表如图1-2所示,uxNumberOfItems被初始化为0,xListEnd.xItemValue初始化为0xffffffff,pxIndex、xListEnd.pxNext和xListEnd.pxPrevious初始化为指向列表项xListEnd。
初始化列表项
列表项的初始化很简答, 只需要声明该列表项不属于任何一个列表就可以了。
cpp
void vListInitialiseItem( ListItem_t * const pxItem )
{
pxItem->pvContainer = NULL;
/*设置为已知值,用于检测列表项数据是否完整*/
listSET_FIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE(pxItem );
listSET_SECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE(pxItem );
}
如果宏configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES设置为1,则使能列表项数据完整性检查,则宏listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE和listSECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE会被两个已知的数值代替,默认为0x5a5a(16位架构)或者0x5a5a5a5a(32位架构)。
假设禁止列表项数据完整性检查,初始化后的列表项如图1-3所示。仅是将指针pvContainer设置为空指针,该指针用于指向包含该列表项的列表,这里设置为NULL表示这个列表项不属于任何列表。
列表插入列表项
每个列表项对象都有一个列表项值(xItemValue),通常是一个被跟踪的任务优先级或是一个调度事件的计数器值。调用API函数vListInsert( List_t * const pxList, ListItem_t * const pxNewListItem)可以将pxNewListItem指向的列表项插入到pxList指向的列表中,列表项在列表的位置由pxNewListItem->xItemValue决定,按照升序排列。
cpp
void vListInsert( List_t * const pxList, ListItem_t * const pxNewListItem )
{
ListItem_t *pxIterator;
const TickType_t xValueOfInsertion = pxNewListItem->xItemValue;
/* 检查列表和列表项数据的完整性,仅当configASSERT()定义时有效。*/
listTEST_LIST_INTEGRITY( pxList );
listTEST_LIST_ITEM_INTEGRITY(pxNewListItem );
/*将新的列表项插入到列表,根据xItemValue的值升序插入列表。*/
if( xValueOfInsertion == portMAX_DELAY)
{
pxIterator =pxList->xListEnd.pxPrevious;
}
else
{
for( pxIterator = (ListItem_t * ) &( pxList->xListEnd );pxIterator->pxNext->xItemValue <= xValueOfInsertion; pxIterator =pxIterator->pxNext )
{
/* 这里为空 */
}
}
pxNewListItem->pxNext =pxIterator->pxNext;
pxNewListItem->pxNext->pxPrevious= pxNewListItem;
pxNewListItem->pxPrevious =pxIterator;
pxIterator->pxNext = pxNewListItem;
pxNewListItem->pvContainer = ( void* ) pxList;
( pxList->uxNumberOfItems )++;
}
列表项末尾插入
cpp
void vListInsertEnd( List_t * const pxList, ListItem_t * const pxNewListItem )
{
ListItem_t* const pxIndex = pxList->pxIndex;
/*检查列表和列表项数据的完整性,仅当configASSERT()定义时有效。*/
listTEST_LIST_INTEGRITY( pxList );
listTEST_LIST_ITEM_INTEGRITY(pxNewListItem );
/*向列表中插入新的列表项*/
pxNewListItem->pxNext = pxIndex;
pxNewListItem->pxPrevious =pxIndex->pxPrevious;
mtCOVERAGE_TEST_DELAY();
pxIndex->pxPrevious->pxNext =pxNewListItem;
pxIndex->pxPrevious = pxNewListItem;
pxNewListItem->pvContainer = ( void* ) pxList;
( pxList->uxNumberOfItems )++;
}
重点
pxIndex指向的是什么
重点:一开始学习的时候,一直不明白这个pxIndex有什么用,因为我在有关列表的list.c中的API函数中根本没发现有改变它的代码,以为它一直是初始化的值,就是一直指向迷你列表项,其实不然,作为一名程序员,一切从源码中得到答案。
搜索代码之后发现,中间对pxIndex赋值的地方只有listGET_OWNER_OF_NEXT_ENTRY这个接口(list.h中的一个有参宏)
每调用一次listGET_OWNER_OF_NEXT_ENTRY,列表的pxIndex会指向下一个列表项。
而调用listGET_OWNER_OF_NEXT_ENTRY,主要是一下的一些函数接口:
xItemValue存的是什么
列表项值,都是通过listSET_LIST_ITEM_VALUE这个宏来赋值的。
#define listSET_LIST_ITEM_VALUE( pxListItem, xValue ) ( ( pxListItem )->xItemValue = ( xValue ) )
搜了一下,主要赋值了两类值
-
优先级
listSET_LIST_ITEM_VALUE( &( pxTCB->xEventListItem ), ( TickType_t ) configMAX_PRIORITIES - ( TickType_t ) uxPriorityToUse ); /*lint !e961 MISRA exception as the casts are only redundant for some ports. */
可以看到和我们之前说的,设置优先级是在任务的事件列表项中。
-
醒来时间(对于状态列表)
listSET_LIST_ITEM_VALUE( &( pxCurrentTCB->xStateListItem ), xTimeToWake );
同理,和之前说的一样,对于任务的阻塞时间,在任务的状态列表项中设置列表项的值。
vListInsertEnd()的插入位置
Tips: 这个函数是最容易造成误解的一个函数,字面理解,在我开始学的时候我以为就是插入到最后一个迷你列表项的前面,所谓末尾插入肯定是最后一项嘛,阅读源码之后,其实不然,因为列表中有一项成员
cpp
ListItem_t * configLIST_VOLATILE pxIndex;
该成员主要作用就是用来遍历列表的。阅读源码发现它是插入在pxIndex所指的列表项的前面。这里体现了FreeRTOS的哲学理念,"公平" ,如果pxIndex,指向的是当前的索引的列表项表示正在使用,这时比如顺序是1->2->3,现在pxIndex指向2,要插入4,这时你4肯定是要最后遍历访问的,意味着就是访问顺序应该是2->3->1->4,所以它要插入在2前面,我的方法是记住一个口诀,末尾插入就是插入pxIndex所指列表项的前一项的后面,可能听着有点别扭(不就是所指列表项的前面嘛🤣 ,细细体会,有公平的哲学思想)
List的头尾在哪里?
结尾是xListEnd吗?
这个是初始化时候的结尾,名字也是结尾。
但是vListInsertEnd又不是根据xListEnd来定的,是插在pxIndex前面的,那有的认为pxIndex就是开头,查了一些资料,头尾好像没有怎么分,但是第二种认可度更高,因为末尾插入是这么用的。
通用链表的三种实现方式
针对不同的列表项,如果不采用通用的链表处理,就需要实现不同链表的操作函数,实现通用链表有三种实现方式:
方法一
这样node节点直接就是它的开头,如果你要根据比如dog的age进行排序,直接可以通过node指针把它转成列表项指针就可以访问到它的age属性。
方法二
这里就要根据node在结构体里的地址,转成列表项的指针进而访问到它的属性,进行排序操作等。这时要计算node在结构体的偏移,可以采用如下方式,结构体地址为0,直接看node的地址。
Linux和RT-Thread采用这种实现通用链表 🤣 。
方法三
直接通过container指针就可以知道它属于那种列表项,进而可以访问到其它属性。
FreeRTOS采用这种方式实现通用链表🤣,它里面的pvOwner,就是我们上面说的方法三的Container的概念 。
总结
FreeRTOS的列表和列表项采用了一种通用的列表管理,不像我以前学的数据结构中的链表操作一样,其中的节点都是具体的结构体的内容,所以是针对具体的一类结构体,比如struct people,这样导致的后果就是所有有关链表操作的内容都是针对这类结构体,如果稍微改成struct dog,这样就需要全部重写链表的所有操作了。FreeRTOS采用一种方法,写出了通用的链表操作,让我不得不感叹这代码确实是写的好!🤣