FreeRTOS学习笔记——链表
一、链表(List)
在c语言中,链表是一种比较基础的数据结构,在操作系统中十分常见。链表主要由节点组成,节点和节点之间相互连接构成一个首尾相连的链表。链表是一种数据集合,不同于数组,其内部可用于储存多种不同类型的数据。链表分为单向链表和双向链表,其中,单向链表类似于单行列车,只能沿一个方向行驶,也就是说每一个节点只知道下一个节点,不能直接返回上一个节点,也就意味着单向链表只能一个方向遍历;双向链表类似于双轨列车,可以在两个方向上行驶,也就是说每一个节点可以知道下一个节点的同时,也可以知道上一个节点,使得双向链表可以在两个方向上实现遍历。道鉴于FreeRTOS使用的是双向链表,这里直接从双向链表入手。单向链表和双向链表区别如下图示。

二、链表节点(ListItem)
节点是链表的基本单位,链表通过节点将信息有序的串联在一起。可以把链表理解成一本书,那么每一个节点就是组成这本书的每一页。每一页包含了一些信息,并且知道下一页的地址(对于双向链表来说,每个节点不仅包含了上一个节点的地址信息,同时也包含了下一个节点的地址信息),并通过这种地址将每一页连接起来,形成这本书的结构。
对于节点的理解不应过于复杂,可以抽象理解成两部分——数据域和指针域。如下图示。

此处,节点的数据域主要用来记录当前节点的辅助值,这个辅助值是为了方便后续将节点按升序插入到链表中。指针域主要包括前驱指针、后继指针和两个通用指针,其中,前驱指针和后继指针用于记录前一个结点和后一个节点的地址信息。两个通用指针用于记录该节点所在的链表以及拥有该节点的内核对象(通常为TCB,TaskControlBlock,任务章节会有详细介绍)。了解了这些代码就很容易实现,代码如下示。
/* 链表节点数据结构定义 */
struct xLIST_ITEM{
/* 数据域 */
TickType_t xItemValue; /* 辅助值 用于帮助节点做顺序排序 */
/* 指针域 */
struct xLIST_ITEM * pxNext; /* 后继指针 */
struct xLIST_ITEM * pxPrevious; /* 前驱指针 */
void * pvOwner; /* 指向拥有该节点的内核对象 通常是TCB(Task Control Block) */
void * pvContainer; /* 指向该节点所在的链表 */
};
/* 链表节点重定义 */
typedef struct xLIST_ITEM ListItem_t;
2.1节点初始化
节点的初始化就是该节点不指向任何链表,即表示该节点没有插入到任何链表中,为后续的插入或操作做好准备。对于节点的其他成员,在后续实际使用中再赋值。代码如下示。
/* 链表节点初始化 */
void vListInitialiseItem(ListItem_t * const pxItem)
{
/* 初始化该节点所在的链表为空 表示该节点没有插入任何链表 */
pxItem->pvContainer = NULL;
}
三、链表根节点(ListRootNode)
在链表中虽然没有特定的根节点,但是在链表的实际应用中,根节点通常是链表的起始点,作为一个访问链表的基准点,可以从根节点遍历整个链表或者回到链表的其他部分。如果把双向链表理解成一个街道网络,那么链表上的每个节点就是街道的交汇点,而根节点则是整个网络中的某个关键位置或者起点。
在根结点中,根节点数据域包含两部分,一部分用来记录当前链表下有多少个节点(其中根节点不在计数范围);另一部分用来记录当前链表的最后一个节点信息。指针域包含一个节点索引指针,用于遍历整个链表,方便后续节点的插入和删除(因包含虚拟节点,故无需判断链表是否为空)。了解了这些代码就很容易实现,代码如下示。
/* 实现链表根节点 */
typedef struct xLIST{
/* 数据域 */
UBaseType_t uxNumberOfItems; /* 链表节点计数器 用于表示该链表有多少个节点 根节点不在计数范围 */
MiniListItem_t xListEnd; /* 链表最后一个节点 */
/* 指针域 */
ListItem_t * pxIndex; /* 节点索引指针 用于遍历整个链表 */
}List_t;
由于链表是一个首尾相连的结构,首也是尾,尾也是首,为了方便后续的节点操作,从链表中精简出一个节点作为整个链表的最后一个节点(ListMiniItem)。代码如下示。
struct xMINI_LIST_ITEM{
/* 数据域 */
TickType_t xItemValue; /* 辅助值 用于帮助节点做顺序排序 */
/* 指针域 */
struct xLIST_ITEM * pxNext; /* 前驱指针 */
struct xLIST_ITEM * pxPrevious; /* 后继指针 */
};
/* 链表节点重定义 */
typedef struct xMINI_LIST_ITEM MiniListItem_t;
3.1链表根节点初始化
为确保链表在创建时是正确配置的,可以安全的进行节点的遍历、插入、删除,对链表进行初始化配置。具体步骤如下图示:

1.1节点计数器初始化
/* 节点计数器设置为0(双向链表中 根节点不在计数范围) */
pxList->uxNumberOfItems = (UBaseType_t)0UL;
此处将节点计数器的值设置为0,即表示当前链表不含任何节点。
1.2设置根节点辅助值
/* 为确保根节点为链表最后一个节点 设置根节点辅助值为portMAX_DELAY */
pxList->xListEnd.xItemValue = (portMAX_DELAY); // 方便后续节点升序插入链表
此处将链表的最后一个节点的辅助值设置为最大值(portMAX_DELAY),确保该节点为链表的最后一个节点。
1.3设置索引指针
/* 将索引指针指向根节点 */
pxList->pxIndex = (ListItem_t *)(&(pxList->xListEnd));
此处将链表的索引指针指向链表的最后一个节点,方便后续的节点操作,确保索引指针指向链表的起始位置。
1.4配置根节点的双向指针
/* 根据双向链表特性设置根节点前驱和后继指针指向自己 */
pxList->xListEnd.pxPrevious = (ListItem_t *)(&(pxList->xListEnd));
pxList->xListEnd.pxNext = (ListItem_t *)(&(pxList->xListEnd));
此处设置根节点的前驱和后继指针指向节点本身,表示该链表是唯一一个节点。完整代码如下示。
/* 链表初始化 */
void vListInitialise(List_t *pxList)
{
/* 节点计数器设置为0(双向链表中 根节点不在计数范围) */
pxList->uxNumberOfItems = (UBaseType_t)0UL;
/* 为确保根节点为链表最后一个节点 设置根节点辅助值为portMAX_DELAY */
pxList->xListEnd.xItemValue = (portMAX_DELAY); // 方便后续节点升序插入链表
/* 将索引指针指向根节点 */
pxList->pxIndex = (ListItem_t *)(&(pxList->xListEnd));
/* 根据双向链表特性设置根节点前驱和后继指针指向自己 */
pxList->xListEnd.pxPrevious = (ListItem_t *)(&(pxList->xListEnd));
pxList->xListEnd.pxNext = (ListItem_t *)(&(pxList->xListEnd));
}
3.2链表的插入
2.1头插法
使用头插法将新节点插入链表时刻保证根节点为最后一个节点,操作起来也比较容易,具体步骤如下图示:

1.1更新新节点的前驱和后继指针
根据上图理解,代码如下示。
/* 更新新节点的指针域 */
pxNewListItem->pxNext = pxIndex;
pxNewListItem->pxPrevious = pxIndex->pxPrevious;
1.2更新根节点的前驱和后继指针
根据上图理解,代码如下示。
/* 更新链表根节点的指针域 */
pxIndex->pxPrevious->pxNext = pxNewListItem;
pxIndex->pxPrevious = pxNewListItem;
1.3更新链表节点计数器
根据上图理解,代码如下示。
/* 更新当前链表的节点计数器的值 */
(pxList->uxNumberOfItems)++;
1.4更新新节点所在链表信息
根据上图理解,代码如下示。
/* 更新当前节点所在的链表信息 */
pxNewListItem->pvContainer = (void *)pxList;
完整代码如下示。
/* 将新节点插入到链表中(头插法) */
void vListInsertEnd(List_t * const pxList,ListItem_t * const pxNewListItem)
{
/* 由于该链表包含虚拟节点 故在插入链表的过程中 无需判断链表是否为空 */
ListItem_t * pxIndex = pxList->pxIndex;
/* 更新新节点的指针域 */
pxNewListItem->pxNext = pxIndex;
pxNewListItem->pxPrevious = pxIndex->pxPrevious;
/* 更新链表根节点的指针域 */
pxIndex->pxPrevious->pxNext = pxNewListItem;
pxIndex->pxPrevious = pxNewListItem;
/* 更新当前链表的节点计数器的值 */
(pxList->uxNumberOfItems)++;
/* 更新当前节点所在的链表信息 */
pxNewListItem->pvContainer = (void *)pxList;
}
2.2升序插入
升序插入是为了是链表变得有序,相对有无序链表来讲,有序链表可以使用二分查找等高效算法快速找到特定的节点或区间,这比在无序链表中线性查找要快得多。在有序链表中插入和删除节点时,通常只需要在特定位置插入或删除,这减少了查找插入位置的复杂度。对于动态调整顺序的操作,虽然插入和删除节点本身可能是O(n)的复杂度,但整体操作的逻辑和管理是更简单的。在需要处理具有优先级的任务时(如任务调度或事件处理),保持链表有序可以让高优先级的任务总是位于链表的前端,使得处理任务的效率更高。具体步骤如下图示:
插入前的链表:

插入后的链表:

2.1遍历链表寻找新节点插入位置
根据上图理解,代码如下示。
/* 定义节点迭代指针 用于确定新节点插入位置 */
ListItem_t * pxInterator;
/* 获取新节点的辅助值 */
const TickType_t xValueOfInsertion = pxNewListItem->xItemValue;
/* 根据新节点辅助值 寻找插入位置 */
if(xValueOfInsertion == portMAX_DELAY)
pxIterator = pxList->xListEnd.pxPrevious; // 如果新节点的辅助值和根节点的辅助值相同,则将新节点插入根节点前一个节点位置
else
{
/* 遍历整个链表 */
for(pxInterator = (ListItem_t *)(&(pxList->xListEnd));
pxInterator->pxNext->xItemValue <= xValueOfInsertion;
pxInterator = pxInterator->pxNext);
}
2.2更新新节点的指针域
根据上图理解,代码如下示。
/* 更新新节点的指针域 */
pxNewListItem->pxNext = pxInterator->pxNext;
pxNewListItem->pxPrevious = pxInterator;
2.3更新原节点的指针域
根据上图理解,代码如下示。
/* 更新新节点的下一个节点的指针域 */
pxNewListItem->pxNext->pxPrevious = pxNewListItem;
/* 更新新节点的上一个节点的指针域 */
pxInterator->pxNext = pxNewListItem;
2.4更新链表节点计数器
根据上图理解,代码如下示。
/* 更新当前链表的节点计数器的值 */
(pxList->uxNumberOfItems)++;
2.5更新新节点所在链表信息
根据上图理解,代码如下示。
/* 更新新节点所在的链表信息 */
pxNewListItem->pvContainer = (void *)pxList;
完整代码如下示。
/* 将新节点插入到列表(升序排列) */
void vListInsert(List_t * const pxList,ListItem_t * const pxNewListItem)
{
/* 定义节点迭代指针 用于确定新节点插入位置 */
ListItem_t * pxInterator;
/* 获取新节点的辅助值 */
const TickType_t xValueOfInsertion = pxNewListItem->xItemValue;
/* 根据新节点辅助值 寻找插入位置 */
if(xValueOfInsertion == pxList->xListEnd.xItemValue)
pxInterator = pxList->pxIndex;
else
{
/* 便利整个链表 */
for(pxInterator = (ListItem_t *)(&(pxList->xListEnd));
pxInterator->pxNext->xItemValue <= xValueOfInsertion;
pxInterator = pxInterator->pxNext);
}
/* 更新新节点的指针域 */
pxNewListItem->pxNext = pxInterator->pxNext;
pxNewListItem->pxPrevious = pxInterator;
/* 更新新节点的系一个节点的指针域 */
pxNewListItem->pxNext->pxPrevious = pxNewListItem;
/* 更新新节点的上一个节点的指针域 */
pxInterator->pxNext = pxNewListItem;
/* 更新当前链表的节点计数器的值 */
(pxList->uxNumberOfItems)++;
/* 更新新节点所在的链表信息 */
pxNewListItem->pvContainer = (void *)pxList;
}
3.3链表的删除
链表节点的删除可以理解成插入的逆过程,具体步骤如下图示:
删除前:

删除后:

3.1更新原节点指针域
/* 更新节点前一个节点的后继指针 */
pxItemToRemove->pxPrevious->pxNext = pxItemToRemove->pxNext;
/* 更新节点后一个节点的前驱指针 */
pxItemToRemove->pxNext->pxPrevious = pxItemToRemove->pxPrevious;
3.2更新链表节点计数器
/* 更新链表节点计数器值 */
(pxList->uxNumberOfItems)--;
3.3更新删除节点链表信息
/* 更新节点所在的链表信息 */
pxItemToRemove->pvContainer = NULL; // 表示当前节点没有插入任何链表
3.4更新链表索引指针
/* 调整链表的索引指针 */
if(pxList->pxIndex == pxItemToRemove)
pxList->pxIndex = pxItemToRemove->pxPrevious;
完整代码如下示。
/* 将节点从链表中删除 */
UBaseType_t vListRemove(ListItem_t * const pxItemToRemove)
{
/* 获取节点所在的链表 */
List_t * const pxList = pxItemToRemove->pvContainer;
/* 更新节点前一个节点的后继指针 */
pxItemToRemove->pxPrevious->pxNext = pxItemToRemove->pxNext;
/* 更新节点后一个节点的前驱指针 */
pxItemToRemove->pxNext->pxPrevious = pxItemToRemove->pxPrevious;
/* 更新链表节点计数器值 */
(pxList->uxNumberOfItems)--;
/* 更新节点所在的链表信息 */
pxItemToRemove->pvContainer = NULL; // 表示当前节点没有插入任何链表
/* 调整链表的索引指针 */
if(pxList->pxIndex == pxItemToRemove)
pxList->pxIndex = pxItemToRemove->pxPrevious;
/* 返回当前列表的节点数 */
return (pxList->uxNumberOfItems);
}
3.4带参数的宏定义
在FreeRTOS中,使用了大量的宏定义,对代码精简有很大的帮助,链表相关带参宏定义如下示。
/*************************************** 节点部分 *************************************************/
/* 设置节点的拥有者 */
#define listSET_LIST_ITEM_OWNER(pxListem,pxOwner)\
(pxListem->pvOwner = (void *)pxOwner)
/* 获取节点拥有者 */
#define listGET_LIST_ITEM_OWNER(pxListem)\
(pxListem->pvOwner)
/* 设置节点辅助排序值 */
#define listSet_LIST_ITEM_VALUE(pxListem,xValue)\
((pxListem->xItemValue) = xValue)
/* 获取节点辅助排序值 */
#define listGET_LIST_ITEM_VALUE(pxListem)\
(pxListem->xItemValue)
/* 获取节点的下一节点 */
#define listGET_LIST_ITEM_NEXT(pxListem)\
(pxListem->pxNext)
/*************************************** 链表部分 *************************************************/
/* 判断链表是否为空链表 */
#define list_LIST_IS_EMPTY(pxList)\
(if(pxList->uxNumberOfItems == (UBaseType_t)0U))
/* 获取链表的节点数(链表长度) */
#define listCURRENT_LIST_LENTH(pxList)\
(pxList->uxNumberOfItems)
/* 获取链表最后一个节点*/
#define listGET_END_MARKER(pxList)\
((ListItem_t const *)(&(pxList->xListEnd)))
/* 获取链表根节点的辅助排序值 */
#define listGET_ITEM_VALUE_OF_HEAD_ENTRY(pxList)\
(((pxList->xListEnd).pxNext)->xItemValue)
/* 获取链表的入口节点 */
#define listGET_HEAD_ENTRY(pxList)\
((pxList->xListEnd).pxNext)
四、实验验证
最后,定义三个节点并将三个节点按照升序插入到同一链表中,main.c如下所示:
#include "FreeRTOS.h"
#include "list.h"
/************************************* 函数声明区 ******************************************/
/************************************* 变量定义区 ******************************************/
/* 定义一个链表 */
List_t xListTest;
/* 定义三个节点 */
ListItem_t xItem1;
ListItem_t xItem2;
ListItem_t xItem3;
/*************************************** 主函数 ********************************************/
int main(void)
{
/* 初始化链表 */
vListInitialise(&xListTest);
/* 初始化节点 */
vListInitialiseItem(&xItem1);
vListInitialiseItem(&xItem2);
vListInitialiseItem(&xItem2);
/* 设置节点辅助值 */
xItem1.xItemValue = 1;
xItem2.xItemValue = 2;
xItem3.xItemValue = 3;
/* 将节点插入链表 */
vListInsert(&xListTest,&xItem3);
vListInsert(&xListTest,&xItem2);
vListInsert(&xListTest,&xItem1);
}
打开软件调试,将xItem1、xItem2、xItem3和xListTest加入观察窗口,全速运行,观察结果如下图示:
观察后继指针结果:

观察前驱指针结果:

实验结果与预期相同,说明代码实现没有什么问题。
528

被折叠的 条评论
为什么被折叠?



