在 FreeRTOS 中存在着大量的基础数据结构列表和列表项的操作,列表和列表项是直接从 FreeRTOS 源码注释中的 list 和 list item 翻译过来的,其实就是对应我们 C 语言当中的链表和节点,在后续的讲解,我们说的链表就是列表,节点就是列表项。
l C 语言链表
链表作为 C 语言中一种基础的数据结构,在平时写程序的时候用的并不多,但在操作系统里面使用的非常多。链表由节点组成,节点与节点之间首尾相连。链表的节点本身不能存储太多东西,或者说链表的节点本来就不是用来存储大量数据的,链表分为单向链表和双向链表,单向链表很少用,使用最多的还是双向链表。
u 单向链表
节点本身必须包含一个节点指针,用于指向后一个节点,除了这个节点指针是必须有的之外,节点本身还可以携带一些私有信息。
在代码清单 6-1 除了 struct node *next 这个节点指针之外,剩下的成员都可以理解为节点携带的数据,但是这种方法很少用。通常的做法是节点里面只包含一个用于指向下一个节点的指针。要通过链表存储的数据内嵌一个节点即可,这些要存储的数据通过这个内嵌的节点即可挂接到链表中。
u 双向链表
双向链表与单向链表的区别就是节点中有两个节点指针,分别指向前后两个节点,其它完全一样。
在 C 语言中,链表与数组确实很像。
链表是通过节点把离散的数据链接成一个表,通过对节点的插入和删除操作从而实现对数据的存取。而数组是通过开辟一段连续的内存来存储数据,这是数组和链表最大的区别。 数组的每个成员对应链表的节点,成员和节点的数据类型可以是标准的 C 类型或者是用户自定义的结构体。数组有起始地址和结束地址,而链表是一个圈,没有头和尾之分,但是为了方便节点的插入和删除操作会人为的规定一个根节点。
l FreeRTOS 中链表的实现
FreeRTOS 中与链表相关的操作均在 list.h 和 list.c 这两个文件中实现, list.h 第一次使用需要在 include 文件夹下面新建然后添加到工程 freertos/source 这个组文件, list.c 第一次使用需要在 freertos 文件夹下面新建然后添加到工程 freertos/source 这个组文件。
Ø 定义链表节点数据结构
链表节点的数据结构在 list.h 中定义
代码清单 6-3(1):一个辅助值,用于帮助节点做顺序排列。 该辅助值的数据类型为TickType_t, 在 FreeRTOS 中,凡是涉及到数据类型的地方, FreeRTOS 都会将标准的 C 数据类型用 typedef 重新取一个类型名。这些经过重定义的数据类型放在 portmacro.h( portmacro.h 第一次使用需要在 include 文件夹下面新建然后添加到工程 freertos/source 这个组文件)这个头文件,具体见代码清单 6-4。代码清单 6-4 中除了 TickType_t 外,其它数据类型重定义是本章后面内容需要使用到,这里统一贴出来,后面将不再赘述。
TickType_t 具 体 表 示 16 位 还 是 32 位 , 由configUSE_16_BIT_TICKS 这个宏决定, 当该宏定义为 1 时, TickType_t 为 16 位,否则为32 位。该宏在 FreeRTOSConfig.h( FreeRTOSConfig.h 第一次使用需要在 include 文件夹下面新建然后添加到工程 freertos/source 这个组文件) 中默认定义为 0
代码清单 6-3(2): 用于指向链表下一个节点。
代码清单 6-3(3): 用于指向链表前一个节点。
代码清单 6-3(4): 用于指向该节点的拥有者, 即该节点内嵌在哪个数据结构中, 属于哪个数据结构的一个成员。代码清单 6-3(5):用于指向该节点所在的链表,通常指向链表的根节点。代码清单 6-3(6): 节点数据类型重定义。
Ø 链表节点初始化
链表节点初始化函数在 list.c 中实现。
代码清单 6-6(1):链表节点 ListItem_t 总共有 5 个成员,但是初始化的时候只需将pvContainer 初始化为空即可, 表示该节点还没有插入到任何链表。
Ø 定义链表根节点数据结构
链表根节点的数据结构在 list.h 中定义。
代码清单 6-7(1): 链表节点计数器, 用于表示该链表下有多少个节点, 根节点除外。代码清单 6-7(2): 链表节点索引指针, 用于遍历节点。代码清单 6-7(3): 链表最后一个节点。我们知道,链表是首尾相连的,是一个圈,首就是尾,尾就是首,这里从字面上理解就是链表的最后一个节点,实际也就是链表的第一个节点,我们称之为生产者。该生产者的数据类型是一个精简的节点, 也在 list.h 中定义。
Ø 链表根节点初始化
链表节点初始化函数在 list.c 中实现。
代码清单 6-10 (1): 将链表索引指针指向最后一个节点, 即第一个节点, 或者第零个节点更准确,因为这个节点不会算入节点计数器的值。代码清单 6-10 (2): 将链表最后(也可以理解为第一) 一个节点的辅助排序的值设置为最大,确保该节点就是链表的最后节点(也可以理解为第一)。代码清单 6-10 (3): 将最后一个节点(也可以理解为第一) 的 pxNext 和 pxPrevious 指针均指向节点自身,表示链表为空。代码清单 6-10 (4): 初始化链表节点计数器的值为 0,表示链表为空。
Ø 将节点插入到链表的尾部
将节点插入到链表的尾部(可以理解为头部) 就是将一个新的节点插入到一个空的链表。
Ø 将节点按照升序排列插入到链表
将节点按照升序排列插入到链表,如果有两个节点的值相同,则新节点在旧节点的后面插入。
代码清单 6-12(1):获取节点的排序辅助值。代码清单 6-12(2):根据节点的排序辅助值,找到节点要插入的位置,按照升序排列。代码清单 6-12(3):按照升序排列,将节点插入到链表。假设将一个节点排序辅助值是 2 的节点插入到有两个节点的链表中,这两个现有的节点的排序辅助值分别是 1 和 3,那么插入过程的示意图具体见图6-11。
Ø 将节点从链表删除
将节点从链表删除的具体实现见代码清单 6-13。假设将一个有三个节点的链表中的中间节点节点删除。
Ø 节点带参宏小函数
在 list.h 中,还定义了各种各样的带参宏,方便对节点做一些简单的操作。
Ø 链表节点插入实验
我们新建一个根节点(也可以理解为链表)和三个普通节点,然后将这三个普通节点按照节点的排序辅助值做升序排列插入到链表中。
代码清单 6-15(1):定义链表根节点,有根了,节点才能在此基础上生长。代码清单 6-15(2):定义 3 个普通节点。代码清单 6-15(3):链表根节点初始化,初始化完毕之后,根节点示意图见图 6-13。
代码清单 6-15(4):节点初始化,初始化完毕之后节点示意图见图 6-14,其中xItemValue 等于你的初始化值。
代码清单 6-15(5):将节点按照他们的排序辅助值做升序排列插入到链表,插入完成后链表的示意图6-15。
实验论证
Hankin
2020.08.19