撕开 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 链表最关键特点
- 双向:能往前,也能往后
- 循环:最后一个节点 → 哨兵 → 第一个节点
- 带哨兵:永远不为空,永远不乱
- 自动排序:插入时按 xItemValue 从小到大排
二、这篇代码到底在干什么?
我写这段代码不是为了实现功能,而是为了学习内核底层原理。
我一共做了 5 件事:
- 初始化列表和 3 个列表项
- 给每个列表项设置排序值(40、60、50)
- 依次插入 3 个列表项,观察指针连接
- 删除一个列表项,观察链表如何重新连接
- 尾部插入一个列表项,不参与排序
每一步我都把地址打印出来,亲眼看到链表怎么连、怎么断、怎么修。
这就是学习 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 的上一个是哨兵 |
现象解读:
- 哨兵
xListEnd和ListItem1形成了一个双向循环。 - 链表现在的顺序:
哨兵 ↔ 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 之间,所以它被插入到ListItem1和ListItem2中间。- 链表现在的顺序:
哨兵 ↔ 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 源码:
- 不再害怕指针
- 不再害怕任务切换
- 不再害怕内核原理
七、总结
- 列表 = 双向循环链
- 列表项 = 链上的节点
- vListInsert = 按排序值插入
- uxListRemove = 删除节点
- vListInsertEnd = 尾部插入(不排序)
这就是 FreeRTOS 最底层、最核心的数据结构。