本文旨在讲清楚列表、列表项、任务控制块、任务之间的关系,涉及底层源码但不讲列表API,你可能很懂列表,但我还是建议你看一下这篇文章。凭什么说列表项表示任务,列表表示任务状态?
FreeRTOS的学习(四)——列表_freertos的实习-CSDN博客
列表、迷你列表项、列表项的结构体及成员变量
(这玩意清楚了才知道任务存在哪,怎么进来的)
列表
uxNumberOfItems:用来记录列表中列表项的数量。
pxIndex:用来记录当前列表项索引号,用于遍历列表。
xListEnd:列表中最后一个列表项,用来表示列表结束。
讲点需要注意的地方和其他文章讲不清楚的地方
1、uxNumberOfItems不包含ListEnd,即初始化时列表中会有一个xListEnd列表项,但uxNumberOfItems = 0
2、configLIST_VOLATILE 是一个宏,用来修饰变量或指针,使其具有 volatile 关键字的特性。volatile(不稳定的)是C语言中的一个关键字,用来告诉编译器该变量可能在程序的其他部分(例如中断服务程序、其他线程或硬件)被意外地修改。因此,编译器在优化代码时不对该变量进行优化,必须每次都从内存中读取它的最新值。
所谓的优化可以理解为其在生成汇编时,若多次读取该变量时其可能会将变量值放入寄存器中以加快读取速度,而不是真正的读取,这使得当某个变量会快速变化时,优化后“读取”的值并不是变量真实的值。
#ifndef configLIST_VOLATILE
#define configLIST_VOLATILE
#endif /* configSUPPORT_CROSS_MODULE_OPTIMISATION */
configLIST_VOLATILE 可以不用定义,因为结构体成员的修改实质上都是在中断中进行的,都要进入临界区才能操作。所以不需要定义为volatile了。
迷你列表项
xListEnd就是一个迷你列表项,迷你列表项结构成员比较简单,值是多少?上一个是谁?下一个是谁?应该比较容易理解。
我们都知道vListInsert列表项插入时是按照这个值的大小升序插入的,那这个值到底存的什么东西?表示什么含义呢?这个问题先放在这里,后面自会水落石出。
至于链表怎么“握手”,包括列表添加删除列表项时这两个指针的指向分析,这个“握手”的例子还是挺生动的。
列表项
它比迷你列表项多了两个成员
pvOwner:指向包含列表项的对象(通常是TCB)的指针。
pxContainer:用来记录此列表项属于哪个列表。
慢慢来分析一下:
1、列表项属于哪个列表,pxContainer这个概念比较好理解
从列表项初始化(vListInitialiseItem)和删除(uxListRemove)函数来看,只有pxContainer指向null,其他成员变量我们并不关心,也并没有释放内存。这就说明列表项初始化和删除就是保证它是一个独立的列表项,不属于任何列表,并且列表项会在不同列表之间换来换去(学名叫挂载)。
我们知道列表就是任务状态,就绪态、阻塞态、挂起态,不同的状态对应不同的就绪列表、阻塞列表、挂起列表,而任务调度(比如从就绪态变为阻塞态就需要将该任务对应的列表项从就绪列表移出,并插入阻塞列表)来跟踪调度任务,所以这个成员变量是很重要的。
2、pvOwner官方说是任务控制块,那任务控制块是怎么传到这里的?凭什么列表项可以表示任务?
先来讲一下任务控制块
系统为了顺利的调度任务,为每个任务都额外定义了一个任务控制块,这个任务控制块就相当于任务的身份证,里面存有任务的所有信息,比如任务的栈指针,任务名称,任务的形参等。有了这个任务控制块之后,以后系统对任务的全部操作都可以通过这个任务控制块来实现。
说回上面
列表添加删除的API并不关心这个变量,他们只管怎么“握手”。这个变量应该是创建任务控制块时就绑定好的。
在使用静态方法创建任务的时候,需要给任务初始化函数 xTaskCreateStatic()传递预先定义好的任务控制块的指针。动态方法把这一参数隐藏起来了,但在源码中仍然可以看到。
在任务创建方法中有一个prvInitialiseNewTask函数,其中便有一些给列表项赋值的操作,尤其是在最后一个函数listSET_LIST_ITEM_OWNER中,便可以看到把TCB赋值给pvOwner的操作
/* Set the pxNewTCB as a link back from the ListItem_t. This is so we can get
back to the containing TCB from a generic item in a list. */
listSET_LIST_ITEM_OWNER( &( pxNewTCB->xStateListItem ), pxNewTCB );
/* Event lists are always in priority order. */
listSET_LIST_ITEM_VALUE( &( pxNewTCB->xEventListItem ), ( TickType_t ) configMAX_PRIORITIES - ( TickType_t ) uxPriority ); /*lint !e961 MISRA exception as the casts are only redundant for some ports. */
listSET_LIST_ITEM_OWNER( &( pxNewTCB->xEventListItem ), pxNewTCB );
listSET_LIST_ITEM_OWNER定义表示
#define listSET_LIST_ITEM_OWNER( pxListItem, pxOwner ) ( ( pxListItem )->pvOwner = ( void * ) ( pxOwner ) )
listSET_LIST_ITEM_VALUE定义表示
#define listSET_LIST_ITEM_VALUE( pxListItem, xValue ) ( ( pxListItem )->xItemValue = ( xValue ) )
TCB_t 中有两个变量 xStateListItem 和 xEventListItem,这两个变量的类型就是 ListItem_t,也就是说这两个成员变量都是列表项。以 xStateListItem 为例,当创建一个任务 以后 xStateListItem 的 pvOwner 变量就指向这个任务的任务控制块,表示 xSateListItem 属于此任务。当任务就绪态以后 xStateListItem 的变量 pvContainer 就指向就绪列表,表明此列表项在就绪列表中。
3、同时我们可以看到listSET_LIST_ITEM_VALUE函数,便是把任务的优先级传给列表项的xItemValue这个参数了,由此便可以解释为什么列表项插入列表排序要根据这个值来排了。就绪列表,阻塞列表,挂起列表,他们都需要优先级来顺序调度任务。
再补充一下xItemValue的解释:
xItemValue是列表项的值,通常是一个被追踪任务的优先级或一个调度事件的计数值。如果任务因为等待从队列中取数据进入阻塞态,则任务事件列表项的xItemValue值会保存任务优先级相关的信息,状态事件列表项的xItemValue值会保存阻塞事件相关的信息。
xListEnd.xItemValue会被初始化为一个常数,其值与硬件架构相关,为0xFFFF(16位架构)或者0xFFFFFFFF(32位架构)。
现在是不是这几个结构体和结构体变量每个字母都清楚了?收藏吧?点赞关注都上来吧!!
至于列表项插入列表API有几点需要注意:
1、vListInsertEnd并不是一直插入在xListEnd的前面,他是插入在pxIndex指向的列表项的前面。
列表初始化时:
pxList->pxIndex = ( ListItem_t * ) &( pxList->xListEnd );
pxIndex指针指向xListEnd的首地址,运行过程中并不一直指向xListEnd。
2、关于列表中的情况,下图给你答案,值得注意的是列表(链表)的物理地址并不是连续的,即每一个虚线框的物理地址并不是连续的,中间还有一大堆。
最后推荐一个画图网页
Excalidraw | Hand-drawn look & feel • Collaborative • Secure
https://zhuanlan.zhihu.com/p/539137319
tmd虽然看起来不太正规,但画出来还真挺好看的,我真吃这一套。