FreeRTOS——列表与列表项

撕开 FreeRTOS 内核第一层:列表与列表项到底是干嘛的?

这段时间在啃 FreeRTOS 内核源码,发现几乎所有核心功能(任务调度、延时、消息队列、信号量) ,底层全靠一个东西撑着 ------列表(List)和列表项(ListItem)

不搞懂它,看源码就是看天书;搞懂了,FreeRTOS 对你就不再神秘。

所以我专门写了一段纯实验代码,手动初始化、插入、删除、尾部插入列表项,把地址全部打印出来,一步一步看链表到底怎么连、怎么断、怎么排。

这篇我就用最接地气、最不绕弯的方式,把理论 + 代码 + 运行逻辑一次性讲透。

一、先搞懂:FreeRTOS 列表与列表项是什么?

你不用记复杂概念,我用一句话总结:

列表 = 一条有序的双向循环链(带哨兵)

列表项 = 链上的每一个节点

1. 列表(List_t)是干嘛的?

它是链表的管理员,负责:

  • 记录链上有多少节点
  • 记录当前遍历到哪了
  • 固定一个尾哨兵(xListEnd),让链表永远闭环

结构大概长这样(简化版):

复制代码
typedef struct xLIST
{
    UBaseType_t uxNumberOfItems;   // 节点数量
    ListItem_t  * pxIndex;         // 遍历指针
    MiniListItem_t xListEnd;       // 尾哨兵(永远在最后)
} List_t;

2. 列表项(ListItem_t)是干嘛的?

它是链上的真正数据节点,FreeRTOS 的任务、队列、事件,全靠它挂在链上。

每个节点有:

  • 排序值(xItemValue)

  • 指向前一个节点

  • 指向后一个节点

  • 属于哪个对象(任务 / 队列 / 信号量)

    typedef struct xLIST_ITEM
    {
    TickType_t xItemValue; // 排序用的键值
    struct xLIST_ITEM * pxNext; // 下一个
    struct xLIST_ITEM * pxPrevious;// 前一个
    void * pvOwner; // 所属者(如任务TCB)
    void * pvContainer; // 属于哪个链表
    } ListItem_t;

3. FreeRTOS 链表最关键特点

  1. 双向:能往前,也能往后
  2. 循环:最后一个节点 → 哨兵 → 第一个节点
  3. 带哨兵:永远不为空,永远不乱
  4. 自动排序:插入时按 xItemValue 从小到大排

二、这篇代码到底在干什么?

我写这段代码不是为了实现功能,而是为了学习内核底层原理

我一共做了 5 件事:

  1. 初始化列表和 3 个列表项
  2. 给每个列表项设置排序值(40、60、50)
  3. 依次插入 3 个列表项,观察指针连接
  4. 删除一个列表项,观察链表如何重新连接
  5. 尾部插入一个列表项,不参与排序

每一步我都把地址打印出来,亲眼看到链表怎么连、怎么断、怎么修。

这就是学习 FreeRTOS 内核最有效的方法

三、完整代码(可直接跑)

复制代码
#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "led.h"
#include "FreeRTOS.h"
#include "task.h"

// 开始任务
#define START_TASK_SIZE 128
#define START_TASK_PRIO 1
TaskHandle_t StartTask_Hander;
void start_task( void * pvParameters );

// 任务1:LED闪烁(无关紧要,只是让程序活着)
#define TASK1_TASK_SIZE 128
#define TASK1_TASK_PRIO 2
TaskHandle_t Task1Task_Hander;
void task1_task( void * pvParameters );

// 列表测试任务(核心!)
#define LIST_TASK_SIZE 128
#define LIST_TASK_PRIO 3
TaskHandle_t ListTask_Hander;
void list_task( void * pvParameters );

// 定义一个链表 + 3个链表节点
List_t TestList;
ListItem_t ListItem1;
ListItem_t ListItem2;
ListItem_t ListItem3;

