尧图网站建设 尧图网络
  • 首页
  • 关于我们
  • 服务项目
  • 案例展示
  • 建站流程
  • 资讯中心
  • 联系我们
首页/资讯中心/详情

三、FreeRTOS 内核数据结构:列表与列表项详解

三、FreeRTOS 内核数据结构:列表与列表项详解
📅 发布时间:2026/7/8 15:19:15

在 FreeRTOS 内核中,任务调度、延时管理、事件等待、软件定时器等功能都离不开一个基础数据结构:链表。FreeRTOS 并不是简单地使用普通单向链表,而是实现了一套适合内核调度使用的双向循环链表。理解List_t、ListItem_t和MiniListItem_t,对于后续学习任务控制块、就绪列表、延时列表和事件列表非常重要。

本文主要从链表的基本思想、FreeRTOS 列表结构、列表项初始化、链表初始化、节点插入、节点删除和实际使用场景几个方面进行整理。

一、为什么 FreeRTOS 需要链表

在裸机程序中,通常是一个while(1)主循环顺序执行各个功能函数。而在 RTOS 中,系统中可能同时存在多个任务,每个任务又会处于不同状态,例如:

任务状态说明
就绪态任务已经具备运行条件,等待调度器分配 CPU
运行态当前正在占用 CPU 执行
阻塞态正在等待延时结束、队列数据、信号量或事件
挂起态被主动挂起,暂时不参与调度

内核需要把这些任务按照状态组织起来。比如,就绪任务放在就绪列表中,延时任务放在延时列表中,等待队列的任务放在事件等待列表中。

如果使用数组管理这些任务,删除和插入都不够灵活。任务状态经常变化,如果每次状态切换都移动数组元素,效率并不理想。链表更适合这种频繁插入、删除、迁移节点的场景。

例如,一个任务调用延时函数后,需要从就绪列表移除,再插入延时列表;延时时间到达后,又需要从延时列表移除,再插回就绪列表。这个过程本质上就是链表节点的移动。

二、普通链表与 FreeRTOS 链表的区别

普通单向链表一般只包含一个指向下一个节点的指针:

/* ============================================ * 一、普通单向链表节点(对比用) * ============================================ */ typedef struct Node { int data; /* 节点存储的数据 */ struct Node *next; /* 指向下一个节点的指针(单向) */ } Node_t;

这种结构适合简单数据管理,但删除某个节点时,如果不知道前一个节点,就需要从头遍历查找。

FreeRTOS 使用的是双向循环链表。每个节点不仅知道后一个节点,也知道前一个节点:

/* ============================================ * 二、双向链表节点(对比用) * ============================================ */ typedef struct Node { int data; /* 节点存储的数据 */ struct Node *next; /* 指向后一个节点的指针 */ struct Node *prev; /* 指向前一个节点的指针(双向核心) */ } Node_t;

这样删除节点时,不需要从头查找前驱节点,直接通过prev和next就可以把当前节点摘下来。

FreeRTOS 的链表还有一个特点:它带有一个固定存在的结束标记节点,也就是常说的“哨兵节点”。这个节点不是任务,也不是普通数据,只是用来标记链表边界,简化插入和删除逻辑。

三、FreeRTOS 中的三个核心结构

FreeRTOS 链表主要由三个结构组成:

结构含义作用
List_t链表结构管理整个链表
ListItem_t列表项作为普通节点插入链表
MiniListItem_t迷你列表项作为链表结束标记节点

在较新的 FreeRTOS 版本中,结构体名称通常通过typedef变成List_t、ListItem_t、MiniListItem_t。在一些旧资料中,也会看到xLIST、xLIST_ITEM、xMINI_LIST_ITEM这样的写法。本质上是同一类结构,只是命名和配置细节有所变化。

四、ListItem_t:真正挂入链表的节点

ListItem_t是链表中的普通节点。简化后的结构如下:

