FreeRTOS 链表源码深度解析:从数据结构到调度内核的基石

40 阅读9分钟

本篇文章所有代码基于 FreeRTOS Kernel V10.3.1,配套完整可运行的示例位于项目中 projects/02_list,使用 QEMU + STM32L475 平台仿真运行,基于VS Code的可视化在线调试方法可参考上篇文章告别硬件开发板,手把手教你零成本学习 FreeRTOS,学习内容也可以参考微信公众号:青衫嵌入式, 本项目源码开源地址:gitcode.com/qingshan120…


一、引言:为什么链表是 RTOS 的命脉?

当你调用 xTaskCreate() 创建任务,调用 vTaskDelay() 让任务等待,或者调度器在 tick 中断中切换任务时——所有这些操作的背后,都离不开同一个基础数据结构:双向循环链表

在 FreeRTOS 中,链表被用来管理:

用途对应列表
就绪任务pxReadyTasksLists[优先级]
阻塞/延时任务xDelayedTaskList1 / xDelayedTaskList2
挂起任务xSuspendedTaskList
空闲任务xTasksWaitingTermination
软件定时器pxTimerList

在这里插入图片描述 可以说,理解了 FreeRTOS 的链表,就掌握了理解调度内核的入口。

本文围绕 projects/02_list 中的 7 个演示,逐一拆解 FreeRTOS 链表的全部核心 API,并配合运行输出验证。


二、核心数据结构

2.1 链表节点:ListItem_t

struct xLIST_ITEM {
    TickType_t xItemValue;            // 节点的排序值
    struct xLIST_ITEM *pxNext;        // 指向下一个节点
    struct xLIST_ITEM *pxPrevious;    // 指向前一个节点
    void *pvOwner;                    // 指向"宿主"对象(如 TCB)
    struct xLIST *pxContainer;        // 指向所属的链表
};
typedef struct xLIST_ITEM ListItem_t;

关键设计思想:

  1. xItemValue —— 排序依据。就绪链表中用任务优先级,延时链表中用任务的唤醒时间点。
  2. pvOwner 双向关联 —— 节点知道自己属于哪个对象(Owner),Owner 反过来嵌入节点。TCB 结构体内部就嵌有一个 ListItem_t
  3. pxContainer —— 节点知道自己在哪条链表里,这使得 uxListRemove() 可以不依赖链表指针,仅凭节点自身就能完成删除。

2.2 迷你节点:MiniListItem_t

struct xMINI_LIST_ITEM {
    TickType_t xItemValue;
    struct xLIST_ITEM *pxNext;
    struct xLIST_ITEM *pxPrevious;
};
typedef struct xMINI_LIST_ITEM MiniListItem_t;

ListItem_t 相比去掉了 pvOwnerpxContainer仅用作链表的结尾标记,节省 RAM(不要小看节省的 8 字节,在 RAM 以 KB 计量的 MCU 上弥足珍贵)。

2.3 链表头:List_t

typedef struct xLIST {
    UBaseType_t uxNumberOfItems;      // 节点计数
    ListItem_t *pxIndex;              // 遍历游标
    MiniListItem_t xListEnd;          // 结尾标记节点
} List_t;

2.4 结构关系总览

用下图展示三个核心数据结构的关系:

在这里插入图片描述 初始化后,xListEnd.pxNextxListEnd.pxPrevious 都指向自身 —— 这是空链表的标志。


三、链表初始化: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 = 0;
}

初始化后的状态:

在这里插入图片描述 三个重点:

  1. xListEnd.xItemValue = portMAX_DELAY —— 赋值为最大值,确保它在排序时永远在链表末尾。
  2. pxNext = pxPrevious = &xListEnd —— 自环结构,表示空链表。
  3. pxIndex = &xListEnd —— 遍历游标初始指向结尾标记。

演示 1 运行输出:

--- 演示1: vListInitialise ---
  listLIST_IS_INITIALISED: 
  listLIST_IS_EMPTY:      
  listCURRENT_LIST_LENGTH: 0

四、排序插入:vListInsert

这是 FreeRTOS 链表最核心的操作。

void vListInsert(List_t *const pxList, ListItem_t *const pxNewListItem)
{
    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)++;
}

插入逻辑图解

假设依次插入:A(30) → B(10) → C(50) → D(20) → E(40)