int main(void)
{
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
    delay_init();
    uart_init(115200);
    LED_Init();

    // 创建开始任务
    xTaskCreate(start_task,  "start_task", START_TASK_SIZE, NULL, START_TASK_PRIO, &StartTask_Hander );	    
    vTaskStartScheduler(); // 开启调度
}

// 开始任务:创建完任务就自杀
void start_task( void * pvParameters )
{
    taskENTER_CRITICAL();

    xTaskCreate(task1_task,   "task1_task", TASK1_TASK_SIZE, NULL, TASK1_TASK_PRIO, &Task1Task_Hander );
    xTaskCreate(list_task,    "list_task",  LIST_TASK_SIZE,  NULL, LIST_TASK_PRIO,  &ListTask_Hander );

    vTaskDelete(StartTask_Hander);
    taskEXIT_CRITICAL();
}

// 任务1:LED闪烁,和链表无关,只是保证系统在跑
void task1_task( void * pvParameters )
{
    while(1)
    {
        LED0= ~LED0;
        vTaskDelay(1000);
    }
}

// ========================== 链表核心测试任务 ==========================
void list_task( void * pvParameters )
{
    // 1. 初始化链表(让哨兵自循环)
    vListInitialise( &TestList );

    // 2. 初始化3个列表项(清空指针)
    vListInitialiseItem( &ListItem1 );
    vListInitialiseItem( &ListItem2 );
    vListInitialiseItem( &ListItem3 );

    // 3. 设置排序值(插入时会按这个从小到大排序)
    ListItem1.xItemValue = 40;
    ListItem2.xItemValue = 60;
    ListItem3.xItemValue = 50;

    // 打印初始地址,方便观察后面指针变化
    printf("--------------------列表与列表项地址--------------\r\n");
    printf("TestList                   %#x\r\n",(int)&TestList);
    printf("TestList->pxIndex          %#x\r\n",(int)TestList.pxIndex);
    printf("TestList->xListEnd         %#x\r\n",(int)&TestList.xListEnd);
    printf("ListItem1                  %#x\r\n",(int)&ListItem1);
    printf("ListItem2                  %#x\r\n",(int)&ListItem2);
    printf("ListItem3                  %#x\r\n",(int)&ListItem3);
    printf("---------------------------------------------------\r\n");

    // ===================== 插入列表项1 =====================
	vListInsert( &TestList, &ListItem1);
	printf("-----------------------添加列表项ListItem1----------------\r\n");
	printf("TestList->xListEnd->pxNext          %#x                   \r\n",(int)TestList.xListEnd.pxNext);
	printf("ListItem1->pxNext                   %#x                   \r\n",(int)ListItem1.pxNext);
	printf("-------------------------前后连接-------------------------\r\n");
	printf("TestList->xListEnd->pxPrevious      %#x                   \r\n",(int)TestList.xListEnd.pxPrevious);
	printf("ListItem1->pxPrevious               %#x                   \r\n",(int)ListItem1.pxPrevious);
	printf("-----------------------------结束-------------------------\r\n");
	
	// ===================== 插入列表项2 =====================
	vListInsert( &TestList, &ListItem2);
	printf("-----------------------添加列表项ListItem2----------------\r\n");
	printf("TestList->xListEnd->pxNext          %#x                   \r\n",(int)TestList.xListEnd.pxNext);
	printf("ListItem1->pxNext                   %#x                   \r\n",(int)ListItem1.pxNext);
	printf("ListItem2->pxNext                   %#x                   \r\n",(int)ListItem2.pxNext);
	printf("-------------------------前后连接-------------------------\r\n");
	printf("TestList->xListEnd->pxPrevious      %#x                   \r\n",(int)TestList.xListEnd.pxPrevious);
	printf("ListItem1->pxPrevious               %#x                   \r\n",(int)ListItem1.pxPrevious);
	printf("ListItem2->pxPrevious               %#x                   \r\n",(int)ListItem2.pxPrevious);
	printf("-----------------------------结束-------------------------\r\n");
	
	// ===================== 插入列表项3 =====================
	vListInsert( &TestList, &ListItem3);
	printf("-----------------------添加列表项ListItem3----------------\r\n");
	printf("TestList->xListEnd->pxNext          %#x                   \r\n",(int)TestList.xListEnd.pxNext);
	printf("ListItem1->pxNext                   %#x                   \r\n",(int)ListItem1.pxNext);
	printf("ListItem2->pxNext                   %#x                   \r\n",(int)ListItem2.pxNext);
	printf("ListItem3->pxNext                   %#x                   \r\n",(int)ListItem3.pxNext);
	printf("-------------------------前后连接-------------------------\r\n");
	printf("TestList->xListEnd->pxPrevious      %#x                   \r\n",(int)TestList.xListEnd.pxPrevious);
	printf("ListItem1->pxPrevious               %#x                   \r\n",(int)ListItem1.pxPrevious);
	printf("ListItem2->pxPrevious               %#x                   \r\n",(int)ListItem2.pxPrevious);
  printf("ListItem3->pxPrevious               %#x                   \r\n",(int)ListItem3.pxPrevious);
	printf("-----------------------------结束-------------------------\r\n");
	
	// ===================== 删除 ListItem2 =====================
	uxListRemove(&ListItem2);
	printf("-----------------------删除列表项ListItem2----------------\r\n");
	printf("TestList->xListEnd->pxNext          %#x                   \r\n",(int)TestList.xListEnd.pxNext);
	printf("ListItem1->pxNext                   %#x                   \r\n",(int)ListItem1.pxNext);
	printf("ListItem3->pxNext                   %#x                   \r\n",(int)ListItem3.pxNext);
	printf("-------------------------前后连接-------------------------\r\n");
	printf("TestList->xListEnd->pxPrevious      %#x                   \r\n",(int)TestList.xListEnd.pxPrevious);
	printf("ListItem1->pxPrevious               %#x                   \r\n",(int)ListItem1.pxPrevious);
	printf("ListItem3->pxPrevious               %#x                   \r\n",(int)ListItem3.pxPrevious);
	printf("-----------------------------结束-------------------------\r\n");
	
	// ===================== 尾部插入 ListItem2(不排序) =====================
	TestList.pxIndex = TestList.pxIndex->pxNext;
	vListInsertEnd(&TestList,&ListItem2);
	printf("--------------------尾部插入列表项ListItem2----------------\r\n");
	printf("TestList->pxIndex                   %#x                   \r\n",(int)TestList.pxIndex);
	printf("TestList->xListEnd->pxNext          %#x                   \r\n",(int)TestList.xListEnd.pxNext);
	printf("ListItem1->pxNext                   %#x                   \r\n",(int)ListItem1.pxNext);
	printf("ListItem2->pxNext                   %#x                   \r\n",(int)ListItem2.pxNext);
	printf("ListItem3->pxNext                   %#x                   \r\n",(int)ListItem3.pxNext);
	printf("-------------------------前后连接-------------------------\r\n");
	printf("TestList->xListEnd->pxPrevious      %#x                   \r\n",(int)TestList.xListEnd.pxPrevious);
	printf("ListItem1->pxPrevious               %#x                   \r\n",(int)ListItem1.pxPrevious);
	printf("ListItem2->pxPrevious               %#x                   \r\n",(int)ListItem2.pxPrevious);
	printf("ListItem3->pxPrevious               %#x                   \r\n",(int)ListItem3.pxPrevious);
	printf("-----------------------------结束-------------------------\r\n");

    while(1)
    {

    }
}