/* ============================================ * 三、ListItem_t:真正挂入链表的节点 * ============================================ */ typedef struct ListItem { uint32_t itemValue; /* 排序辅助值,用于延时列表等有序链表 */ struct ListItem *next; /* 指向链表中后一个节点 */ struct ListItem *prev; /* 指向链表中前一个节点(双向,便于删除) */ void *owner; /* 指向拥有该节点的对象,通常指向任务控制块 TCB */ void *container; /* 指向该节点当前所在的链表,为 NULL 表示未插入任何链表 */ } ListItem_t;

每个成员都有明确作用。

itemValue用于排序。比如在延时列表中,它可以表示任务应该在什么 tick 时刻被唤醒。数值越小,说明越早被处理。

next和prev用于连接前后节点,这是双向链表的基础。

owner表示这个列表项属于哪个对象。在任务调度中,它通常指向任务控制块 TCB。

container表示这个列表项当前位于哪个链表中。如果一个节点已经插入某个链表,container就指向该链表;如果没有插入任何链表,container通常为NULL。

这个设计非常关键。FreeRTOS 并不是把任务控制块本身直接放进链表,而是把任务控制块内部的列表项挂到链表上。

可以用一个简化的任务控制块表示:

/* ============================================ * 四、任务控制块 TCB 简化示例 * 说明:FreeRTOS 将列表项内嵌在 TCB 中, * 通过列表项挂入不同链表,而非直接移动 TCB * ============================================ */ typedef struct TaskControlBlock { char taskName[16]; /* 任务名称 */ uint32_t priority; /* 任务优先级 */ ListItem_t stateListItem; /* 用于挂入就绪列表、延时列表等状态链表 */ ListItem_t eventListItem; /* 用于挂入队列、信号量等事件等待链表 */ } TCB_t;

这样做的好处是,同一个任务可以根据不同用途挂入不同链表。例如stateListItem用于表示任务状态,eventListItem用于表示任务等待某个事件。

五、List_t:管理整个链表

List_t是链表的管理结构。简化后的定义如下:

/* ============================================ * 五、List_t:链表管理结构(链表头) * ============================================ */ typedef struct List { uint32_t numberOfItems; /* 链表中有效节点数量(不包含哨兵节点) */ ListItem_t *index; /* 遍历游标,调度器用它指向当前任务,实现轮转调度 */ ListItem_t end; /* 哨兵节点(结束标记),始终存在,不属于任何任务 */ } List_t;

numberOfItems记录链表中真实节点的数量,不包含哨兵节点。

index是遍历链表时使用的指针。FreeRTOS 调度器在同优先级任务之间轮转时,会用类似游标的方式找到下一个任务。

end是链表结束标记节点。它始终存在,不代表任何任务。很多资料中把它理解成“根节点”,从学习角度看可以这么记,但更准确的叫法是“哨兵节点”或“结束标记节点”。

六、MiniListItem_t:为什么需要迷你节点

MiniListItem_t是精简版列表项。它一般只保留排序值和前后指针:

/* ============================================ * 六、MiniListItem_t:精简版列表项(哨兵专用) * 说明:哨兵节点不需要 owner 和 container, * 因此可用更小的结构节省内存 * ============================================ */ typedef struct MiniListItem { uint32_t itemValue; /* 排序值,哨兵节点设为最大值,确保位于链表末尾 */ struct ListItem *next; /* 指向后一个节点 */ struct ListItem *prev; /* 指向前一个节点 */ } MiniListItem_t;

它没有owner和container,因为哨兵节点不属于任何任务,也不需要反向找到任务控制块。它只是链表的边界标记。

在一些新版本配置中,MiniListItem_t也可能直接等同于ListItem_t。这是 FreeRTOS 为了兼容性和配置灵活性做的处理。学习时只需要记住:xListEnd是链表内部自带的结束标记,不是普通任务节点。

七、链表初始化

链表在使用前必须初始化。初始化后的链表是一个空的双向循环链表。

简化代码如下:

