1. 引言:为什么需要理解列表与列表项?

在 FreeRTOS 的源码海洋中,列表(List)和列表项(ListItem)是最基础、最核心的数据结构之一。它们如同操作系统的"骨架",贯穿于任务调度、队列管理、事件组等核心模块。无论是为了深入阅读 FreeRTOS 源码,还是立志从零开始实现一个自己的 RTOS,彻底掌握列表和列表项的操作都是必经之路。
好消息是,它并没有想象中那么难。列表和列表项直接对应我们 C 语言中熟悉的链表节点。在后续的讲解中,我们所说的“链表”就是列表,“节点”就是列表项。

2. C 语言链表简介

链表作为一种基础的数据结构,在普通的应用程序中可能使用频率不高,但在操作系统的世界里,它却是无处不在的核心组件。

2.1 生动的比喻:晾衣架

理解链表,可以借助一个生动的比喻:圆形的晾衣架(见图 6-1)。

  • 钩子:晾衣架上有很多钩子,它们首尾相连。这对应链表中的节点,节点之间通过指针连接。
  • 挂载功能:钩子本身不能代表很多东西,但它可以挂载衣服、毛巾等物品。同样,链表的节点本身不存储大量数据,其核心作用是作为一个"挂钩",用于挂载真正需要管理的数据结构(如任务控制块、消息等)。### 2.2 链表的种类

链表主要分为两类:

  1. 单向链表:每个节点只有一个指针,指向下一个节点。
  2. 双向链表:每个节点有两个指针,分别指向前一个节点和后一个节点。

在 FreeRTOS 以及绝大多数操作系统中,双向链表因其操作的灵活性而被广泛采用。

2.3 单向链表与循环链表

您提供的图 6-2 展示了一种特殊的单向链表:循环链表。> “该链表中共有 n 个节点,前一个节点都有一个箭头指向后一个节点,首尾相连,组成一个圈。”

标准单向链表的尾节点指向 NULL,而循环链表则让尾节点的指针指回头节点,形成一个闭环。这种结构适用于需要轮询或循环访问的场景。

下面用Mermaid图展示单向链表和循环链表的区别:

单向链表

next →

next →

next → NULL

节点A

节点B

节点C

NULL

循环链表

next →

next →

next →

首尾相连

节点A

节点B

节点C

尾节点指回头节点,形成闭环

尾节点指向NULL

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图展示双向链表的结构:

双向链表结构

prev 指针

prev 指针

prev 指针

prev 指针

根节点 (生产者)
count = 3

节点A
prev: 根节点
next: 节点B

节点B
prev: 节点A
next: 节点C

节点C
prev: 节点B
next: 根节点

双向链表特点:
- 每个节点有 prev 和 next 两个指针
- 可双向遍历
- 插入/删除操作更方便

2.7 链表 vs. 数组

直观地对比二者:

特性 数组 (Array) 链表 (Linked List)
内存组织 一段连续的内存空间。 离散的内存块,通过指针“链接”成表。
大小 创建时通常需要指定固定大小。 动态,可随时增删节点。
访问方式 随机访问,通过下标可直接定位,时间复杂度 O(1)。 顺序访问,要访问第 i 个元素必须从头遍历,时间复杂度 O(n)。
插入/删除 在中间插入或删除需要移动大量元素,效率低 O(n)。 在已知节点位置时,只需修改指针,效率高 O(1)。
内存开销 只有数据本身的开销。 每个节点都有额外的指针开销。
缓存友好性 好,连续内存利于CPU缓存。 差,节点分散不利于缓存。
结构形态 线性,有明确的起始和结束地址。 可以构成一个“圈”(循环链表),常规定义一个根节点作为操作入口。

核心区别:数组是“一体”的,链表是“串联”的。数组的优势在于快速访问,链表的优势在于动态调整。

下面用Mermaid图直观对比数组和链表的内存布局:

内存连续

内存分散

链表内存布局

指针开销

指针开销

指针开销

指针开销

节点A
地址: 0x2000
next: 0x3000

节点B
地址: 0x3000
next: 0x5000

节点C
地址: 0x5000
next: 0x7000

节点D
地址: 0x7000
next: NULL

8-16字节

8-16字节

8-16字节

8-16字节

数组内存布局

arr[0]
地址: 0x1000

arr[1]
地址: 0x1004

arr[2]
地址: 0x1008

arr[3]
地址: 0x100C

arr[4]
地址: 0x1010

连续内存空间
固定大小
随机访问O(1)

0x1000-0x1010
连续区域

离散内存块
动态大小
顺序访问O(n)

0x2000, 0x3000, 0x5000, 0x7000
分散地址

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