在这里插入图片描述 排序规则核心: 遍历时,条件 pxIterator->pxNext->xItemValue <= xValueOfInsertion 表示"继续向前走,直到下一个节点的值大于新值"。这意味着:

  • 按值升序排列(小值在前)。
  • 相同值时,新节点插入到所有相同值节点之后 —— FIFO 行为

演示 2 运行输出:

--- 演示2: vListInsert (排序插入) ---
  插入顺序: A(30) B(10) C(50) D(20) E(40)
  预期结果: B(10) D(20) A(30) E(40) C(50)
-- vListInsert 结果 (共 5 项)
  [0] B value=10
  [1] D value=20
  [2] A value=30
  [3] E value=40
  [4] C value=50
  => 验证: 按 xItemValue 升序排列正确

五、相同值处理:FIFO 行为

当多个节点具有相同的 xItemValue 时,vListInsert 将它们按插入顺序排列,先入先出(FIFO)。

// 循环条件 <= 保证了新节点插在所有 <= 自身值的节点之后
pxIterator->pxNext->xItemValue <= xValueOfInsertion

在这里插入图片描述

演示 6 运行输出:

--- 演示6: 相同 value 的 FIFO 插入行为 ---
  插入: A(50) B(50) C(50) D(30)
  预期: D(30) -> A(50) -> B(50) -> C(50)
-- 相同 value 插入结果 (共 4 项)
  [0] D30 value=30
  [1] A50 value=50
  [2] B50 value=50
  [3] C50 value=50
  => 同 value 按插入顺序 FIFO,值小的在前

重要影响: 在就绪链表中,同优先级的任务按 FIFO 顺序被调度,这正是 FreeRTOS 的同优先级时间片轮转的实现基础。


六、尾部插入:vListInsertEnd

vListInsert 不同,vListInsertEnd 不按值排序,而是将节点插入到 pxIndex 游标的当前位置之前(逻辑尾部)。

void vListInsertEnd(List_t *const pxList, ListItem_t *const pxNewListItem)
{
    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 的初值就是 xListEnd,所以第一次插入时 vListInsertEnd 的效果实际上等同于在列表末尾追加。但 pxIndex 会随着 listGET_OWNER_OF_NEXT_ENTRY 的遍历而移动。

演示 3 运行输出:

--- 演示3: vListInsertEnd (尾部插入) ---
  vListInsertEnd 保留插入顺序,不按 value 排序
-- vListInsertEnd 结果 (共 3 项)
  [0] X value=300
  [1] Y value=200
  [2] Z value=100
  => 顺序: X(300) Y(200) Z(100) 与插入顺序一致

注意:X(300) 在 Y(200) 之前,而 Z(100) 在最后,完全保留了插入顺序,无视 value 大小


七、删除操作:uxListRemove

删除是最简单的操作——因为节点自身保存了 pxContainer 指针,只要传入节点即可完成删除。

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;
}

删除过程图解

在这里插入图片描述

三个关键细节:

  1. pxContainer 自动清零 —— 删除后节点不再认为自己属于任何链表。
  2. pxIndex 保护 —— 如果删除的刚好是游标指向的节点,游标自动回退到前一个节点,防止遍历指针悬空。
  3. 返回值是有用信息 —— 返回删除后链表中剩余节点的数量。

演示 4 运行输出:

--- 演示4: uxListRemove (删除链表项) ---
-- 插入 5 个元素 (共 5 项)
  [0] A value=10
  [1] B value=20
  [2] C value=30
  [3] D value=40
  [4] E value=50
  删除C(中间) 后剩余: 4
-- 删除C后 (共 4 项)
  [0] A value=10
  [1] B value=20
  [2] D value=40
  [3] E value=50
  删除A(头部) 后剩余: 3
-- 删除A后 (共 3 项)
  [0] B value=20
  [1] D value=40
  [2] E value=50
  删除E(尾部) 后剩余: 2
-- 删除E后 (共 2 项)
  [0] B value=20
  [1] D value=40
  全部删除后 listLIST_IS_EMPTY: 是

八、辅助宏与工具函数

以下宏虽短,但贯穿了整个 FreeRTOS 源码。

