FreeRTOS(8)——列表和列表项

在 FreeRTOS 里,任务的创建、阻塞、就绪、延时、挂起等状态变化,都离不开一个非常基础的数据结构:列表

FreeRTOS 的列表可以先理解成链表,列表项就是链表里的节点。更准确一点说,它是一个带有末尾哨兵节点的双向环形链表。本文的重点不是单纯背 API,而是理解 FreeRTOS 为什么要这样设计,以及这些列表操作最终怎样服务于任务调度。

为什么 FreeRTOS 要用列表

数组的特点是成员地址连续,访问方便,但长度通常在一开始就确定下来,后期不适合频繁增删。

RTOS 里的任务则不同:

  1. 任务数量不一定固定。
  2. 任务会不断在就绪、阻塞、挂起等状态之间切换。
  3. 同一个任务控制块可能在不同时间挂到不同的系统列表里。
  4. 调度器需要高效地插入、删除和遍历任务。

所以 FreeRTOS 选择用列表来管理任务。列表项之间不要求地址连续,它们通过 pxNextpxPrevious 人为连接起来,数量也可以随着任务状态变化动态增减。

在 FreeRTOS 源码中,列表相关内容主要在 list.clist.h 中。

三个核心结构体

理解 FreeRTOS 列表,先抓住三个结构体:

  1. List_t:列表本身。
  2. ListItem_t:普通列表项。
  3. MiniListItem_t:迷你列表项,主要用于列表末尾的哨兵节点。

List_t:列表结构

List_t 的典型定义如下:

typedef struct xLIST
{
   listFIRST_LIST_INTEGRITY_CHECK_VALUE
   volatile UBaseType_t uxNumberOfItems;
   ListItem_t * configLIST_VOLATILE pxIndex;
   MiniListItem_t xListEnd;
   listSECOND_LIST_INTEGRITY_CHECK_VALUE
} List_t;

几个成员的含义:

uxNumberOfItems 用来记录列表中普通列表项的数量,不包含 xListEnd

pxIndex 是一个遍历指针,FreeRTOS 在遍历列表项时会用它记住当前位置。

xListEnd 是列表的末尾列表项,也可以理解成哨兵节点。它不是普通业务节点,而是用来标记列表边界、简化插入和遍历逻辑。

listFIRST_LIST_INTEGRITY_CHECK_VALUElistSECOND_LIST_INTEGRITY_CHECK_VALUE 是完整性检查相关宏。开启对应配置后,FreeRTOS 可以通过这些固定值判断列表结构是否在运行过程中被破坏。

ListItem_t:普通列表项

普通列表项定义如下:

struct xLIST_ITEM
{
   listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE
   configLIST_VOLATILE TickType_t xItemValue;
   struct xLIST_ITEM * configLIST_VOLATILE pxNext;
   struct xLIST_ITEM * configLIST_VOLATILE pxPrevious;
   void * pvOwner;
   struct xLIST * configLIST_VOLATILE pxContainer;
   listSECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE
};

typedef struct xLIST_ITEM ListItem_t;

这里最重要的是五个字段:

xItemValue 是列表项的值,常用于排序。例如延时列表会根据唤醒 tick 值排序。

pxNextpxPrevious 分别指向下一个和上一个列表项,因此 FreeRTOS 的列表是双向链表。

pvOwner 指向拥有这个列表项的对象,通常是任务控制块 TCB。也就是说,调度器可以通过列表项反查到对应任务。

pxContainer 指向当前列表项所在的列表。列表项被移除时,这个字段会被清空为 NULL

MiniListItem_t:迷你列表项

迷你列表项定义如下:

struct xMINI_LIST_ITEM
{
   listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE
   configLIST_VOLATILE TickType_t xItemValue;
   struct xLIST_ITEM * configLIST_VOLATILE pxNext;
   struct xLIST_ITEM * configLIST_VOLATILE pxPrevious;
};

typedef struct xMINI_LIST_ITEM MiniListItem_t;

它比普通列表项少了 pvOwnerpxContainer,因为 MiniListItem_t 只用于标记列表末尾,不需要关联具体任务,也不需要记录自己属于哪个列表。

List_t 里的 xListEnd 就是一个 MiniListItem_t

列表初始化:vListInitialise()

vListInitialise() 用于初始化一个列表:

void vListInitialise( List_t * const pxList )
{
   pxList->pxIndex = ( ListItem_t * ) &( pxList->xListEnd );

   pxList->xListEnd.xItemValue = portMAX_DELAY;

   pxList->xListEnd.pxNext = ( ListItem_t * ) &( pxList->xListEnd );
   pxList->xListEnd.pxPrevious = ( ListItem_t * ) &( pxList->xListEnd );

   pxList->uxNumberOfItems = ( UBaseType_t ) 0U;

   listSET_LIST_INTEGRITY_CHECK_1_VALUE( pxList );
   listSET_LIST_INTEGRITY_CHECK_2_VALUE( pxList );
}

初始化后的列表有几个关键状态:

  1. 列表中暂时只有 xListEnd
  2. pxIndex 指向 xListEnd
  3. xListEnd.xItemValue 被设置为 portMAX_DELAY,也就是最大值。
  4. xListEnd.pxNextxListEnd.pxPrevious 都指向自己,形成一个只有哨兵节点的环形链表。
  5. uxNumberOfItems 为 0,因为它不统计 xListEnd

这个设计非常巧妙:即使列表为空,pxNextpxPrevious 也始终有效,后续插入和删除时就不需要处理大量空指针特例。

列表项初始化:vListInitialiseItem()

vListInitialiseItem() 用于初始化一个普通列表项:

void vListInitialiseItem( ListItem_t * const pxItem )
{
   pxItem->pxContainer = NULL;

   listSET_FIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE( pxItem );
   listSET_SECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE( pxItem );
}

它最重要的动作是把 pxContainer 设置为 NULL。这表示该列表项当前不属于任何列表。

一个列表项是否已经被挂入某个列表,可以通过 pxContainer 判断。插入时它会指向目标列表,移除时又会被清空。

有序插入:vListInsert()

vListInsert() 会根据 xItemValue 的值,将列表项按升序插入到列表中。

函数原型如下:

void vListInsert( List_t * const pxList, ListItem_t * const pxNewListItem );

核心逻辑可以分成四步:

  1. 读取新列表项的 xItemValue
  2. 从列表中寻找合适的插入位置。
  3. 修改前后节点的 pxNextpxPrevious,把新节点接入链表。
  4. 更新新节点的 pxContainer,并让 uxNumberOfItems 加 1。

源码中的关键片段如下:

ListItem_t * pxIterator;
const TickType_t xValueOfInsertion = pxNewListItem->xItemValue;

if( xValueOfInsertion == portMAX_DELAY )
{
   pxIterator = pxList->xListEnd.pxPrevious;
}
else
{
   for( pxIterator = ( ListItem_t * ) &( pxList->xListEnd );
        pxIterator->pxNext->xItemValue <= xValueOfInsertion;
        pxIterator = pxIterator->pxNext )
  {
  }
}

pxNewListItem->pxNext = pxIterator->pxNext;
pxNewListItem->pxNext->pxPrevious = pxNewListItem;
pxNewListItem->pxPrevious = pxIterator;
pxIterator->pxNext = pxNewListItem;

pxNewListItem->pxContainer = pxList;
( pxList->uxNumberOfItems )++;

假设依次插入值为 406050 的三个列表项,最终顺序会是:

xListEnd <-> 40 <-> 50 <-> 60 <-> xListEnd

注意这里的 xListEnd.xItemValue 是最大值,所以它天然排在最后。代码里还专门处理了 xValueOfInsertion == portMAX_DELAY 的情况,否则遍历时会和同样为最大值的 xListEnd 发生边界问题。

vListInsert() 常用于需要按时间或优先级相关值排序的场景。例如延时列表中,任务会按照唤醒 tick 值挂入列表,调度器就可以更快找到下一个应该被唤醒的任务。

末尾插入:vListInsertEnd()

vListInsertEnd() 的函数原型如下:

void vListInsertEnd( List_t * const pxList, ListItem_t * const pxNewListItem );

它的名字叫“末尾插入”,但更准确地说,它是把新列表项插入到 pxIndex 当前指向的列表项前面:

ListItem_t * const pxIndex = pxList->pxIndex;

pxNewListItem->pxNext = pxIndex;
pxNewListItem->pxPrevious = pxIndex->pxPrevious;

pxIndex->pxPrevious->pxNext = pxNewListItem;
pxIndex->pxPrevious = pxNewListItem;

pxNewListItem->pxContainer = pxList;
( pxList->uxNumberOfItems )++;

它不会按 xItemValue 排序,所以是一种无序插入。

如果 pxIndex 正好指向 xListEnd,它看起来就像把节点插到了普通列表项的末尾。例如原来有一个值为 40 的列表项,再插入一个值为 30 的列表项,顺序可能变成:

xListEnd <-> 40 <-> 30 <-> xListEnd

可以看到,30 并没有排到 40 前面。这就是它和 vListInsert() 最大的区别。

在 FreeRTOS 中,这种插入方式常用于不需要按 xItemValue 排序、但需要保持某种轮转顺序的列表。

移除列表项:uxListRemove()

uxListRemove() 用于把一个列表项从它所在的列表中移除:

UBaseType_t uxListRemove( ListItem_t * const pxItemToRemove );

返回值是移除后,该列表剩余的普通列表项数量。

核心代码如下:

UBaseType_t uxListRemove( ListItem_t * const pxItemToRemove )
{
   List_t * const pxList = pxItemToRemove->pxContainer;

   pxItemToRemove->pxNext->pxPrevious = pxItemToRemove->pxPrevious;
   pxItemToRemove->pxPrevious->pxNext = pxItemToRemove->pxNext;

   if( pxList->pxIndex == pxItemToRemove )
  {
       pxList->pxIndex = pxItemToRemove->pxPrevious;
  }

   pxItemToRemove->pxContainer = NULL;

  ( pxList->uxNumberOfItems )--;

   return pxList->uxNumberOfItems;
}

它的操作顺序也很清晰:

  1. 通过 pxContainer 找到列表项所在的列表。
  2. 修改前后列表项的指针,把当前节点从链表中摘出去。
  3. 如果 pxIndex 正好指向待移除项,就把 pxIndex 回退到上一个列表项。
  4. 将待移除项的 pxContainer 清空为 NULL
  5. 列表项数量减 1。
  6. 返回列表剩余列表项数量。

假设列表中有 405060 三个列表项:

xListEnd <-> 40 <-> 50 <-> 60 <-> xListEnd

移除 60 后,链表变为:

xListEnd <-> 40 <-> 50 <-> xListEnd

被移除的列表项不再属于任何列表,因此它的 pxContainer 会变成 NULL

五个 API 的对比

API作用关键点
vListInitialise()初始化列表建立只包含 xListEnd 的双向环形链表
vListInitialiseItem()初始化列表项pxContainer 设置为 NULL
vListInsert()有序插入列表项xItemValue 升序插入
vListInsertEnd()插入到 pxIndex 前面不排序,常表现为末尾插入
uxListRemove()移除列表项摘链、清空 pxContainer、返回剩余数量

这五个函数构成了 FreeRTOS 列表操作的基础。后面看任务调度、延时列表、事件列表时,本质上都是这些基础操作的组合。

实验设计思路

课程中的实验目标是:学会使用 FreeRTOS 列表和列表项相关 API,并观察实际运行结果是否和理论分析一致。

实验设计了三个任务:

start_task 用来创建其他任务。

task1 每 500ms 翻转一次 LED0,用来提示系统正在运行。

task2 调用列表和列表项相关 API,通过串口输出列表状态,观察初始化、插入和删除后的变化。

实验时可以重点观察这些信息:

  1. 初始化后 uxNumberOfItems 是否为 0。
  2. 列表项插入后 pxContainer 是否指向目标列表。
  3. 使用 vListInsert() 插入多个不同 xItemValue 的列表项后,顺序是否升序。
  4. 使用 vListInsertEnd() 时,列表项是否没有按 xItemValue 排序。
  5. 调用 uxListRemove() 后,列表数量是否减少,被移除项的 pxContainer 是否变为 NULL

这些观察点能帮助我们把“源码指针操作”和“实际链表变化”对应起来。

小结

FreeRTOS 的列表不是一个孤立的数据结构练习,而是整个任务管理机制的地基。

List_t 负责维护列表状态,ListItem_t 作为节点挂入列表,MiniListItem_t 作为 xListEnd 哨兵节点简化边界处理。列表初始化后会形成一个只有 xListEnd 的双向环形链表,后续插入和删除都围绕这个结构展开。

vListInsert() 负责按 xItemValue 有序插入,适合需要排序的场景;vListInsertEnd() 负责插入到 pxIndex 前面,不关心 xItemValueuxListRemove() 则负责把列表项从当前列表中摘除,并维护列表数量和归属关系。

理解这些之后,再看 FreeRTOS 的就绪列表、延时列表、事件等待列表,就会发现调度器并没有什么神秘魔法。它只是在正确的时机,把代表任务的列表项从一个列表移到另一个列表中。

文末附加内容
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