/* ============================================ * 七、链表初始化函数 * 作用:将链表初始化为空的双向循环链表 * ============================================ */ #define MAX_DELAY 0xffffffffUL /* 32 位无符号最大值,作为哨兵节点的 itemValue */ void List_Init(List_t *list) { /* 初始化时链表中没有任何有效节点 */ list->numberOfItems = 0; /* 遍历游标初始指向哨兵节点,表示从头开始 */ list->index = &list->end; /* 哨兵节点排序值设为最大,确保在有序链表中永远位于最后 */ list->end.itemValue = MAX_DELAY; /* 哨兵节点的前后指针都指向自己,形成闭环 */ /* 这是空双向循环链表的标志 */ list->end.next = &list->end; list->end.prev = &list->end; /* 哨兵节点不属于任何任务,也不属于任何外部链表 */ list->end.owner = NULL; list->end.container = NULL; }

初始化后,链表中没有普通节点,但哨兵节点已经存在:

end.next = &end; end.prev = &end; numberOfItems = 0;

这表示链表为空。由于end的前后指针都指向自己,所以整个链表形成了一个闭环。

这种设计可以减少很多边界判断。普通链表在插入第一个节点、删除最后一个节点时经常需要单独处理,而带哨兵节点的循环链表可以把空链表、单节点链表、多节点链表统一处理。

八、为什么 end 的 itemValue 是最大值

FreeRTOS 的很多链表需要按照itemValue升序排列。比如延时列表中,itemValue可能表示任务解除阻塞的 tick 值。

end.itemValue被设置为最大值,是为了让哨兵节点永远位于排序链表的末尾。普通节点的值一般不会超过它,所以插入时总能在到达哨兵节点之前找到合适位置。

例如链表中有三个节点:

10 -> 20 -> 30 -> end

end的值是最大值,因此它自然位于最后。

九、列表项初始化

列表项初始化主要是把container设置为空,表示当前节点没有插入任何链表。

/* ============================================ * 八、列表项初始化 * ============================================ */ void ListItem_Init(ListItem_t *item) { /* 将 container 置空,表示当前节点未插入任何链表 */ /* 这是判断节点是否在链表中的重要标志 */ item->container = NULL; }

实际使用时,还需要设置itemValue和owner

/* 设置列表项的排序值 */ void ListItem_SetValue(ListItem_t *item, uint32_t value) { item->itemValue = value; } /* 设置列表项的拥有者(通常传入 TCB 指针) */ void ListItem_SetOwner(ListItem_t *item, void *owner) { item->owner = owner; }

组合起来可以这样使用:

TCB_t task; ListItem_Init(&task.stateListItem); ListItem_SetValue(&task.stateListItem, task.priority); ListItem_SetOwner(&task.stateListItem, &task);

这样,链表节点和任务控制块之间就建立了关系:

ListItem_t.owner -> TCB_t TCB_t.stateListItem -> ListItem_t

通过列表项可以找到任务,通过任务也可以找到自己的列表项。

十、插入到链表尾部

尾插操作常用于就绪列表。比如相同优先级下有多个任务时,可以把新任务插到链表尾部,后续调度时实现时间片轮转。

简化代码如下:

/* ============================================ * 九、插入到链表尾部(常用于就绪列表) * 说明:在 index 指向的位置之前插入新节点 * 若 index 指向哨兵 end,则插入到链表尾部 * ============================================ */ void List_InsertEnd(List_t *list, ListItem_t *newItem) { /* 获取当前遍历游标位置,作为插入的参考点 */ ListItem_t *index = list->index; /* 新节点的 next 指向 index(即插入到 index 前面) */ newItem->next = index; /* 新节点的 prev 指向 index 原来的前一个节点 */ newItem->prev = index->prev; /* 将原前一个节点的 next 指向新节点,完成前向链接 */ index->prev->next = newItem; /* 将 index 的 prev 指向新节点,完成后向链接 */ index->prev = newItem; /* 记录新节点所属的链表,便于后续删除时直接定位 */ newItem->container = list; /* 有效节点数加一 */ list->numberOfItems++; }

这段代码可以分成四步理解:

newItem->next = index;

新节点的下一个节点指向index。

newItem->prev = index->prev;

新节点的前一个节点指向index原来的前一个节点。

index->prev->next = newItem;

原来的尾节点的下一个节点改成新节点。

index->prev = newItem;

index的前一个节点改成新节点。

如果index指向哨兵节点end,那么这次插入就相当于把新节点插到了end前面,也就是链表尾部。