四、代码每一步到底在干嘛?

我不跟你绕内核原理,直接告诉你每一步的真实作用

1. 初始化列表

复制代码
vListInitialise( &TestList );

让链表的尾哨兵自己指向自己,形成一个空循环链表。

2. 初始化列表项

复制代码
vListInitialiseItem(&ListItem1);

清空节点里的所有指针,让它变成一个干净节点。

3. 设置排序值

复制代码
ListItem1.xItemValue = 40;
ListItem2.xItemValue = 60;
ListItem3.xItemValue = 50;

这是插入顺序的依据,FreeRTOS 会自动从小到大排序。

最终顺序: 40 → 50 → 60

4. 依次插入三个节点

复制代码
vListInsert( &TestList, &ListItem1 );
vListInsert( &TestList, &ListItem2 );
vListInsert( &TestList, &ListItem3 );

每插入一个,内核自动找到它该在的位置,并修改前后指针双向连接。

插入后顺序: ListItem1 (40) → ListItem3 (50) → ListItem2 (60) → 哨兵

5. 删除节点

复制代码
uxListRemove(&ListItem2);

把节点从链上摘掉,并自动把前后节点重新连接

6. 尾部插入(不排序)

复制代码
vListInsertEnd(&TestList,&ListItem2);

直接插到哨兵前面,不按大小排序。

