欢迎光临
我们一直在努力

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 链表最关键特点

  • 双向:能往前,也能往后
  • 循环:最后一个节点 → 哨兵 → 第一个节点
  • 带哨兵:永远不为空,永远不乱
  • 自动排序:插入时按 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 最底层、最核心的数据结构。

    赞(0)
    未经允许不得转载:171主机测评 » FreeRTOS——列表与列表项
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址