十一、按照 itemValue 升序插入

FreeRTOS 中还有一种常见插入方式:按照itemValue从小到大插入。延时列表就需要这种方式。

简化代码如下:

/* ============================================ * 十、按 itemValue 升序插入(常用于延时列表) * 说明:从哨兵开始向后遍历,找到第一个大于 * newItem->itemValue 的节点,插入其前面 * ============================================ */ void List_InsertSorted(List_t *list, ListItem_t *newItem) { /* 从哨兵节点开始遍历,哨兵在有序链表中位于末尾 */ ListItem_t *iterator = &list->end; /* 向后遍历,直到找到 itemValue 大于新节点的位置 */ /* 循环结束时,iterator 指向新节点应该插入位置的前一个节点 */ while (iterator->next->itemValue <= newItem->itemValue) { iterator = iterator->next; } /* 新节点的 next 指向 iterator 原来的下一个节点 */ newItem->next = iterator->next; /* 新节点的 prev 指向 iterator */ newItem->prev = iterator; /* 将原下一个节点的 prev 指向新节点 */ iterator->next->prev = newItem; /* 将 iterator 的 next 指向新节点,完成插入 */ iterator->next = newItem; /* 标记新节点已插入该链表 */ newItem->container = list; /* 有效节点数加一 */ list->numberOfItems++; }

假设已有链表如下:

10 -> 30 -> end

现在插入一个itemValue = 20的节点。遍历时会发现 20 应该插入 10 和 30 之间,插入后变为:

10 -> 20 -> 30 -> end

如果用于延时列表,数值越小的任务越早被唤醒,调度器检查链表头部即可快速判断是否有任务到期。

十二、从链表中删除节点

删除节点时,只需要传入列表项本身。因为container已经记录了它属于哪个链表。

/* ============================================ * 十一、从链表中删除节点 * 说明:通过 container 字段直接定位所属链表, * 无需从头遍历查找 * ============================================ */ uint32_t List_Remove(ListItem_t *item) { /* 通过 container 反向找到该节点所属的链表头 */ List_t *list = (List_t *)item->container; /* 核心删除操作:让后一个节点的 prev 跳过当前节点 */ item->next->prev = item->prev; /* 核心删除操作:让前一个节点的 next 跳过当前节点 */ /* 这两步执行后,当前节点从链表中脱离 */ item->prev->next = item->next; /* 如果删除的节点正好是游标指向的节点,将游标回退到前一个节点 */ /* 防止游标悬空,保证后续遍历正常 */ if (list->index == item) { list->index = item->prev; } /* 清除 container 标记,表示该节点已不在任何链表中 */ item->container = NULL; /* 有效节点数减一 */ list->numberOfItems--; /* 返回删除后链表的剩余节点数 */ return list->numberOfItems; }

删除过程的核心是两行:

item->next->prev = item->prev; item->prev->next = item->next;

第一行让后一个节点的prev跳过当前节点,指向当前节点的前一个节点。

第二行让前一个节点的next跳过当前节点,指向当前节点的后一个节点。

这样当前节点就从链表中脱离出来了。

删除后需要把container设置为NULL,表示该节点已经不属于任何链表。

十三、遍历链表

FreeRTOS 中遍历链表时,经常通过index游标获取下一个节点对应的 owner。简化示例如下:

/* ============================================ * 十二、遍历链表:获取下一个节点的 owner * 说明:常用于任务调度,返回下一个要运行的任务 TCB * ============================================ */ void *List_GetNextOwner(List_t *list) { /* 游标向后移动一个节点 */ list->index = list->index->next; /* 如果移动后指向哨兵节点,说明已经到达链表末尾 */ /* 跳过哨兵,回到链表头部(循环链表特性) */ if (list->index == &list->end) { list->index = list->index->next; } /* 返回当前游标指向节点的拥有者,即任务控制块 */ return list->index->owner; }

这段逻辑表示:先移动到下一个节点,如果遇到哨兵节点,就跳过哨兵节点,继续移动到真正的第一个节点。

在任务调度中,owner通常是任务控制块,因此这个函数可以返回下一个要调度的任务。

