在 FreeRTOS 里,任务的创建、阻塞、就绪、延时、挂起等状态变化,都离不开一个非常基础的数据结构:列表。
FreeRTOS 的列表可以先理解成链表,列表项就是链表里的节点。更准确一点说,它是一个带有末尾哨兵节点的双向环形链表。本文的重点不是单纯背 API,而是理解 FreeRTOS 为什么要这样设计,以及这些列表操作最终怎样服务于任务调度。
为什么 FreeRTOS 要用列表
数组的特点是成员地址连续,访问方便,但长度通常在一开始就确定下来,后期不适合频繁增删。
RTOS 里的任务则不同:
- 任务数量不一定固定。
- 任务会不断在就绪、阻塞、挂起等状态之间切换。
- 同一个任务控制块可能在不同时间挂到不同的系统列表里。
- 调度器需要高效地插入、删除和遍历任务。
所以 FreeRTOS 选择用列表来管理任务。列表项之间不要求地址连续,它们通过 pxNext 和 pxPrevious 人为连接起来,数量也可以随着任务状态变化动态增减。
在 FreeRTOS 源码中,列表相关内容主要在 list.c 和 list.h 中。
三个核心结构体
理解 FreeRTOS 列表,先抓住三个结构体:
List_t:列表本身。ListItem_t:普通列表项。MiniListItem_t:迷你列表项,主要用于列表末尾的哨兵节点。
List_t:列表结构
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;
几个成员的含义:
uxNumberOfItems 用来记录列表中普通列表项的数量,不包含 xListEnd。
pxIndex 是一个遍历指针,FreeRTOS 在遍历列表项时会用它记住当前位置。
xListEnd 是列表的末尾列表项,也可以理解成哨兵节点。它不是普通业务节点,而是用来标记列表边界、简化插入和遍历逻辑。
listFIRST_LIST_INTEGRITY_CHECK_VALUE 和 listSECOND_LIST_INTEGRITY_CHECK_VALUE 是完整性检查相关宏。开启对应配置后,FreeRTOS 可以通过这些固定值判断列表结构是否在运行过程中被破坏。
ListItem_t:普通列表项
普通列表项定义如下:
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;
这里最重要的是五个字段:
xItemValue 是列表项的值,常用于排序。例如延时列表会根据唤醒 tick 值排序。
pxNext 和 pxPrevious 分别指向下一个和上一个列表项,因此 FreeRTOS 的列表是双向链表。
pvOwner 指向拥有这个列表项的对象,通常是任务控制块 TCB。也就是说,调度器可以通过列表项反查到对应任务。
pxContainer 指向当前列表项所在的列表。列表项被移除时,这个字段会被清空为 NULL。
MiniListItem_t:迷你列表项
迷你列表项定义如下:
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;
它比普通列表项少了 pvOwner 和 pxContainer,因为 MiniListItem_t 只用于标记列表末尾,不需要关联具体任务,也不需要记录自己属于哪个列表。
List_t 里的 xListEnd 就是一个 MiniListItem_t。
列表初始化:vListInitialise()
vListInitialise() 用于初始化一个列表:
void vListInitialise( List_t * const pxList )
{
pxList->pxIndex = ( ListItem_t * ) &( pxList->xListEnd );
pxList->xListEnd.xItemValue = portMAX_DELAY;
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 );
}
初始化后的列表有几个关键状态:
- 列表中暂时只有
xListEnd。 pxIndex指向xListEnd。xListEnd.xItemValue被设置为portMAX_DELAY,也就是最大值。xListEnd.pxNext和xListEnd.pxPrevious都指向自己,形成一个只有哨兵节点的环形链表。uxNumberOfItems为 0,因为它不统计xListEnd。
这个设计非常巧妙:即使列表为空,pxNext 和 pxPrevious 也始终有效,后续插入和删除时就不需要处理大量空指针特例。
列表项初始化:vListInitialiseItem()
vListInitialiseItem() 用于初始化一个普通列表项:
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 );
}
它最重要的动作是把 pxContainer 设置为 NULL。这表示该列表项当前不属于任何列表。
一个列表项是否已经被挂入某个列表,可以通过 pxContainer 判断。插入时它会指向目标列表,移除时又会被清空。
有序插入:vListInsert()
vListInsert() 会根据 xItemValue 的值,将列表项按升序插入到列表中。
函数原型如下:
void vListInsert( List_t * const pxList, ListItem_t * const pxNewListItem );
核心逻辑可以分成四步:
- 读取新列表项的
xItemValue。 - 从列表中寻找合适的插入位置。
- 修改前后节点的
pxNext和pxPrevious,把新节点接入链表。 - 更新新节点的
pxContainer,并让uxNumberOfItems加 1。
源码中的关键片段如下:
ListItem_t * pxIterator;
const TickType_t xValueOfInsertion = 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->pxContainer = pxList;
( pxList->uxNumberOfItems )++;
假设依次插入值为 40、60、50 的三个列表项,最终顺序会是:
xListEnd <-> 40 <-> 50 <-> 60 <-> xListEnd
注意这里的 xListEnd.xItemValue 是最大值,所以它天然排在最后。代码里还专门处理了 xValueOfInsertion == portMAX_DELAY 的情况,否则遍历时会和同样为最大值的 xListEnd 发生边界问题。
vListInsert() 常用于需要按时间或优先级相关值排序的场景。例如延时列表中,任务会按照唤醒 tick 值挂入列表,调度器就可以更快找到下一个应该被唤醒的任务。
末尾插入:vListInsertEnd()
vListInsertEnd() 的函数原型如下:
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 )++;
它不会按 xItemValue 排序,所以是一种无序插入。
如果 pxIndex 正好指向 xListEnd,它看起来就像把节点插到了普通列表项的末尾。例如原来有一个值为 40 的列表项,再插入一个值为 30 的列表项,顺序可能变成:
xListEnd <-> 40 <-> 30 <-> xListEnd
可以看到,30 并没有排到 40 前面。这就是它和 vListInsert() 最大的区别。
在 FreeRTOS 中,这种插入方式常用于不需要按 xItemValue 排序、但需要保持某种轮转顺序的列表。
移除列表项:uxListRemove()
uxListRemove() 用于把一个列表项从它所在的列表中移除:
UBaseType_t uxListRemove( ListItem_t * const pxItemToRemove );
返回值是移除后,该列表剩余的普通列表项数量。
核心代码如下:
UBaseType_t uxListRemove( ListItem_t * const pxItemToRemove )
{
List_t * const pxList = pxItemToRemove->pxContainer;
pxItemToRemove->pxNext->pxPrevious = pxItemToRemove->pxPrevious;
pxItemToRemove->pxPrevious->pxNext = pxItemToRemove->pxNext;
if( pxList->pxIndex == pxItemToRemove )
{
pxList->pxIndex = pxItemToRemove->pxPrevious;
}
pxItemToRemove->pxContainer = NULL;
( pxList->uxNumberOfItems )--;
return pxList->uxNumberOfItems;
}
它的操作顺序也很清晰:
- 通过
pxContainer找到列表项所在的列表。 - 修改前后列表项的指针,把当前节点从链表中摘出去。
- 如果
pxIndex正好指向待移除项,就把pxIndex回退到上一个列表项。 - 将待移除项的
pxContainer清空为NULL。 - 列表项数量减 1。
- 返回列表剩余列表项数量。
假设列表中有 40、50、60 三个列表项:
xListEnd <-> 40 <-> 50 <-> 60 <-> xListEnd
移除 60 后,链表变为:
xListEnd <-> 40 <-> 50 <-> xListEnd
被移除的列表项不再属于任何列表,因此它的 pxContainer 会变成 NULL。
五个 API 的对比
| API | 作用 | 关键点 |
|---|---|---|
vListInitialise() | 初始化列表 | 建立只包含 xListEnd 的双向环形链表 |
vListInitialiseItem() | 初始化列表项 | 将 pxContainer 设置为 NULL |
vListInsert() | 有序插入列表项 | 按 xItemValue 升序插入 |
vListInsertEnd() | 插入到 pxIndex 前面 | 不排序,常表现为末尾插入 |
uxListRemove() | 移除列表项 | 摘链、清空 pxContainer、返回剩余数量 |
这五个函数构成了 FreeRTOS 列表操作的基础。后面看任务调度、延时列表、事件列表时,本质上都是这些基础操作的组合。
实验设计思路
课程中的实验目标是:学会使用 FreeRTOS 列表和列表项相关 API,并观察实际运行结果是否和理论分析一致。
实验设计了三个任务:
start_task 用来创建其他任务。
task1 每 500ms 翻转一次 LED0,用来提示系统正在运行。
task2 调用列表和列表项相关 API,通过串口输出列表状态,观察初始化、插入和删除后的变化。
实验时可以重点观察这些信息:
- 初始化后
uxNumberOfItems是否为 0。 - 列表项插入后
pxContainer是否指向目标列表。 - 使用
vListInsert()插入多个不同xItemValue的列表项后,顺序是否升序。 - 使用
vListInsertEnd()时,列表项是否没有按xItemValue排序。 - 调用
uxListRemove()后,列表数量是否减少,被移除项的pxContainer是否变为NULL。
这些观察点能帮助我们把“源码指针操作”和“实际链表变化”对应起来。
小结
FreeRTOS 的列表不是一个孤立的数据结构练习,而是整个任务管理机制的地基。
List_t 负责维护列表状态,ListItem_t 作为节点挂入列表,MiniListItem_t 作为 xListEnd 哨兵节点简化边界处理。列表初始化后会形成一个只有 xListEnd 的双向环形链表,后续插入和删除都围绕这个结构展开。
vListInsert() 负责按 xItemValue 有序插入,适合需要排序的场景;vListInsertEnd() 负责插入到 pxIndex 前面,不关心 xItemValue;uxListRemove() 则负责把列表项从当前列表中摘除,并维护列表数量和归属关系。
理解这些之后,再看 FreeRTOS 的就绪列表、延时列表、事件等待列表,就会发现调度器并没有什么神秘魔法。它只是在正确的时机,把代表任务的列表项从一个列表移到另一个列表中。