五、你看到的串口输出是什么意思?

你串口看到的一堆地址,其实就是在展示:

  • 谁指向谁
  • 插入时怎么连
  • 删除时怎么断
  • 尾部插入时怎么加

这就是双向循环链表最真实的运行样子

1、先记住几个关键地址

先把这些地址记下来,后面每一步变化都要用到:

项目 地址
TestList 0x200000b4
TestList->pxIndex 0x200000bc
TestList->xListEnd(哨兵节点) 0x200000bc
ListItem1 0x200000c8
ListItem2 0x200000dc
ListItem3 0x200000f0

注:TestList->pxIndex 初始时指向 xListEnd,所以两者地址一样。

2、步骤 1:添加 ListItem1

这是第一次调用 vListInsert(&TestList, &ListItem1) 后的结果:

项目 地址 含义
TestList->xListEnd->pxNext 0x200000c8 哨兵的下一个是 ListItem1
ListItem1->pxNext 0x200000bc ListItem1 的下一个是哨兵
TestList->xListEnd->pxPrevious 0x200000c8 哨兵的上一个是 ListItem1
ListItem1->pxPrevious 0x200000bc ListItem1 的上一个是哨兵

现象解读:

  • 哨兵 xListEndListItem1 形成了一个双向循环。
  • 链表现在的顺序:哨兵 ↔ ListItem1

3、步骤 2:添加 ListItem2

第二次调用 vListInsert(&TestList, &ListItem2)xItemValue=60):

项目 地址 含义
TestList->xListEnd->pxNext 0x200000c8 哨兵的下一个还是 ListItem1
ListItem1->pxNext 0x200000dc ListItem1 的下一个变成了 ListItem2
ListItem2->pxNext 0x200000bc ListItem2 的下一个是哨兵
TestList->xListEnd->pxPrevious 0x200000dc 哨兵的上一个变成了 ListItem2
ListItem1->pxPrevious 0x200000bc ListItem1 的上一个还是哨兵
ListItem2->pxPrevious 0x200000c8 ListItem2 的上一个是 ListItem1

现象解读:

  • 因为 ListItem2 的值(60)比 ListItem1(40)大,所以它被插入到 ListItem1 和哨兵之间。
  • 链表现在的顺序:哨兵 ↔ ListItem1 ↔ ListItem2

4、步骤 3:添加 ListItem3

第三次调用 vListInsert(&TestList, &ListItem3)xItemValue=50):

项目 地址 含义
ListItem1->pxNext 0x200000f0 ListItem1 的下一个变成了 ListItem3
ListItem2->pxNext 0x200000bc ListItem2 的下一个还是哨兵
ListItem3->pxNext 0x200000dc ListItem3 的下一个是 ListItem2
TestList->xListEnd->pxPrevious 0x200000dc 哨兵的上一个还是 ListItem2
ListItem1->pxPrevious 0x200000bc ListItem1 的上一个还是哨兵
ListItem2->pxPrevious 0x200000f0 ListItem2 的上一个变成了 ListItem3
ListItem3->pxPrevious 0x200000c8 ListItem3 的上一个是 ListItem1

现象解读:

  • ListItem3 的值(50)介于 40 和 60 之间,所以它被插入到 ListItem1ListItem2 中间。
  • 链表现在的顺序:哨兵 ↔ ListItem1 ↔ ListItem3 ↔ ListItem2

