FreeRTOS列表和列表项
今天继续跟着正点原子学习FreeRTOS列表和列表项的内容。列表和列表项这个知识点用到了C语言链表的知识点。所以必须对C语言中的链表这个数据结构才能更好的理解这部分内容。TIPS:正点原子这节课内容讲的特别好,强烈推荐:第20讲列表和列表项简介
什么是列表和列表项
列表就是FreeRTOS中的数据结构,它被用来跟踪FreeRTOS中的任务。主要的列表包括:
-
就绪列表(Ready Lists):对于每个优先级,FreeRTOS都维护一个就绪列表。当任务处于就绪状态,能够运行,但由于CPU正被其他任务使用而无法立即执行时,该任务会被放入相应优先级的就绪列表中。当CPU可用时,调度器会从最高优先级的就绪列表中选择任务来执行。
-
阻塞列表(Blocked Lists):当任务等待某个事件(如信号量、互斥量或定时器)时,它会被放入阻塞列表。阻塞列表用于跟踪哪些任务正在等待资源变得可用。当等待的事件发生时,任务可以从阻塞列表移回就绪列表。
-
挂起列表(Suspended List):当任务被显式挂起(例如,调用了
vTaskSuspend
函数)时,它会被放入挂起列表。挂起的任务不会被调度器考虑运行,直到它们被显式地恢复(例如,调用了vTaskResume
)。 -
延时列表(Delay Lists):当任务需要延迟执行或等待一段时间时,它会被放入延时列表。这通常用于实现非阻塞延时或定时功能。延时列表实际上分为两个:一个用于短期延时(当前延时列表),另一个用于长期延时(溢出延时列表)。这种分法帮助管理时间的回绕问题。
-
终止列表(Terminated List):某些FreeRTOS配置允许已删除或已结束的任务被放入终止列表,直到它们的任务控制块(TCB)被回收。这不是所有FreeRTOS配置都支持的功能。
每个列表都是通过链表实现的,每个链表节点都包含了指向任务控制块(Task Control Block, TCB)的指针。TCB是FreeRTOS用来存储任务状态、堆栈指针和其他必要信息的结构。
FreeRTOS的调度器会根据这些列表及其内任务的状态和优先级来决定哪个任务应该获得CPU时间。通过这种方式,FreeRTOS实现了任务的优先级调度和时间共享调度。与列表有关的内容都在list.c和list.h中。在list.h中定义一个叫做List_t的结构体。下面我们来看一下:
typedef struct xLIST
{listFIRST_LIST_INTEGRITY_CHECK_VALUE /*< Set to a known value if configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES is set to 1. */volatile UBaseType_t uxNumberOfItems;ListItem_t * configLIST_VOLATILE pxIndex; /*< Used to walk through the list. Points to the last item returned by a call to listGET_OWNER_OF_NEXT_ENTRY (). */MiniListItem_t xListEnd; /*< List item that contains the maximum possible item value meaning it is always at the end of the list and is therefore used as a marker. */listSECOND_LIST_INTEGRITY_CHECK_VALUE /*< Set to a known value if configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES is set to 1. */
} List_t;
在FreeRTOS中,List_t
结构体是用于实现各种任务列表(如就绪列表、阻塞列表等)的核心数据结构。下面是List_t
结构体各个成员的作用解释:
-
listFIRST_LIST_INTEGRITY_CHECK_VALUE:这是一个可选的完整性检查值,仅在
configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES
设置为1时使用。它的目的是帮助开发者检测内存损坏或不正确的列表操作。这个值通常被设置为一个特定的常量,以便在运行时检查列表的完整性。 -
volatile UBaseType_t uxNumberOfItems:这个成员变量用来记录列表中项目的数量。它是
volatile
类型的,因为它可能会在中断服务例程和普通任务代码之间共享,确保编译器在每次访问时都会从内存中重新读取其值,而不是使用可能已经过时的寄存器副本。 -
ListItem_t * configLIST_VOLATILE pxIndex:这是一个指向列表中某个项目的指针,用作遍历列表的索引。
configLIST_VOLATILE
是一个宏,用于确保在特定的编译器或配置下,pxIndex
以正确的方式被视为volatile
。pxIndex
通常用于通过listGET_OWNER_OF_NEXT_ENTRY()
宏来遍历列表,每次调用都会返回列表中的下一个元素。 -
MiniListItem_t xListEnd:这个成员是列表的一个哨兵(或标记)元素,表示列表的末尾。
xListEnd
的xItemValue
通常被设置为最大可能的值,以确保它总是位于列表的末尾。这个哨兵元素使得列表操作(如插入和删除)可以在不需要特殊情况处理的情况下进行,因为列表始终至少包含一个元素(即xListEnd
)。 -
listSECOND_LIST_INTEGRITY_CHECK_VALUE:这是第二个可选的完整性检查值,其作用与
listFIRST_LIST_INTEGRITY_CHECK_VALUE
相同,提供额外的完整性检查。这也只有在configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES
设置为1时才会启用。
至于第四点为啥要有一个哨兵元素,如果有链表操作知识的基础,在没有哨兵元素的链表中,插入和删除操作需要检查列表是否为空,以及操作是否发生在列表的开头或末尾。这增加了实现的复杂性。使用哨兵元素后,这些特殊情况的处理被统一化,因为列表永远不会为空(至少有一个哨兵元素),且不需要单独处理列表的末尾。统一插入和删除操作:哨兵元素确保每个插入和删除操作都有一个统一的前驱节点和后继节点,无论操作发生在列表的哪个位置。这意味着代码可以使用相同的逻辑来处理所有情况。
所以列表中忽略那两个检查元素,所有的具体的成员如上图所示。
接下来让我们来看下列表项和迷你列表项。其中迷你列表项就是xlistend,也就是列表标记的哨兵元素。首先来看下列表项和迷你列表项所拥有成员的差异,具体差异如下图所示。
可以看出迷你列表项只有三个成员。xMINI_LIST_ITEM
结构体是FreeRTOS中用来表示列表中的一个元素或节点的数据结构。它是ListItem_t
的简化版本,通常用于实现哨兵元素(如xListEnd
),以简化列表操作。下面是xMINI_LIST_ITEM
结构体每个成员的详细解释:
. configLIST_VOLATILE TickType_t xItemValue:这个成员变量用于存储元素的排序值或优先级。TickType_t
通常是一个无符号整型,用于表示时间或者计数。在FreeRTOS中,列表经常根据这个值进行排序,例如,用于管理延时任务的列表就是按照xItemValue
的值(表示唤醒时间)进行排序的。configLIST_VOLATILE
是一个宏,用于确保在特定的编译器或配置下,xItemValue
以正确的方式被视为volatile
。这是因为xItemValue
可能会在中断服务例程中被修改。
struct xLIST_ITEM * configLIST_VOLATILE pxNext:这是一个指向列表中下一个元素的指针。它使得列表可以以链表的形式进行遍历。configLIST_VOLATILE
确保pxNext
在需要时被正确处理为volatile
,以适应可能的并发修改。
struct xLIST_ITEM * configLIST_VOLATILE pxPrevious:这是一个指向列表中前一个元素的指针。它的存在使得列表成为一个双向链表,允许从任一方向遍历列表。这对于某些操作,如从列表中删除一个元素,是非常有用的,因为可以直接访问前一个元素,从而简化了指针的重新链接过程。同样,configLIST_VOLATILE
确保在特定配置下,pxPrevious
被正确地视为volatile
。
至于为啥非要整出来一个迷你列表项,而不直接全部都用列表项。这可能会让人疑惑它们之间的区别以及为什么需要两种不同的结构体。主要区别通常在于它们的用途和设计意图,而不仅仅是结构体成员本身。下面是创建迷你列表项的原因。
1. 简化和优化
xMINI_LIST_ITEM
是ListItem_t
的简化版本。虽然它们的成员变量可能相似,xMINI_LIST_ITEM
通常用于特定场景,如作为哨兵节点(xListEnd
)或在不需要完整ListItem_t
功能的场合。这种简化有助于减少内存占用和提高代码效率,尤其是在资源受限的嵌入式系统中。
2. 特定用途
xMINI_LIST_ITEM
通常用于实现列表的哨兵节点,这是一个始终存在于列表末尾的特殊节点,用以简化列表操作逻辑。而ListItem_t
则用于表示实际的列表数据项。这种区分使得代码更加清晰,逻辑更加简单。
3. 减少复杂性
在某些情况下,列表项可能不需要ListItem_t
提供的所有功能。例如,哨兵节点不需要存储额外的数据(如所属任务的指针或其他用户定义数据)。在这种情况下,使用更简单的xMINI_LIST_ITEM
可以减少实现的复杂性,同时保持足够的功能性。
4. 内存效率
在资源受限的嵌入式系统中,每个字节的内存都很宝贵。xMINI_LIST_ITEM
由于其简化的设计,占用的内存可能比ListItem_t
少。在只需要基本链表功能(如哨兵节点)时,使用xMINI_LIST_ITEM
可以节省宝贵的系统资源。
那了解了迷你列表项和成员变量和其作用后下面我们来了解下,列表项中各个成员的作用。
具体代码如下:
struct xLIST_ITEM
{listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE /*< Set to a known value if configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES is set to 1. */configLIST_VOLATILE TickType_t xItemValue; /*< The value being listed. In most cases this is used to sort the list in ascending order. */struct xLIST_ITEM * configLIST_VOLATILE pxNext; /*< Pointer to the next ListItem_t in the list. */struct xLIST_ITEM * configLIST_VOLATILE pxPrevious; /*< Pointer to the previous ListItem_t in the list. */void * pvOwner; /*< Pointer to the object (normally a TCB) that contains the list item. There is therefore a two way link between the object containing the list item and the list item itself. */struct xLIST * configLIST_VOLATILE pxContainer; /*< Pointer to the list in which this list item is placed (if any). */listSECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE /*< Set to a known value if configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES is set to 1. */
};
typedef struct xLIST_ITEM ListItem_t; /* For some reason lint wants this as two separate definitions. */
ListItem_t
(或struct xLIST_ITEM
)是FreeRTOS中用于构建双向链表的核心数据结构之一。它比xMINI_LIST_ITEM
更为复杂,提供了更多的功能和灵活性。
-
listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE: 当
configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES
设置为1时,此成员被设置为一个已知的值,用于运行时检查列表项的完整性。这有助于开发者识别内存损坏或不当的列表操作,增加了代码的健壮性。 -
configLIST_VOLATILE TickType_t xItemValue: 这是列表项的值,通常用于排序列表。在大多数情况下,列表是按照
xItemValue
的升序排列的。例如,FreeRTOS的延时任务列表就是根据任务唤醒时间(用xItemValue
表示)进行排序的。TickType_t
是一个基于配置的类型,通常为无符号整型,代表时间或计数。configLIST_VOLATILE
确保此成员在需要时被视为volatile
,适用于可能在中断服务例程中修改的场景。 -
struct xLIST_ITEM * configLIST_VOLATILE pxNext: 指向链表中下一个
ListItem_t
的指针。这使得可以从当前列表项向后遍历整个列表。 -
struct xLIST_ITEM * configLIST_VOLATILE pxPrevious: 指向链表中前一个
ListItem_t
的指针。这使得可以从当前列表项向前遍历整个列表,是双向链表结构的关键组成部分。 -
void * pvOwner: 指向包含此列表项的对象的指针。通常,这个指针指向一个任务控制块(TCB),但也可以指向其他使用列表项的数据结构。这实现了对象和其所属列表项之间的双向链接。
-
struct xLIST * configLIST_VOLATILE pxContainer: 指向包含此列表项的列表的指针。这个成员变量使得可以从列表项访问其所属的列表,进而实现诸如移除列表项或调整列表结构等操作。
-
listSECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE: 类似于
listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE
,这是另一个完整性检查值,当configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES
设置为1时使用。它位于结构体的末尾,为列表项提供额外的内存完整性验证。
上面已经介绍完了列表、列表项、迷你列表项的结构以及成员变量,接下来来看下如何形成双向链表。这一点视频里面讲的很好。直接来看视频这个地方:
其实双向环形链表就是三个人拉成一个圈一样,小明的右手指向小黑的左手,小明的左手指向小红的右手。小红、小黑的过程类似。依次形成了双向环形链表。
比如这里列表有两个列表项成员,如下图所示:详细PPT可以看正点原子开源网中课件与源码:
我们从这个图可以看出来,列表项1相当于为头,列表项2为第二项,末尾列表项就是在末尾。所以末尾列表项前一个就是列表项2,末尾列表项下一个就是头,因为是双向环形链表。也就是列表项1,列表项2前一个就是就是列表项1,所以指向列表项1,列表项2下一个是末尾列表项,而列表一前一个就是末尾列表项,下一个就是列表项2.具体指向关系就是上图所示。
列表和列表项API函数
我们去FreeRTOS的具体工程去看上述函数的具体实现代码:
void vListInitialise( List_t * const pxList )
void vListInitialise( List_t * const pxList )
{/* The list structure contains a list item which is used to mark the* end of the list. To initialise the list the list end is inserted* as the only list entry. */pxList->pxIndex = ( ListItem_t * ) &( pxList->xListEnd ); /*这行代码将链表的索引(pxIndex)设置为链表结束标志(xListEnd)的地址。在FreeRTOS中,每个链表都有一个特殊的列表项作为链表的结束标志,这个特殊的列表项不存储任何用户数据,它的主要目的是标记链表的末尾。 */listSET_FIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE( &( pxList->xListEnd ) );/* 这行代码设置列表结束标志的xItemValue为portMAX_DELAY,这是一个非常大的值,确保结束标志始终位于链表的最末端。 */pxList->xListEnd.xItemValue = portMAX_DELAY;/* The list end next and previous pointers point to itself so we know* when the list is empty. */pxList->xListEnd.pxNext = ( ListItem_t * ) &( pxList->xListEnd ); /*lint !e826 !e740 !e9087 The mini list structure is used as the list end to save RAM. This is checked and valid. */pxList->xListEnd.pxPrevious = ( ListItem_t * ) &( pxList->xListEnd ); /*这两行代码使列表结束标志的pxNext和pxPrevious指针都指向自身,这样做的目的是在链表为空时,能够通过检查这些指针来快速识别。 *//* 如果配置没有使用迷你列表项(configUSE_MINI_LIST_ITEM为0),则将xListEnd的pvOwner和pxContainer字段初始化为NULL,并设置完整性检查值。这是为了确保列表结束标志作为一个完整的列表项符合预期的数据完整性要求 */#if ( configUSE_MINI_LIST_ITEM == 0 ){pxList->xListEnd.pvOwner = NULL;pxList->xListEnd.pxContainer = NULL;listSET_SECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE( &( pxList->xListEnd ) );}#endifpxList->uxNumberOfItems = ( UBaseType_t ) 0U;//这行代码将链表中的项数初始化为0,表示链表是空的。/* Write known values into the list if* configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES is set to 1. */listSET_LIST_INTEGRITY_CHECK_1_VALUE( pxList );listSET_LIST_INTEGRITY_CHECK_2_VALUE( pxList );
}
void vListInitialiseItem( ListItem_t * const pxItem )
void vListInitialiseItem( ListItem_t * const pxItem )
{/* Make sure the list item is not recorded as being on a list. */pxItem->pxContainer = NULL;/* Write known values into the list item if* configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES is set to 1. */listSET_FIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE( pxItem );listSET_SECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE( pxItem );
}
void vListInsertEnd( List_t * const pxList, ListItem_t * const pxNewListItem )
void vListInsertEnd( List_t * const pxList,ListItem_t * const pxNewListItem )
{ListItem_t * const pxIndex = pxList->pxIndex;/*这行代码定义了一个指向ListItem_t的常量指针pxIndex,并将其初始化为指向pxList中的pxIndex成员。pxIndex通常用于标记链表的起始位置或用于遍历链表。*//* Only effective when configASSERT() is also defined, these tests may catch* the list data structures being overwritten in memory. They will not catch* data errors caused by incorrect configuration or use of FreeRTOS. */listTEST_LIST_INTEGRITY( pxList );listTEST_LIST_ITEM_INTEGRITY( pxNewListItem );/* Insert a new list item into pxList, but rather than sort the list,* makes the new list item the last item to be removed by a call to* listGET_OWNER_OF_NEXT_ENTRY(). */pxNewListItem->pxNext = pxIndex;pxNewListItem->pxPrevious = pxIndex->pxPrevious;/*由于要将这个列表项插入到pxindex的前一个。由于是双向环形链表,所以相当于该列表项伸出来2只手,一个拉前面,一个拉后面*//* Only used during decision coverage testing. */mtCOVERAGE_TEST_DELAY();pxIndex->pxPrevious->pxNext = pxNewListItem;pxIndex->pxPrevious = pxNewListItem;/*新列表项已经伸出来2只手,那么前面的是不是也要伸出来一只手连接到新的列表项。那新的列表项后面那个是不是也要往前面伸出来一只手与之连接才能形成双向环形链表*//* Remember which list the item is in. */pxNewListItem->pxContainer = pxList;( pxList->uxNumberOfItems )++;
}
上面这段函数如果对链表操作熟悉的话,就其实是把pxNewListItem 插入pxIndex前面的链表操作。
void vListInsert( List_t * const pxList, ListItem_t * const pxNewListItem )
/*-----------------------------------------------------------*/void vListInsert( List_t * const pxList,ListItem_t * const pxNewListItem )
{ListItem_t * pxIterator;const TickType_t xValueOfInsertion = pxNewListItem->xItemValue;/* Only effective when configASSERT() is also defined, these tests may catch* the list data structures being overwritten in memory. They will not catch* data errors caused by incorrect configuration or use of FreeRTOS. */listTEST_LIST_INTEGRITY( pxList );listTEST_LIST_ITEM_INTEGRITY( pxNewListItem );/* 如果xValueOfInsertion的值等于portMAX_DELAY,这通常表示一个特殊的值,用于将新的列表项插入到列表的末尾。因此,pxIterator被设置为指向列表末尾前的项(pxList->xListEnd.pxPrevious)。如果xValueOfInsertion不等于portMAX_DELAY,则通过一个循环找到新列表项应该插入的位置。循环遍历列表,直到找到一个其xItemValue大于xValueOfInsertion的项。pxIterator在这个过程中用于遍历列表。 */if( xValueOfInsertion == portMAX_DELAY ){pxIterator = pxList->xListEnd.pxPrevious;}else{/*该循环用来寻找插入的正确位置,这个函数插入是按照数值升序去插入的。首先循环的初始值就是哨兵节点的地址,由于要找到合适的插入位置,也就是终止循环的条件是pxIterator->pxNext->xItemValue > xValueOfInsertion。所以此时的pxIterator就是要插入位置的前一个节点。*/for( pxIterator = ( ListItem_t * ) &( pxList->xListEnd ); pxIterator->pxNext->xItemValue <= xValueOfInsertion; pxIterator = pxIterator->pxNext ) /*lint !e826 !e740 !e9087 The mini list structure is used as the list end to save RAM. This is checked and valid. *//*lint !e440 The iterator moves to a different value, not xValueOfInsertion. */{/* There is nothing to do here, just iterating to the wanted* insertion position. */}}/*这里更好的理解就是理解为手拉手操作,要更新插入新的人前后指向位置,以及新的人前面的人指向后的位置。还有新的人后面的人指向前面的位置,这样才能形成双向链表。这里用图其实更好理解*/pxNewListItem->pxNext = pxIterator->pxNext;pxNewListItem->pxNext->pxPrevious = pxNewListItem;pxNewListItem->pxPrevious = pxIterator;pxIterator->pxNext = pxNewListItem;/* Remember which list the item is in. This allows fast removal of the* item later. */pxNewListItem->pxContainer = pxList;( pxList->uxNumberOfItems )++;
}
/*-----------------------------------------------------------*/
这个函数完成链表操作我用图大概画了下:
这样理解为牵手更容易去理解。
下面来看最后一个函数也就是删除链表中某个节点。删除要进行的操作就是
UBaseType_t uxListRemove( ListItem_t * const pxItemToRemove )
{
/* The list item knows which list it is in. Obtain the list from the list* item. */List_t * const pxList = pxItemToRemove->pxContainer;pxItemToRemove->pxNext->pxPrevious = pxItemToRemove->pxPrevious;pxItemToRemove->pxPrevious->pxNext = pxItemToRemove->pxNext;/* Only used during decision coverage testing. */mtCOVERAGE_TEST_DELAY();/* Make sure the index is left pointing to a valid item. */if( pxList->pxIndex == pxItemToRemove ){pxList->pxIndex = pxItemToRemove->pxPrevious;}else{mtCOVERAGE_TEST_MARKER();}pxItemToRemove->pxContainer = NULL;( pxList->uxNumberOfItems )--;return pxList->uxNumberOfItems;
}
实际要完成的操作就是把1,2,3,4都给断掉,变成蓝色的。
所以要完成代码的操作就是下面这个:
1:pxItemToRemove->pxNext->pxPrevious = pxItemToRemove->pxPrevious;
2:pxItemToRemove->pxPrevious->pxNext = pxItemToRemove->pxNext;