功能调度器中的典型用法
listSET_LIST_ITEM_VALUE设置节点的排序值将延时任务的唤醒 tick 写入节点
listGET_LIST_ITEM_VALUE读取节点的排序值比较任务优先级或唤醒时间
listSET_LIST_ITEM_OWNER设置节点的宿主指针将 TCB 指针赋给节点
listGET_LIST_ITEM_OWNER获取节点的宿主指针遍历就绪链表获取待运行任务
listGET_HEAD_ENTRY获取链表的第一个节点获取最高优先级的就绪任务
listGET_END_MARKER获取链表的结尾标记遍历时判断是否走到末尾
listGET_NEXT获取当前节点的下一个节点遍历链表
listLIST_IS_EMPTY判断链表是否为空检查就绪链表是否有任务
listCURRENT_LIST_LENGTH获取链表长度调试/统计
listIS_CONTAINED_WITHIN判断节点是否在指定链表中验证任务状态一致性

在这里插入图片描述

容器检查

listIS_CONTAINED_WITHIN 利用了节点中的 pxContainer 指针:

#define listIS_CONTAINED_WITHIN(pxList, pxListItem) \
    ((pxListItem)->pxContainer == (pxList)) ? pdTRUE : pdFALSE

演示 5 运行输出:

--- 演示5: listIS_CONTAINED_WITHIN (容器检查) ---
  插入 listA: in A=是  in B=否
  移除后: in A=否  pxContainer=NULL
  插入 listB: in B=是

九、综合实战:模拟调度器就绪链表

演示 7 模拟了 FreeRTOS 调度器的核心行为——从就绪链表中按优先级取任务执行。

场景

4 个任务,优先级分别为:1(低)、2(中)、3(高)、2(中优先级2)

vListInsert(&readyList, &tasks[0].listItem);  // 优先级 1
vListInsert(&readyList, &tasks[1].listItem);  // 优先级 2
vListInsert(&readyList, &tasks[2].listItem);  // 优先级 3
vListInsert(&readyList, &tasks[3].listItem);  // 优先级 2

就绪链表结构

在这里插入图片描述

运行输出:

--- 演示7: 综合 -- 模拟调度器就绪链表 ---
  4 个任务按优先级(1/2/3/2)插入就绪链表
  vListInsert 按 value 升序排列
-- 就绪链表 (共 4 项)
  [0] 低优先级 value=1
  [1] 中优先级 value=2
  [2] 中优先级2 value=2
  [3] 高优先级 value=3

  出队顺序 (升序,value 小先出):
    出队: 低优先级 (value=1)
    出队: 中优先级 (value=2)
    出队: 中优先级2 (value=2)
    出队: 高优先级 (value=3)
  全部出队完毕

与真实调度器的对应关系

演示中的元素真实 FreeRTOS 调度器
DemoItem_t 嵌入 ListItem_tTCB_t 嵌入 ListItem_t(通过 xStateListItemxEventListItem
xItemValue 存储优先级就绪链表pxReadyTasksLists[pri] 中所有 TCB 的 xItemValue 相同
vListInsert 升序排列同优先级任务按 FIFO 插入,实现时间片轮转
listGET_HEAD_ENTRY 取队首selectHighestPriorityTask() 获取最高优先级队列的第一个 TCB
uxListRemove 出队任务被调度运行后从就绪链表移除

十、总结

FreeRTOS 的链表设计处处体现着嵌入式系统 RAM 极度受限下的工程智慧:

  1. 双向循环链表 —— 任意节点插入/删除都是 O(1) 时间复杂度,确定性对 RTOS 至关重要。
  2. MiniListItem_t 作为结尾标记 —— 省去 2 个指针(8 字节),避免为结尾标记分配完整的 ListItem_t
  3. pvOwner 双向关联 —— 链表节点 ↔ 宿主对象的"双向指针",遍历时能直接从节点拿到 TCB。
  4. pxContainer 自包含删除 —— 节点保存所属链表信息,uxListRemove 不需要外部传入链表指针。
  5. 排序插入 FIFO —— <= 比较条件保证同值节点的先入先出行为。

在这里插入图片描述

掌握 FreeRTOS 链表,就等于拿到了阅读整个 FreeRTOS 内核源码的钥匙。无论你是想理解任务调度、延时机制,还是软件定时器、事件组,底层的链表操作逻辑始终如一。


参考资料

  • FreeRTOS Kernel V10.3.1 官方源码:freertos/include/list.hfreertos/list.c
  • 本演示项目完整代码:projects/02_list/app/main.c
  • 构建方式:cmake -S . -B build -DCMAKE_TOOLCHAIN_FILE=cmake/arm-none-eabi.cmake -DBUILD_PROJECT=02_list && cmake --build build

如果你觉得这篇文章有帮助,欢迎转发分享。