要想看懂FreeRTOS源码并学习其原理,有一个东西绝对跑不了,那就是FreeRTOS的列表和列表项。列表和列表项是FreeRTOS的一个数据结构,FreeRTOS 大量使用到了列表和列表项,它是 FreeRTOS 的基石。要想深入学习并理解 FreeRTOS,那么列表和列表项就必须首先掌握,否则后面根本就没法进行。本章我们就来学习一下 FreeRTOS 的列表和列表项,包括对列表和列表项的操作。
注意区分列表和队列。列表一般被叫做任务列表,队列一般被叫做消息队列。
什么是列表
列表是 FreeRTOS 中的一个数据结构,概念上和链表有点类似,其实就是用链表来实现的,列表被用来跟踪 FreeRTOS中的任务。与列表相关的全部东西都在文件 list.c 和 list.h 中。在 list.h 中定义了一个叫 List_t 的结构体,如下:
其中,(1) 和 (5) 、 这两个都是用来检查列表完整性的 ,需要将宏configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES 设置为 1,开启以后会向这两个地方分别添加一个变量 xListIntegrityValue1 和 xListIntegrityValue2,在初始化列表的时候会在这两个变量中写入一个特殊的值,默认不开启这个功能。以后我们在学习列表的时候不讨论这个功能!
(2)、uxNumberOfItems 用来记录列表中列表项的数量。
(3)、pxIndex 用来记录当前列表项索引号,用于遍历列表。
(4)、列表中最后一个列表项,用来表示列表结束,此变量类型为 MiniListItem_t,这是一个 迷你列表项,关于列表项稍后讲解。
列表结构示意图如下图所示:
注意!上图中并未列出用于列表完整性检查的成员变量。
比如说这些地方都会创建一个列表
这里面包括就绪列表、阻塞列表、挂起列表等等,用户如果有需要也可以使用。
什么是列表项
列表项就是存放在列表中的项目,其实就是链表的结点,FreeRTOS 提供了两种列表项:列表项和迷你列表项。这两个都在文件 list.h 中有定义,先来看一下列表项,定义如下:
其中,(1)和(7)用法和列表一样,用来检查列表项完整性的。以后我们在学习列表项的时候不讨论这个功能!
(2)、xItemValue 为列表项值。
(3)、pxNext 指向下一个列表项。
(4)、pxPrevious 指向前一个列表项,和 pxNext 配合起来实现类似双向链表的功能。
(5)、pvOwner 记录此链表项指向哪个任务控制块。
(6)、pvContainer 用来记录此列表项归哪个列表。注意和 pvOwner 的区别,在前面讲解任务控制块 TCB_t 的时候说了在 TCB_t 中有两个变量 xStateListItem 和 xEventListItem,这两个变量的类型就是 ListItem_t,也就是说这两个成员变量都是列表项。以 xStateListItem 为例,当创建一个任务以后,xStateListItem 的 pvOwner 变量就指向这个任务的任务控制块,表示 xSateListItem属于此任务。当任务就绪态以后 xStateListItem 的变量 pvContainer 就指向就绪列表,表明此列表项在就绪列表中。由此,对于某个任务,我们通过pvContainer 就能知道它被挂在哪类列表中,并且我们通过列表项的pvOwner 可以找到对应任务的任务控制块,也就能因此操作对应的任务了。
也就是说:列表下有列表项,列表项指向哪个任务。
列表项结构示意图如下图所示:
注意!上图中并未列出用于列表项完整性检查的成员变量!
迷你列表项
上面我们我们说了列表项,现在来看一下迷你列表项,迷你列表项在文件 list.h 中有定义, 如下:
(1)、用于检查迷你列表项的完整性。
(2)、xItemValue 记录列表列表项值。
(3)、pxNext 指向下一个列表项。
(4)、pxPrevious 指向上一个列表项。
可以看出迷你列表项只是比列表项少了几个成员变量,迷你列表项有的成员变量列表项都 有的,没感觉有什么本质区别啊?那为什么要弄个迷你列表项出来呢?那是因为有些情况下我们不需要列表项这么全的功能,可能只需要其中的某几个成员变量,如果此时用列表项的话会造成内存浪费!比如上面列表结构体 List_t 中表示最后一个列表项的成员变量xListEnd 就是MiniListItem_t 类型的。
列表初始化
新创建或者定义的列表需要对其做初始化处理,列表的初始化其实就是初始化列表结构体List_t 中的各个成员变量,列表的初始化通过函数 vListInitialise()来完成,此函数在 list.c 中有定义,函数如下:
具体内容参考视频,此处不赘述。
list.c里基本就是链表的操作。去复习回顾以及补充学习之前看过的数据结构的知识就能知道了。这一块的基本必须掌握。
列表项初始化
同列表一样,列表项在使用的时候也需要初始化,列表项初始化由函数 vListInitialiseItem()来完成,函数如下:
列表项的初始化很简单,只是将列表项成员变量 pvContainer 初始化为 NULL,并且给用于完整性检查的变量赋值。有朋友可能会问,列表项的成员变量比列表要多,怎么初始化函数就这么短?其他的成员变量什么时候初始化呢?这是因为列表项要根据实际使用情况来初始化,比如任务创建函数 xTaskCreate()就会对任务堆栈中的 xStateListItem 和xEventListItem 这两个列表项中的其他成员变量在做初始化。
列表项插入
列表项的插入操作通过函数 vListInsert()来完成,函数原型如下:
pxList: 列表项要插入的列表。
pxNewListItem: 要插入的列表项。
作用就是将某个列表项插入某个列表中,其实就是把某个任务插入到某个列表中,因为列表项中有对应任务控制块的信息。
函数 vListInsert()的参数 pxList 决定了列表项要插入到哪个列表中,pxNewListItem 决定了要插入的列表项,但是这个列表项具体插入到什么地方呢?要插入的位置由列表项中成员变量xItemValue 来决定。列表项的插入根据 xItemValue 的值按照升序的方式排列!
具体过程可参考视频,此处不赘述。
列表项末尾插入
列表末尾插入列表项的操作通过函数 vListInsertEnd ()来完成,函数原型如下:
pxList: 要插入的列表。
pxNewListItem: 要插入的列表项。
使用函数vListInsert()向列表中插入一个列表项的时候这个列表项的位置是通过列表项的值,也就是列表项成员变量 xItemValue 来确定。vListInsertEnd()是往列表的末尾添加列表项的,我们知道列表中的 xListEnd 成员变量表示列表末尾的,那么函数 vListInsertEnd()插入一个列表项是不是就是插到 xListEnd 的前面或后面啊?这个是不一定的,这里所谓的末尾要根据列表的成员变量pxIndex 来确定的!前面说了列表中的 pxIndex 成员变量是用来遍历列表的,pxIndex 所指向的列表项就是要遍历的开始列表项,也就是说 pxIndex 所指向的列表项就代表列表头!由于是个环形列表,所以新的列表项就应该插入到 pxIndex 所指向的列表项的前面。 开头的前面就是末尾。
列表项的删除
有列表项的插入,那么必然有列表项的删除,列表项的删除通过函数 uxListRemove()来完成,函数原型如下:
pxItemToRemove: 要删除的列表项。
返回值: 返回删除列表项以后的列表剩余列表项数目。
要删除一个列表项我们得先知道这个列表项处于哪个列表中,直接读取列表项中的成员变量 pvContainer 就可以得到此列表项处于哪个列表中。
注意,列表项的删除只是将指定的列表项从列表中删除掉,并不会将这个列表项的内存给释放掉!
列表的遍历
介绍列表结构体的时候说过列表 List_t 中的成员变量 pxIndex 是用来遍历列表的,FreeRTOS提供了一个函数来完成列表的遍历,这个函数是listGET_OWNER_OF_NEXT_ENTRY()。每调用一次这个函数列表的 pxIndex 变量就会指向下一个列表项,并且返回这个列表项的 pxOwner变量值。这个函数本质上是一个宏,这个宏在文件 list.h 中如下定义:
#define listGET_OWNER_OF_NEXT_ENTRY( pxTCB, pxList )
{
……
}此函数用于从多个同优先级的就绪任务中查找下一个要运行的任务。
总结
Freertos中列表和列表项的主要用途就是在三大链表(挂起、阻塞、就绪)中将任务(列表项)进行排序管理。
列表被FreeRTOS调度器使用,用于跟踪任务,处于就绪、挂起、阻塞的任务,都会被挂接到各自的列表中。
用户程序如果有需要,也可以使用列表。