FreeRTOS 内核探秘(一):深入理解列表与列表项
1. 引言:为什么需要理解列表与列表项?
在 FreeRTOS 的源码海洋中,列表(List)和列表项(ListItem)是最基础、最核心的数据结构之一。它们如同操作系统的"骨架",贯穿于任务调度、队列管理、事件组等核心模块。无论是为了深入阅读 FreeRTOS 源码,还是立志从零开始实现一个自己的 RTOS,彻底掌握列表和列表项的操作都是必经之路。
好消息是,它并没有想象中那么难。列表和列表项直接对应我们 C 语言中熟悉的链表和节点。在后续的讲解中,我们所说的“链表”就是列表,“节点”就是列表项。
2. C 语言链表简介
链表作为一种基础的数据结构,在普通的应用程序中可能使用频率不高,但在操作系统的世界里,它却是无处不在的核心组件。
2.1 生动的比喻:晾衣架
理解链表,可以借助一个生动的比喻:圆形的晾衣架(见图 6-1)。
- 钩子:晾衣架上有很多钩子,它们首尾相连。这对应链表中的节点,节点之间通过指针连接。
- 挂载功能:钩子本身不能代表很多东西,但它可以挂载衣服、毛巾等物品。同样,链表的节点本身不存储大量数据,其核心作用是作为一个"挂钩",用于挂载真正需要管理的数据结构(如任务控制块、消息等)。### 2.2 链表的种类
链表主要分为两类:
- 单向链表:每个节点只有一个指针,指向下一个节点。
- 双向链表:每个节点有两个指针,分别指向前一个节点和后一个节点。
在 FreeRTOS 以及绝大多数操作系统中,双向链表因其操作的灵活性而被广泛采用。
2.3 单向链表与循环链表
您提供的图 6-2 展示了一种特殊的单向链表:循环链表。> “该链表中共有 n 个节点,前一个节点都有一个箭头指向后一个节点,首尾相连,组成一个圈。”
标准单向链表的尾节点指向 NULL,而循环链表则让尾节点的指针指回头节点,形成一个闭环。这种结构适用于需要轮询或循环访问的场景。
下面用Mermaid图展示单向链表和循环链表的区别:
2.4 节点的结构
节点本身是一个自定义的结构体(struct),它必须包含至少一个节点指针(用于指向下一个或上一个节点)。除此之外,节点还可以携带私有信息。
代码清单 6-1:一种简单的节点结构(不常用)
struct node {
struct node *next; // 必须有的:指向下一个节点的指针
int data; // 携带的数据:可以是基本类型
char name[20]; // 携带的数据:可以是数组
// ... 其他自定义成员
};
然而,更常见、更优雅的做法是:节点结构体只包含连接用的指针。而需要被链表管理的数据结构,则通过“内嵌”一个节点结构体成员的方式,“挂”到链表上。
FreeRTOS 风格对比:
您提供的 struct xLIST_ITEM2 代码片段与 FreeRTOS 实际的 ListItem_t 定义非常相似,都体现了“数据内嵌节点”的思想。在 FreeRTOS 源码中,列表项(ListItem_t)正是这样一个内嵌在任务控制块(TCB_t)等数据结构中的节点。不过,FreeRTOS 的实现通常更直接:ListItem_t 结构体本身除了指向前后节点的指针(pxNext, pxPrevious),还直接包含了辅助排序的值(xItemValue)以及指向拥有者和所在链表的指针(pvOwner, pvContainer)。这种设计将节点信息和部分管理数据融合在一起,而非完全分离的“纯钩子”。但核心逻辑一致:通过操作内嵌的列表项,来管理其所属的“数据”(如任务)。
/* FreeRTOS 中列表项(节点)的典型定义(简化) */
struct xLIST_ITEM
{
TickType_t xItemValue; /* 辅助值,用于帮助节点做顺序排列 */
struct xLIST_ITEM * pxNext; /* 指向链表下一个节点 */
struct xLIST_ITEM * pxPrevious; /* 指向链表前一个节点 */
void * pvOwner; /* 指向拥有该节点的内核对象,通常是 TCB */
void * pvContainer; /* 指向该节点所在的链表 */
};
typedef struct xLIST_ITEM ListItem_t;
因此,FreeRTOS 正是采用这种“数据内嵌节点”的风格。它没有将节点指针与数据完全分离,而是将节点作为数据结构的一部分,并通过指针关联到所属对象和链表,以实现高效的管理。
这就完美契合了“晾衣架”的比喻:my_data 是衣服,list_node 是衣服上的挂钩,链表就是晾衣架本身。通过操作 node 这个“挂钩”,就能实现对 my_data 的插入、删除和遍历。
2.5 链表的根节点与操作
为了让链表操作更顺畅,我们通常会人为地定义一个根节点(Root Node),有时也称为生产者或链表头。
- 作用:作为链表操作的入口点,它本身不存储业务数据。
- 扩展:根节点结构体里通常会包含一个节点计数器,用于快速获取链表中元素的数量(见图 6-4 中的
root_node)。
链表的常规操作主要包括:
- 节点的插入(头部插入、尾部插入、中间插入)
- 节点的删除
- 节点的遍历
关于这些操作的具体代码实现,我们将在分析 FreeRTOS 源码时详细展开。
2.6 双向链表
双向链表与单向链表的唯一区别,在于其节点中有两个指针:prev 指向前驱节点,next 指向后继节点。这使得它可以双向遍历,插入和删除操作也更方便(无需再记录前驱节点)。其示意图见图 6-5。
下面用Mermaid图展示双向链表的结构:
2.7 链表 vs. 数组
直观地对比二者:
| 特性 | 数组 (Array) | 链表 (Linked List) |
|---|---|---|
| 内存组织 | 一段连续的内存空间。 | 离散的内存块,通过指针“链接”成表。 |
| 大小 | 创建时通常需要指定固定大小。 | 动态,可随时增删节点。 |
| 访问方式 | 随机访问,通过下标可直接定位,时间复杂度 O(1)。 | 顺序访问,要访问第 i 个元素必须从头遍历,时间复杂度 O(n)。 |
| 插入/删除 | 在中间插入或删除需要移动大量元素,效率低 O(n)。 | 在已知节点位置时,只需修改指针,效率高 O(1)。 |
| 内存开销 | 只有数据本身的开销。 | 每个节点都有额外的指针开销。 |
| 缓存友好性 | 好,连续内存利于CPU缓存。 | 差,节点分散不利于缓存。 |
| 结构形态 | 线性,有明确的起始和结束地址。 | 可以构成一个“圈”(循环链表),常规定义一个根节点作为操作入口。 |
核心区别:数组是“一体”的,链表是“串联”的。数组的优势在于快速访问,链表的优势在于动态调整。
下面用Mermaid图直观对比数组和链表的内存布局:
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)