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

本篇文章所有代码基于 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

c 复制代码
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

c 复制代码
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

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

2.4 结构关系总览

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

初始化后,xListEnd.pxNextxListEnd.pxPrevious 都指向自身 ------ 这是空链表的标志。


三、链表初始化:vListInitialise

c 复制代码
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 运行输出:

yaml 复制代码
--- 演示1: vListInitialise ---
  listLIST_IS_INITIALISED: 是
  listLIST_IS_EMPTY:      是
  listCURRENT_LIST_LENGTH: 0

四、排序插入:vListInsert

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

c 复制代码
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 运行输出:

ini 复制代码
--- 演示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)。

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

演示 6 运行输出:

ini 复制代码
--- 演示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 游标的当前位置之前(逻辑尾部)。

c 复制代码
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 运行输出:

ini 复制代码
--- 演示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 指针,只要传入节点即可完成删除。

c 复制代码
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 运行输出:

ini 复制代码
--- 演示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 指针:

c 复制代码
#define listIS_CONTAINED_WITHIN(pxList, pxListItem) \
    ((pxListItem)->pxContainer == (pxList)) ? pdTRUE : pdFALSE

演示 5 运行输出:

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

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

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

场景

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

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

就绪链表结构

运行输出:

ini 复制代码
--- 演示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_t TCB_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

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

相关推荐
大侠锅锅3 小时前
第 1 篇:开篇|物联网边缘计算的真实挑战与云边端架构全景
物联网·架构·边缘计算
国科安芯11 小时前
ASC4T245S分组双向控制架构深度解析:独立DIR/OE控制、QFN16封装与混合方向总线桥接
单片机·嵌入式硬件·物联网·fpga开发·架构·risc-v
KaMeidebaby12 小时前
卡梅德生物技术快报|蛋白 N 端测序在重组贻贝融合蛋白表征中的应用,解决原核表达序列偏移工艺难题
前端·人工智能·物联网·算法·百度
JNX_SEMI13 小时前
AT2401C 2.4GHz 全集成射频前端单芯片技术解析
前端·单片机·嵌入式硬件·物联网·硬件工程
SMT贴片河南芯途电子17 小时前
工业数据采集终端硬件定制:低功耗、多传感与无线通信融合!
物联网·边缘计算
老孙讲技术18 小时前
10 分钟把门口摄像机写进 Home Assistant:Core 集成复盘
物联网
机汇五金_20 小时前
钣金外壳定制厂家助力设备升级
大数据·人工智能·python·物联网
yxl8746464621 小时前
PCTG-1015型Profinet转Ethernet/IP协议转换器
服务器·网络·物联网·网络协议·自动化·信息与通信
奥莱维1 天前
智能酒店系统构建指南
大数据·网络·人工智能·物联网