5、步骤 4:删除 ListItem2

调用 uxListRemove(&ListItem2) 后的结果:

项目 地址 含义
ListItem1->pxNext 0x200000f0 不变,仍是 ListItem3
ListItem3->pxNext 0x200000bc ListItem3 的下一个直接指向哨兵
TestList->xListEnd->pxPrevious 0x200000f0 哨兵的上一个变成了 ListItem3
ListItem3->pxPrevious 0x200000c8 不变,仍是 ListItem1

现象解读:

  • ListItem2 被从链上摘除了,ListItem3 直接和哨兵连接。
  • 链表现在的顺序:哨兵 ↔ ListItem1 ↔ ListItem3

6、步骤 5:尾部插入 ListItem2

调用 vListInsertEnd(&TestList, &ListItem2) 后的结果:

项目 地址 含义
TestList->pxIndex 0x200000c8 指向 ListItem1
TestList->xListEnd->pxNext 0x200000dc 哨兵的下一个变成了 ListItem2
ListItem1->pxNext 0x200000f0 不变,仍是 ListItem3
ListItem2->pxNext 0x200000c8 ListItem2 的下一个是 ListItem1(形成循环)
ListItem3->pxNext 0x200000bc 不变,仍是哨兵
TestList->xListEnd->pxPrevious 0x200000f0 哨兵的上一个还是 ListItem3
ListItem1->pxPrevious 0x200000dc ListItem1 的上一个变成了 ListItem2
ListItem2->pxPrevious 0x200000bc ListItem2 的上一个是哨兵
ListItem3->pxPrevious 0x200000c8 不变,仍是 ListItem1

现象解读:

  • vListInsertEnd 不按 xItemValue 排序,而是直接插到 pxIndex 指向节点的前面(这里是哨兵前面)。
  • 链表现在的顺序:哨兵 ↔ ListItem2 ↔ ListItem1 ↔ ListItem3
  • 注意:ListItem2 被插到了链表的尾部,成为了新的 "最后一个节点"。

六、学这个有什么用?

一句大实话:

FreeRTOS 内核 70% 的代码,都在操作链表

  • 任务延时列表
  • 任务就绪列表
  • 阻塞列表
  • 挂起列表
  • 队列、信号量、事件组

全是链表!

今天看懂了这段代码,以后看 FreeRTOS 源码:

  • 不再害怕指针
  • 不再害怕任务切换
  • 不再害怕内核原理

七、总结

  1. 列表 = 双向循环链
  2. 列表项 = 链上的节点
  3. vListInsert = 按排序值插入
  4. uxListRemove = 删除节点
  5. vListInsertEnd = 尾部插入(不排序)

这就是 FreeRTOS 最底层、最核心的数据结构。

相关推荐
总结所学1 小时前
电路定理 叠加定理 基尔霍夫定律
单片机·嵌入式硬件
加成BUFF4 小时前
机器人专业2025年12月5日《嵌入式系统STM32》期末考试范围+试卷
stm32·嵌入式·期末复习
雪度娃娃4 小时前
存储器层次结构——随机访问存储器
单片机·嵌入式硬件·计算机组成原理
少年潜行5 小时前
ESP01S使用笔记05--ESP01S 进行 MQTT 通信 发送 JSON 字符串遇到的问题
单片机
项目題供诗6 小时前
STM32-PWM驱动LED呼吸灯&PWM驱动直流电机(十三)
stm32·单片机·嵌入式硬件
不脱发的程序猿6 小时前
如何让Skill同时跑在Cursor、Codex和Claude Code里?
单片机·嵌入式硬件·嵌入式
longxiangam6 小时前
esp-idf dsi 屏幕的驱动实现原理—— 关于零拷贝和 DMA 永续刷新
c语言·单片机·嵌入式硬件
星夜夏空996 小时前
FreeRTOS学习(6)——任务创建
单片机·嵌入式硬件·学习
Lance_mu8 小时前
UFS协议学习大纲
嵌入式硬件·七牛云存储