十四、一个完整的小例子

下面用一个简化例子模拟任务挂入链表的过程。

/* ============================================ * 十三、完整示例:模拟任务挂入就绪链表 * ============================================ */ #include <stdio.h> #include <stdint.h> #include <string.h> #define MAX_DELAY 0xffffffffUL /* 列表项结构 */ typedef struct ListItem { uint32_t itemValue; struct ListItem *next; struct ListItem *prev; void *owner; void *container; } ListItem_t; /* 链表管理结构 */ typedef struct List { uint32_t numberOfItems; ListItem_t *index; ListItem_t end; } List_t; /* 简化任务控制块 */ typedef struct TaskControlBlock { char name[16]; uint32_t priority; ListItem_t stateItem; /* 内嵌列表项,用于挂入就绪链表 */ } TCB_t; /* 链表初始化:构造空双向循环链表 */ void List_Init(List_t *list) { list->numberOfItems = 0; list->index = &list->end; /* 游标初始指向哨兵 */ list->end.itemValue = MAX_DELAY; /* 哨兵值最大,确保位于有序链表末尾 */ list->end.next = &list->end; /* 空表:哨兵自己指向自己 */ list->end.prev = &list->end; list->end.owner = NULL; list->end.container = NULL; } /* 列表项初始化:标记为未插入任何链表 */ void ListItem_Init(ListItem_t *item) { item->container = NULL; } /* 尾插法:在 index 之前插入新节点 */ void List_InsertEnd(List_t *list, ListItem_t *item) { ListItem_t *index = list->index; /* 新节点插入到 index 和 index->prev 之间 */ item->next = index; item->prev = index->prev; index->prev->next = item; /* 原尾节点的 next 指向新节点 */ index->prev = item; /* index 的 prev 指向新节点 */ item->container = list; /* 标记所属链表 */ list->numberOfItems++; /* 节点计数加一 */ } /* 任务初始化:设置任务名、优先级,并初始化其列表项 */ void Task_Init(TCB_t *task, const char *name, uint32_t priority) { /* 拷贝任务名,并确保字符串以 '\0' 结尾,防止越界 */ strncpy(task->name, name, sizeof(task->name) - 1); task->name[sizeof(task->name) - 1] = '\0'; task->priority = priority; /* 初始化列表项,此时未插入任何链表 */ ListItem_Init(&task->stateItem); /* 在就绪链表中,通常用优先级作为排序值 */ task->stateItem.itemValue = priority; /* 列表项的 owner 指向任务本身,便于从链表节点找回 TCB */ task->stateItem.owner = task; } /* 打印链表:遍历除哨兵外的所有节点,输出任务信息 */ void Print_List(List_t *list) { /* 从哨兵的下一个节点开始遍历(即链表第一个有效节点) */ ListItem_t *item = list->end.next; /* 当遍历回到哨兵时,说明链表已遍历完毕 */ while (item != &list->end) { /* 通过 owner 指针获取对应的任务控制块 */ TCB_t *task = (TCB_t *)item->owner; printf("task name: %s, priority: %lu\r\n", task->name, (unsigned long)task->priority); /* 移动到下一个节点 */ item = item->next; } } /* 主函数:演示链表初始化、任务创建和插入过程 */ int main(void) { List_t readyList; /* 定义就绪链表 */ TCB_t task1; /* 定义三个任务控制块 */ TCB_t task2; TCB_t task3; /* 步骤 1:初始化链表,构造空表 */ List_Init(&readyList); /* 步骤 2:初始化三个任务,分别设置名称和优先级 */ Task_Init(&task1, "LED", 1); /* 任务 LED,优先级 1 */ Task_Init(&task2, "UART", 2); /* 任务 UART,优先级 2 */ Task_Init(&task3, "KEY", 1); /* 任务 KEY,优先级 1 */ /* 步骤 3:将三个任务的 stateItem 依次挂入就绪链表尾部 */ /* 插入顺序:LED -> UART -> KEY */ List_InsertEnd(&readyList, &task1.stateItem); List_InsertEnd(&readyList, &task2.stateItem); List_InsertEnd(&readyList, &task3.stateItem); /* 步骤 4:遍历并打印链表内容,验证插入结果 */ Print_List(&readyList); return 0; }

