3.8 FreeRTOS列表和列表项

列表和列表项简介

FreeRTOS中,列表是非常重要的!想要看懂FreeRTOS的源码,学会列表是必不可少的。

列表是 FreeRTOS 中的一个数据结构,概念上和链表有点类似,列表被用来跟踪 FreeRTOS中的任务。

列表项就是存放在列表中的项目。

我们看下图,可以明显的感受到,列表就是循环双向链表:

列表的特点:列表项间的地址非连续的,是人为的连接到一起的。列表项的数目是由后期添加的个数决定的,随时可以改变。

数组的特点:数组成员地址是连续的,数组在最初确定了成员数量后期无法改变。

在OS中任务的数量是不确定的,并且任务状态是会发生改变的,所以非常适用链表这种数据结构。

有关于列表的东西均在文件 list.c 和 list.h 中,首先我们先看下在list.h中的,列表相关结构体:

①List_t结构体:

typedef struct xLIST
{
    	listFIRST_LIST_INTEGRITY_CHECK_VALUE			/* 校验值 */
    	volatile UBaseType_t uxNumberOfItems;			/* 列表中的列表项数量 */
   	ListItem_t * configLIST_VOLATILE pxIndex			/* 用于遍历列表项的指针 */
    	MiniListItem_t xListEnd					/* 末尾列表项 */
    	listSECOND_LIST_INTEGRITY_CHECK_VALUE		/* 校验值 */
} List_t;

1、我们发现有两个宏,分别是:

listFIRST_LIST_INTEGRITY_CHECK_VALUE

listSECOND_LIST_INTEGRITY_CHECK_VALUE

这两个宏是确定的已知常量, FreeRTOS通过检查这两个常量的值, 来判断列表的数据在程序运行过程中,是否遭到破坏 ,该功能一般用于调试, 默认是不开启的,所以这两个宏,我们无需关注,以后在其他结构体中也无需关注,没啥用。

2、成员uxNumberOfItems,用于记录列表中列表项的个数(不包含末尾列表项xListEnd)。

3、成员 pxIndex 用于指向列表中的某个列表项,一般用于遍历列表中的所有列表项 。

4、成员变量 xListEnd 是一个迷你列表项,总是排在最末尾 。

②xLIST_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							/* 列表项的拥有者 */
    	struct xLIST * configLIST_VOLATILE pxContainer; 			/* 列表项所在列表 */
   	listSECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE			/* 用于检测列表项的数据完整性 */
};
typedef struct xLIST_ITEM ListItem_t; 

1、成员变量 xItemValue 为列表项的值,这个值多用于按升序对列表中的列表项进行排序 。

2、成员变量 pxNext 和 pxPrevious 分别用于指向列表中列表项的下一个列表项和上一个列表项。

3、成员变量 pxOwner 用于指向包含列表项的对象,通常是任务控制块。

4、成员变量 pxContainer 用于指向列表项所在列表(就序列表、阻塞列表、挂起列表)。

 

③xMINI_LIST_ITEM结构体:

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;

迷你列表项,迷你列表项也是列表项,但迷你列表项仅用于标记列表的末尾和挂载其他插入列表中的列表项。标记列表的末尾我们好理解,就是把迷你列表项当成某个列表的末尾,但是“挂载其他插入列表的列表项”是什么意思呢?我们一会来讲,先看结构体成员。

1、成员变量 xItemValue 为列表项的值,这个值多用于按升序对列表中的列表项进行排序。

2、成员变量 pxNext 和 pxPrevious 分别用于指向列表中列表项的下一个列表项和上一个列表项 。

3、迷你列表项只用于标记列表的末尾和挂载其他插入列表中的列表项,因此不需要成员变量 pxOwner 和 pxContainer,以节省内存开销。

我们来细细品鉴一下什么叫做“挂载其他插入列表的列表项”:

我们的一个列表,他一开始初始化的时候,只有三个成员变量(那两个常量不算),一个是uxNumberOfItems(列表中的列表项数目)、一个是*pxIndex(用于遍历列表项的指针)、一个是xListEnd(末尾列表项,即迷你列表项),那我们想要插入一个列表项,该怎么插入?那就是通过迷你列表项中的列表项指向想要插入的列表项,想要插入的列表的下一个项要指向迷你列表项中的列表项。

再看一下我们一开始的图,是否就有更好的理解的呢?

列表和列表项的关系

1、列表刚开始是这样的:

分别是列表项分别是 列表项的数目、指向某一个列表项和末尾列表项。

 

2、我们初始化列表后,会变成这样:

当然,初始化列表的函数我们还没有介绍,我们先看这一个示意图,可以看到,列表项的数目是0,代表末尾列表项不计入列表项数目中;*pxIndex是指向某个列表项,但现在列表中只有末尾列表项,所以是指向末尾列表项。我们再看末尾列表项,可以看到它关于列表项的值是最大的,这可以保证他是挂载到最后,一直充当末尾列表项。然后我们也可以看到,末尾列表项中的上一项和下一项都是指向他自己的,因为是刚初始化,还没有其他列表。

3、让我们插入两个列表项,分别是列表项1、列表项2:

列表相关API函数

函数

描述
vListInitialise() 初始化列表,列表的初始状态通过其实现。
vListInitialiseItem() 初始化列表项,其实就是对列表和列表项各自成员赋初始值。
vListInsertEnd() 列表末尾插入列表项(指定插入法)
vListInsert() 列表插入列表项(升序插入法)

uxListRemove()

列表移除列表项

 

我们来看初始化列表vListInitialise() 函数的内部实现:

形参 描述
pxList 待初始化列表
void vListInitialise(List_t * const pxList)
{	
//初始化时,列表中只有 xListEnd,因此 pxIndex 指向 xListEnd 。
	pxList->pxIndex = ( ListItem_t * ) &( pxList->xListEnd );
//xListEnd 的值初始化为最大值,用于列表项升序排序时,排在最后。
	pxList->xListEnd.xItemValue = portMAX_DELAY;
//初始化时,列表中只有 xListEnd,因此上一个和下一个列表项都为xListEnd 本身。
	pxList->xListEnd.pxNext = ( ListItem_t * ) &( pxList->xListEnd );
        pxList->xListEnd.pxPrevious = ( ListItem_t * ) &( pxList->xListEnd );	
//初始化时,列表中的列表项数量为 0(不包含 xListEnd)
	pxList->uxNumberOfItems = ( UBaseType_t ) 0U;	
//初始化用于检测列表数据完整性的校验,我们没用这两个代码,不用管
	listSET_LIST_INTEGRITY_CHECK_1_VALUE( pxList );	 
        listSET_LIST_INTEGRITY_CHECK_2_VALUE( pxList );
} 

我们来看初始化列表项vListInitialiseItem()  函数的内部实现:

形参 描述
pxItem 待初始化列表项
void vListInitialiseItem( ListItem_t * const pxItem )
{		
//初始化时,列表项所在列表设为空
pxItem->pxContainer = NULL;
//初始化用于检测列表项数据完整性的校验值
listSET_FIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE( pxItem );	
listSET_SECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE( pxItem );
} 

列表项目的升序插入vListInsert()函数的内部实现:

形参 描述
pxList 列表
pxNewListItem 待插入列表项
void vListInsert( List_t * const pxList, ListItem_t * const pxNewListItem ) 
{
/*创建一个指针,用于后续操作*/
 	ListItem_t * pxIterator; 

/* 获取列表项的数值依据数值升序排列 */
	const TickType_t  xValueOfInsertion  =  pxNewListItem->xItemValue;

/* 检查参数是否正确,无需关心这两行 */ 	
	listTEST_LIST_INTEGRITY( pxList ); 				
	listTEST_LIST_ITEM_INTEGRITY( pxNewListItem ); 	

 /* 如果待插入列表项的值为最大值 */ 			
	if( xValueOfInsertion == portMAX_DELAY )
 	{ 
                /* 插入的位置为列表 xListEnd 前面 */ 
		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->pxContainer = pxList; 	
	
/* 更新列表中列表项的数量 */ 			
	( pxList->uxNumberOfItems )++;						
}

 

列表项目的无序(指定)插入vListInsert()函数的内部实现:

       形参

描述
       pxList 列表

pxNewListItem

待插入列表项

void vListInsertEnd (  List_t * const pxList ,   ListItem_t * const pxNewListItem  )
{
	 省略部分非关键代码 … …
/* 获取列表 pxIndex 指向的列表项 */	
ListItem_t * const pxIndex = pxList->pxIndex;	
/* 更新待插入列表项的指针成员变量 */
pxNewListItem->pxNext = pxIndex;
pxNewListItem->pxPrevious = pxIndex->pxPrevious;

/* 更新列表中原本列表项的指针成员变量 */
pxIndex->pxPrevious->pxNext = pxNewListItem;
pxIndex->pxPrevious = pxNewListItem;
/* 更新待插入列表项的所在列表成员变量 */
pxNewListItem->pxContainer = pxList;
/* 更新列表中列表项的数量 */
( pxList->uxNumberOfItems )++;
} 

此函数用于将待插入列表的列表项插入到列表 pxIndex 指针指向的列表项前面,是一种无序的插入方法 。

列表项目的移除列表项uxListRemove()函数的内部实现:

形参

描述

pxItemToRemove 待移除的列表项,这个列表项是一个结构体,成员里面存储着它所属列表

返回值

描述

整数 待移除列表项移除后,所在列表剩余列表项的数量
UBaseType_t uxListRemove( ListItem_t * const pxItemToRemove ) 
{
	List_t * const pxList = pxItemToRemove->pxContainer; 
/* 从列表中移除列表项 */ 
	pxItemToRemove->pxNext->pxPrevious = pxItemToRemove->pxPrevious;
	pxItemToRemove->pxPrevious->pxNext = pxItemToRemove->pxNext; 	

 /*如果 pxIndex 正指向待移除的列表项 */ 
	if( pxList->pxIndex == pxItemToRemove ) 
	{
                 /*pxIndex 指向上一个列表项*/ 
	 	pxList->pxIndex = pxItemToRemove->pxPrevious;
	} else 
	{ 
		mtCOVERAGE_TEST_MARKER(); 
	} 
/*将待移除的列表项的所在列表指针清空*/ 
	pxItemToRemove->pxContainer = NULL; 
/*更新列表中列表项的数量*/ 
	( pxList->uxNumberOfItems )--; 
/*返回移除后的列表中列表项的数量*/ 
	return pxList->uxNumberOfItems; 
}

其实看完上述代码,其实不难发现,这些关于列表的函数其实就是简单的双向循环链表的增删查改。

列表项的插入和删除实现

1、实验目的:学会对FreeRTOS 列表和列表项的操作函数使用,并观察运行结果和理论分析是否一致。

2、实验设计:将设计三个任务:start_task、task1、task2。

三个任务的功能如下:

start_task:用来创建其他的2个任务。

task1:实现LED0每500ms闪烁一次,用来提示系统正在运行。

task2:调用列表和列表项相关API函数,并且通过串口输出相应的信息,进行观察 。

List_t          TestList;           /* 定义测试列表 */
ListItem_t      ListItem1;          /* 定义测试列表项1 */
ListItem_t      ListItem2;          /* 定义测试列表项2 */
ListItem_t      ListItem3;          /* 定义测试列表项3 */
//任务2:调用列表和列表项相关API函数,并且通过串口输出相应的信息,进行观察 。
void task2(void *pvParameters)
{
	
	  vListInitialise(&TestList);         /* 初始化列表 */
    vListInitialiseItem(&ListItem1);    /* 初始化列表项1 */
    vListInitialiseItem(&ListItem2);    /* 初始化列表项2 */
    vListInitialiseItem(&ListItem3);    /* 初始化列表项3 */
    ListItem1.xItemValue = 40;
    ListItem2.xItemValue = 60;
    ListItem3.xItemValue = 50;

    /* 第二步:打印列表和其他列表项的地址 */
    printf("/**************第二步:打印列表和列表项的地址**************/\r\n");
    printf("项目\t\t\t地址\r\n");
    printf("TestList\t\t0x%p\t\r\n", &TestList);
    printf("TestList->pxIndex\t0x%p\t\r\n", TestList.pxIndex);
    printf("TestList->xListEnd\t0x%p\t\r\n", (&TestList.xListEnd));
    printf("ListItem1\t\t0x%p\t\r\n", &ListItem1);
    printf("ListItem2\t\t0x%p\t\r\n", &ListItem2);
    printf("ListItem3\t\t0x%p\t\r\n", &ListItem3);
    printf("/**************************结束***************************/\r\n");
    
    printf("\r\n/*****************第三步:列表项1插入列表******************/\r\n");
    vListInsert((List_t*    )&TestList,         /* 列表 */
                (ListItem_t*)&ListItem1);       /* 列表项 */
    printf("项目\t\t\t\t地址\r\n");
    printf("TestList->xListEnd->pxNext\t0x%p\r\n", (TestList.xListEnd.pxNext));
    printf("ListItem1->pxNext\t\t0x%p\r\n", (ListItem1.pxNext));
    printf("TestList->xListEnd->pxPrevious\t0x%p\r\n", (TestList.xListEnd.pxPrevious));
    printf("ListItem1->pxPrevious\t\t0x%p\r\n", (ListItem1.pxPrevious));
    printf("/**************************结束***************************/\r\n");
    
    /* 第四步:列表项2插入列表 */
    printf("\r\n/*****************第四步:列表项2插入列表******************/\r\n");
    vListInsert((List_t*    )&TestList,         /* 列表 */
                (ListItem_t*)&ListItem2);       /* 列表项 */
    printf("项目\t\t\t\t地址\r\n");
    printf("TestList->xListEnd->pxNext\t0x%p\r\n", (TestList.xListEnd.pxNext));
    printf("ListItem1->pxNext\t\t0x%p\r\n", (ListItem1.pxNext));
    printf("ListItem2->pxNext\t\t0x%p\r\n", (ListItem2.pxNext));
    printf("TestList->xListEnd->pxPrevious\t0x%p\r\n", (TestList.xListEnd.pxPrevious));
    printf("ListItem1->pxPrevious\t\t0x%p\r\n", (ListItem1.pxPrevious));
    printf("ListItem2->pxPrevious\t\t0x%p\r\n", (ListItem2.pxPrevious));
    printf("/**************************结束***************************/\r\n");
    
    /* 第五步:列表项3插入列表 */
    printf("\r\n/*****************第五步:列表项3插入列表******************/\r\n");
    vListInsert((List_t*    )&TestList,         /* 列表 */
                (ListItem_t*)&ListItem3);       /* 列表项 */
    printf("项目\t\t\t\t地址\r\n");
    printf("TestList->xListEnd->pxNext\t0x%p\r\n", (TestList.xListEnd.pxNext));
    printf("ListItem1->pxNext\t\t0x%p\r\n", (ListItem1.pxNext));
    printf("ListItem2->pxNext\t\t0x%p\r\n", (ListItem2.pxNext));
    printf("ListItem3->pxNext\t\t0x%p\r\n", (ListItem3.pxNext));
    printf("TestList->xListEnd->pxPrevious\t0x%p\r\n", (TestList.xListEnd.pxPrevious));
    printf("ListItem1->pxPrevious\t\t0x%p\r\n", (ListItem1.pxPrevious));
    printf("ListItem2->pxPrevious\t\t0x%p\r\n", (ListItem2.pxPrevious));
    printf("ListItem3->pxPrevious\t\t0x%p\r\n", (ListItem3.pxPrevious));
    printf("/**************************结束***************************/\r\n");
    
    /* 第六步:移除列表项2 */
    printf("\r\n/*******************第六步:移除列表项2********************/\r\n");
    uxListRemove((ListItem_t*   )&ListItem2);   /* 移除列表项 */
    printf("项目\t\t\t\t地址\r\n");
    printf("TestList->xListEnd->pxNext\t0x%p\r\n", (TestList.xListEnd.pxNext));
    printf("ListItem1->pxNext\t\t0x%p\r\n", (ListItem1.pxNext));
    printf("ListItem3->pxNext\t\t0x%p\r\n", (ListItem3.pxNext));
    printf("TestList->xListEnd->pxPrevious\t0x%p\r\n", (TestList.xListEnd.pxPrevious));
    printf("ListItem1->pxPrevious\t\t0x%p\r\n", (ListItem1.pxPrevious));
    printf("ListItem3->pxPrevious\t\t0x%p\r\n", (ListItem3.pxPrevious));
    printf("/**************************结束***************************/\r\n");
    
    /* 第七步:列表末尾添加列表项2 */
    printf("\r\n/****************第七步:列表末尾添加列表项2****************/\r\n");
    TestList.pxIndex = &ListItem1;
    vListInsertEnd((List_t*     )&TestList,     /* 列表 */
                   (ListItem_t* )&ListItem2);   /* 列表项 */
    printf("项目\t\t\t\t地址\r\n");
    printf("TestList->pxIndex\t\t0x%p\r\n", TestList.pxIndex);
    printf("TestList->xListEnd->pxNext\t0x%p\r\n", (TestList.xListEnd.pxNext));
    printf("ListItem1->pxNext\t\t0x%p\r\n", (ListItem1.pxNext));
    printf("ListItem2->pxNext\t\t0x%p\r\n", (ListItem2.pxNext));
    printf("ListItem3->pxNext\t\t0x%p\r\n", (ListItem3.pxNext));
    printf("TestList->xListEnd->pxPrevious\t0x%p\r\n", (TestList.xListEnd.pxPrevious));
    printf("ListItem1->pxPrevious\t\t0x%p\r\n", (ListItem1.pxPrevious));
    printf("ListItem2->pxPrevious\t\t0x%p\r\n", (ListItem2.pxPrevious));
    printf("ListItem3->pxPrevious\t\t0x%p\r\n", (ListItem3.pxPrevious));
    printf("/************************实验结束***************************/\r\n");
		
	while(1)
	{
		printf("任务2运行!\r\n");
		vTaskDelay(1000);
	}
	
}

通过观察串口助手的数据,能够和我们上面所学的理论知识一一对应,我们对列表和列表项相比也是掌握的比较好了。