这个例子体现了 FreeRTOS 链表设计的核心思想:链表中挂入的是ListItem_t,但通过owner可以找到真正的任务控制块。

十五、FreeRTOS 链表与任务调度的关系

FreeRTOS 中常见的任务列表包括:

列表作用
就绪列表保存已经准备好运行的任务
延时列表保存正在延时或等待超时的任务
挂起列表保存被挂起的任务
事件等待列表保存等待队列、信号量、事件组的任务

例如,一个任务创建成功后,会被插入就绪列表。任务调用vTaskDelay()后,会从就绪列表移除,再插入延时列表。系统 tick 中断到来时,内核会检查延时列表中是否有任务到期。如果到期,就把任务从延时列表移除,再放回就绪列表。

这个过程可以概括为:

任务运行 -> 调用延时函数 -> 从就绪列表删除 -> 插入延时列表 -> tick 到期 -> 从延时列表删除 -> 插入就绪列表 -> 等待调度器再次运行

所以,任务状态切换的底层,本质上就是列表项在不同链表之间移动。

十六、学习这部分时容易混淆的点

List_t不是普通节点,而是链表管理结构。

ListItem_t才是真正挂入链表的普通节点。

xListEnd不是任务节点,而是哨兵节点。

pvOwner指向拥有该列表项的对象,通常是 TCB。

pxContainer指向该列表项当前所在的链表。

vListInitialise()初始化链表。

vListInitialiseItem()初始化列表项。

vListInsertEnd()通常用于尾插。

vListInsert()通常用于按xItemValue升序插入。

uxListRemove()用于把节点从所在链表中移除。

相关新闻

  • 为什么DyscheOS-meta仓库被关闭?核心原因与社区影响全解析
  • openEuler Jenkins入门指南:如何为你的开源项目配置自动化门禁检查
  • AI实时抠像技术革新:obs-backgroundremoval插件如何实现无绿幕专业直播

最新新闻

  • 解决Windows 10下PL2303驱动问题的完整指南:让老旧串口设备重获新生
  • 深度学习论文实验设计:从48次失败到成功涨点的3个核心策略与5个数据集处理技巧
  • 谜语大全 API 接入教程:随机查询与分页列表实践
  • 直播间数据监控深度解析:从实时弹幕到用户行为分析的全栈实战指南
  • 智能车竞赛应该是软硬不分家的
  • NBM7100A与PIC18F2458的低功耗电源管理方案

日新闻

  • PROPKA 3深度解析:蛋白质pKa预测的实战指南与算法原理
  • 微信小程序 globalData 监听:基于 Object.defineProperty 的 3 种实现方案对比
  • MySQL 8.0 数据清洗实战:3类异常值识别与 UPDATE/DELETE 批量处理

周新闻

  • 基于YOLOv12的番茄成熟度智能检测系统开发
  • 终极RimWorld模组管理指南:用RimSort告别模组冲突烦恼
  • AI Agent框架开发:从理论到实践的完整指南

月新闻

  • 2026年6月公司网站搭建最新热门渠道测评:四大低成本/零代码平台对比+避坑
  • 【Linux】Linux arm 编译QT程序,出现expected “}“报错
  • 【MATLAB例程】四基站二维AOA定位与距离辅助增强对比仿真。基于角度观测和测距修正的固定目标平面定位精度分析

关于尧图

  • 公司简介
  • 团队介绍
  • 企业文化
  • 荣誉资质

服务项目

  • 定制开发
  • 电商建站
  • UI 设计
  • 运维服务

快速链接

  • 案例展示
  • 建站流程
  • 常见问题
  • 资讯中心

联系方式

  • 📍北京市朝阳区互联网产业园 A 座 10 层
  • 📞400-888-8888
  • ✉️contact@rkmt.cn
  • 🕐周一至周日 9:00-21:00

© 2024 北京尧图网络科技有限公司 版权所有 | 京 ICP 备 XXXXXXXX 号