FreeRTOS原理剖析:列表和列表项

1. 列表和列表项概念

1.1 对C语言中链表的简介

在FreeRTOS中,列表和列表项是非常重要的数据结构,它跟C语言中的链表很相似。在C语言中,链表包括单链表、单循环链表、双向循环链表,如下:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在FreeRTOS中,采用的是双项循环链表,对于双项循环链表,有:

  • 链表中包含一个表头(也称头结点)和多个结点,若链表中仅有一个表头,结点数为0,称为空表。
  • 每个结点包含两个指针域和储存信息域。其中,两个指针域中一个指向前结点,一个指向后结点。第一个结点和最后一个结点有点特殊,如图3所示,结点1的前结点为表头,后结点为结点2;结点4的前结点为结点3,后结点为表头。储存信息域包含储存的可用数据,一般可在该处设置指针,将其指向储存数据处。
  • 表头跟结点不同,它一般只需要包含两个指针域,不需要储存信息域。在实际应用中,在表头中也会加入链表的状态信息,如结点的总数、遍历整个链表的指针等信息。
  • 表头位置固定不变,当对链表进行插入或删除等操作,其本质是对链表中结点进行插入或删除。
  • 表头和结点的数据成员不同,在实际应用中,一般会定义两个不同的数据类型(结构体),分别用于表头和结点。
  • 在循环链表中,有些特殊操作不设置链表表头,而直接设置表尾,如两个链表合并时,仅需一个表的表头与另一个表的表尾相连。双项循环列表是一个圈,对于直接设置表尾这种情况,其理解方式可以看成表头一样。

1.2 FreeRTOS中列表和列表项的理解

理解了C语言中的双向循环链表,就很好理解FreeRTOS中的列表和列表项。
在FreeRTOS中,列表和列表项的数据结构声明和函数定义的源代码都在list.c和list.h中。在list.h中存在三个重要的结构体声明,分别是:

struct xLIST				//列表
struct xLIST_ITEM			//列表项
struct xMINI_LIST_ITEM		//Mini列表项

其中,列表相当于链表,Mini列表项相当于表头,列表项相当于双向链表中的结点。在定义列表时,Mini列表项定义为xListEnd,从字面上看,它表示列表中最后一个列表项。由于链表中是一个圈,首就是尾,尾就是首,可以将其理解为表头,但是定义为xListEnd。另外,在定义一个列表时,其本质是定义一个表头,如果对列表的操作,就是对列表中的列表项操作。

在FreeRTOS中,列表的结构体声明如下:

typedef struct xLIST
{
	listFIRST_LIST_INTEGRITY_CHECK_VALUE				/*< Set to a known value if configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES is set to 1. */
	configLIST_VOLATILE UBaseType_t uxNumberOfItems;
	ListItem_t * configLIST_VOLATILE pxIndex;			/*< Used to walk through the list.  Points to the last item returned by a call to listGET_OWNER_OF_NEXT_ENTRY (). */
	MiniListItem_t xListEnd;							/*< List item that contains the maximum possible item value meaning it is always at the end of the list and is therefore used as a marker. */
	listSECOND_LIST_INTEGRITY_CHECK_VALUE				/*< Set to a known value if configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES is set to 1. */
} List_t;

其中:

struct xMINI_LIST_ITEM
{
	listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE			/*< Set to a known value if configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES is set to 1. */
	configLIST_VOLATILE TickType_t xItemValue;
	struct xLIST_ITEM * configLIST_VOLATILE pxNext;
	struct xLIST_ITEM * configLIST_VOLATILE pxPrevious;
};
typedef struct xMINI_LIST_ITEM MiniListItem_t;

即列表的结构为:
在这里插入图片描述

通过分析列表的结构体声明可知:

  • uxNumberOfItems、pxIndex和Mimi列表项组成一个完整列表的“表头”,其中包括列表的状态信息,如uxNumberOfItems表示列表中列表项的数目、pxIndex表示索引,Mimi列表项主要包含指针域,分别指向前一个列表项和后一个列表项。
  • MiniListItem_t(Mimi列表项)被包含在List_t(列表)里,它被命名为xListEnd,从字面上可知,它表示列表的最后一个列表项。由于在链表中是首尾相连,首就是尾,尾就是首,可将其理解为表头,但是定义为xListEnd。
  • 列表(List_t)中表示列表的表头,如果列表项为0,称为空列表。对某个列表的插入和删除,其本质对列表项的插入和删除。

2. 列表和列表项源代码分析

2.1 列表项

struct xLIST_ITEM
{
	listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE			(1)
	configLIST_VOLATILE TickType_t xItemValue;			(2)
	struct xLIST_ITEM * configLIST_VOLATILE pxNext;		(3)
	struct xLIST_ITEM * configLIST_VOLATILE pxPrevious;	(4)
	void * pvOwner;										(5)
	void * configLIST_VOLATILE pvContainer;             (6)				
	listSECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE	        (7)		
};

如上图,该结构体表示列表项,在源代码list.h中定义,分析如下:

  • (1)和(5)处:该处检查代码的完整性,分析过程较长,单独列一节,具体分析参考2.4节。

  • (2)处:表示列表项的数值(后面称项值),为TickType_t 类型,即为uint32_t类型。列表中列表项以项值升序排列,该项值跟任务优先级有关,在任务创建时,一般设为:
    (TickType_t ) configMAX_PRIORITIES - ( TickType_t ) uxPriority
    其中:
    configMAX_PRIORITIES 为可使用的最大优先级,在FreeRTOSConfig.h中定义。uxPriority在创建任务时设置的优先级。可以看出,优先级越高,排在越前,且项值越小,但创建任务时设置的优先级uxPriority越高。

  • (3)处:指向下一个列表项的首地址。

  • (4)处:指向前一个列表项的首地址。

  • (5)处:Owner译为“拥有者”,即列表项的拥有者,表示该列表项属于哪个数据结构的成员,通常是TCB。

  • (6)处:Container译为“容器,集装箱”,该指针理解为指向它的“容器”,即列表项所属于的列表,pvContainer将会指向它属于的列表。

2.2 Mini列表项

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;
};

分析如下:

  • 在FreeRTOS中称为xMINI_LIST_ITEM,即为xLIST_ITEM的“缩小版”,它跟xLIST_ITEM相比减少了pvOwner指针和pvContainer指针。
  • xMINI_LIST_ITEM没有数据保存项,同时没有指向列表的指针,因为在定义时,它就被自动包含在列表中。
  • xMINI_LIST_ITEM被包含在列表中,表示最后一个列表项的“表头”,在创建一个列表时,会将xItemValue值设置为portMAX_DELAY。

2.3 列表

typedef struct xLIST
{
    listFIRST_LIST_INTEGRITY_CHECK_VALUE                   (1)
	configLIST_VOLATILE UBaseType_t uxNumberOfItems;	   (2)
	ListItem_t * configLIST_VOLATILE pxIndex;              (3)
	MiniListItem_t xListEnd;						       (4)
	listSECOND_LIST_INTEGRITY_CHECK_VALUE			       (5)	     
} List_t;

如上图所示,列表用一个List_t类型的结构体表示,它在list.h中定义,分析如下:

  • (1)和(5)处:该处检查代码的完整性,分析过程较长,单独列一节,具体分析参考2.4节。
  • (2)处:表示该列表中列表项的数量,不包括自己本身,初始值为0。
  • (3)处:指向改列表中列表项的索引,可以遍历列表项。
  • (4)处:表示列表中的最后一个列表项,在FreeRTOS中称为MiniListItem_t,与列表项相比它少了pvOwner和pvContainer两个指针,主要用于表头中。

2.4 检查代码完整性

用此功能检测代码的完整性,需要将宏configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES设置为1。在源代码中该宏默认为0,在projdefs.h中定义,如下:

#ifndef configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES
	#define configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES 0
#endif

在list.h中有如下定义:

#if( configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES == 0 )
	/* Define the macros to do nothing. */
	#define listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE
	#define listSECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE
	#define listFIRST_LIST_INTEGRITY_CHECK_VALUE
	#define listSECOND_LIST_INTEGRITY_CHECK_VALUE
	#define listSET_FIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE( pxItem )
	#define listSET_SECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE( pxItem )
	#define listSET_LIST_INTEGRITY_CHECK_1_VALUE( pxList )
	#define listSET_LIST_INTEGRITY_CHECK_2_VALUE( pxList )
	#define listTEST_LIST_ITEM_INTEGRITY( pxItem )
	#define listTEST_LIST_INTEGRITY( pxList )
#else
	/* Define macros that add new members into the list structures. */
	#define listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE				TickType_t xListItemIntegrityValue1;
	#define listSECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE				TickType_t xListItemIntegrityValue2;
	#define listFIRST_LIST_INTEGRITY_CHECK_VALUE					TickType_t xListIntegrityValue1;
	#define listSECOND_LIST_INTEGRITY_CHECK_VALUE					TickType_t xListIntegrityValue2;

	/* Define macros that set the new structure members to known values. */
	#define listSET_FIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE( pxItem )		( pxItem )->xListItemIntegrityValue1 = pdINTEGRITY_CHECK_VALUE
	#define listSET_SECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE( pxItem )	( pxItem )->xListItemIntegrityValue2 = pdINTEGRITY_CHECK_VALUE
	#define listSET_LIST_INTEGRITY_CHECK_1_VALUE( pxList )		( pxList )->xListIntegrityValue1 = pdINTEGRITY_CHECK_VALUE
	#define listSET_LIST_INTEGRITY_CHECK_2_VALUE( pxList )		( pxList )->xListIntegrityValue2 = pdINTEGRITY_CHECK_VALUE

	/* Define macros that will assert if one of the structure members does not
	contain its expected value. */
	#define listTEST_LIST_ITEM_INTEGRITY( pxItem )		configASSERT( ( ( pxItem )->xListItemIntegrityValue1 == pdINTEGRITY_CHECK_VALUE ) && ( ( pxItem )->xListItemIntegrityValue2 == pdINTEGRITY_CHECK_VALUE ) )
	#define listTEST_LIST_INTEGRITY( pxList )			configASSERT( ( ( pxList )->xListIntegrityValue1 == pdINTEGRITY_CHECK_VALUE ) && ( ( pxList )->xListIntegrityValue2 == pdINTEGRITY_CHECK_VALUE ) )
#endif /* configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES */

由上可知,如configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES设置为1,将有效。如果设置为0,所有的宏定义为空。

对于代码完整性检查的理解,主要分为三步:

(1) 在列表和列表项结构体的开头和结尾分别加上宏定义。

listFIRST_LIST_INTEGRITY_CHECK_VALUE	     
listSECOND_LIST_INTEGRITY_CHECK_VALUE	        
//或   
listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE		   
listSECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE

(2) 在列表和列表项初始化时,给列表或列表项中的宏定义赋初始值。

listSET_LIST_INTEGRITY_CHECK_1_VALUE(pxList );     
listSET_LIST_INTEGRITY_CHECK_2_VALUE( pxList );     
//或     
listSET_FIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE( pxItem );    
listSET_SECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE( pxItem ); 

(3) 对列表进行操作,如插入列表项时,先对列表和列表项进行检查,执行如下语句。

listTEST_LIST_ITEM_INTEGRITY( pxItem )	
//或
listTEST_LIST_INTEGRITY( pxList )

其中:

#define listTEST_LIST_ITEM_INTEGRITY( pxItem )		configASSERT( ( ( pxItem )->xListItemIntegrityValue1 == pdINTEGRITY_CHECK_VALUE ) && ( ( pxItem )->xListItemIntegrityValue2 == pdINTEGRITY_CHECK_VALUE ) )
#define listTEST_LIST_INTEGRITY( pxList )			configASSERT( ( ( pxList )->xListIntegrityValue1 == pdINTEGRITY_CHECK_VALUE ) && ( ( pxList )->xListIntegrityValue2 == pdINTEGRITY_CHECK_VALUE ) )


/**
  * ANSI C标准中有几个标准预定义宏(也是常用的):
  * __LINE__:在源代码中插入当前源代码行号;
  * __FILE__:在源文件中插入当前源文件名;
  * __DATE__:在源文件中插入当前的编译日期
  * __TIME__:在源文件中插入当前编译时间;
  * __STDC__:当要求程序严格遵循ANSI C标准时该标识被赋值为1;
  * __cplusplus:当编写C++程序时该标识符被定义。
 **/
#define vAssertCalled(char,int) printf("Error:%s,%d\r\n",char,int)
#define configASSERT(x) if((x)==0) vAssertCalled(__FILE__,__LINE__)

如果条件不成立,则最终通过printf()函数打印出错误信息。

3. 列表和列表项API函数

3.1 函数说明

函数描述
void vListInitialise( List_t * const pxList )列表初始化
void vListInitialiseItem( ListItem_t * const pxItem )列表项初始化
void vListInsert( List_t * const pxList, ListItem_t * const pxNewListItem )往列表中(pxList)插入新的列表项(pxNewListItem )
void vListInsertEnd( List_t * const pxList, ListItem_t * const pxNewListItem )插入新的列表项(pxNewListItem )到列表中(pxList)末尾
UBaseType_t uxListRemove( ListItem_t * const pxItemToRemove )删除列表项

3.2 宏定义说明

宏定义描述
listSET_LIST_ITEM_OWNER( pxListItem, pxOwner )设置列表项的拥有者 ,即TCB
listGET_LIST_ITEM_OWNER( pxListItem )获取列表项的拥有者,即TCB
listSET_LIST_ITEM_VALUE( pxListItem, xValue )设置列表项值,该值跟任务优先级有关
listGET_LIST_ITEM_VALUE( pxListItem )获取列表项值,该值跟任务优先级有关
listGET_ITEM_VALUE_OF_HEAD_ENTRY( pxList )获取表头入口的列表项中的值,即第一个列表项的值
listGET_HEAD_ENTRY( pxList )获取表头入口,即第一个列表项的地址
listGET_NEXT( pxListItem )获取pxListItem 列表项的下一个列表项
listGET_END_MARKER( pxList )获取列表的最后项标记,即列表中Mini列表的首地址
listLIST_IS_EMPTY( pxList )列表中列表项的数量是否为空
listCURRENT_LIST_LENGTH( pxList )列表中列表项的数量
listGET_OWNER_OF_NEXT_ENTRY( pxTCB, pxList )获取下一个结点的Owner,即TCB
listGET_OWNER_OF_HEAD_ENTRY( pxList )获取表头入口(第一个列表项)的Owner,即TCB
listIS_CONTAINED_WITHIN( pxList, pxListItem )当前列表项(pxListItem )是否属于列表(pxList)
listLIST_IS_INITIALISED( pxList )列表是否初始化完成

4. 函数源代码分析

4.1 列表初始化

列表初始化通过vListInitialise()函数,函数原型如下:

/********************************************************
参数:pxList :需要初始化的列表    
返回:无
*********************************************************/
void vListInitialise( List_t * const pxList )

函数代码如下:

void vListInitialise( List_t * const pxList )
{
	/* 将列表的索引指向最后一个列表项,因为此时只有一个列表项xListEnd  */
	pxList->pxIndex = ( ListItem_t * ) &( pxList->xListEnd );	

	/* 
	* 定义列表项值为最大值portMAX_DELAY,该值portmacro.h中定义
	* 每个列表都是按照列表项值升序排列
    */
   	pxList->xListEnd.xItemValue = portMAX_DELAY;
    
   	/* 列表中只包含一个列表项 xListEnd ,即该列表项的前一项和后一项指向自身*/
   	pxList->xListEnd.pxNext = ( ListItem_t * ) &( pxList->xListEnd );	
   	pxList->xListEnd.pxPrevious = ( ListItem_t * ) &( pxList->xListEnd );
  
   	/* 设置列表中列表项数据为0 不包括列表项xListEnd */
   	pxList->uxNumberOfItems = ( UBaseType_t ) 0U;
   
  	/* 初始化完整性检查字段 */
   	listSET_LIST_INTEGRITY_CHECK_1_VALUE( pxList );
   	listSET_LIST_INTEGRITY_CHECK_2_VALUE( pxList );
    }

列表初始化完成后,如下:

在这里插入图片描述

4.2 列表项初始化

列表项初始化通过vListInitialiseItem()函数,函数原型如下:

/********************************************************
参数:pxItem:需要初始化的列表项   
返回:无
*********************************************************/
void vListInitialiseItem( ListItem_t * const pxItem )

pxItem表示需要初始化的列表项,函数代码如下:

void vListInitialiseItem( ListItem_t * const pxItem )
{
	/* 列表项成员pvContainer为NULL,表示该列表项未包含在任何列表中 */
	pxItem->pvContainer = NULL;

	/* 初始化完整性检查字段的变量 */ 
	listSET_FIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE( pxItem );
	listSET_SECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE( pxItem );
}

列表初始化完成后,如下:
在这里插入图片描述

4.3 列表项插入

列表项插入函数为vListInsert()函数,函数原型如下:

/********************************************************
参数:pxList:列表项要插入的列表   
     pxNewListItem :要插入的列表项
返回:无
*********************************************************/
void vListInsert( List_t *  const      pxList,
   				  ListItem_t * const   pxNewListItem )

函数代码如下:

void vListInsert( List_t * const pxList, ListItem_t * const pxNewListItem )
{
ListItem_t *pxIterator;
/* 
 * 列表中列表项按照项值升序排列,该值也表示优先级,项值越小,优先级越高。
 * 获取列表项的项值,可以找到 列表项插入的位置
 */
const TickType_t xValueOfInsertion = pxNewListItem->xItemValue;

	/* 检查列表和列表项的完整性 */
	listTEST_LIST_INTEGRITY( pxList );
	listTEST_LIST_ITEM_INTEGRITY( pxNewListItem );

	/* 如果插入的列表项项值为 portMAX_DELAY ,则插入到列表中pxList->xListEnd的前一项 */
	if( xValueOfInsertion == portMAX_DELAY )
	{
		pxIterator = pxList->xListEnd.pxPrevious;
	}
	else
	{
		/* 通过比较项值,找到列表项插入的地方, pxIterator 指向的列表项中项值大于xValueOfInsertion,则退出 */
		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->pvContainer = ( void * ) pxList;		⑤

	/* 列表项数量加1 */
	( pxList->uxNumberOfItems )++;						⑥
}

如下图,有XItemValue 为1和3的两个列表项,现将xItemValue为2的列表项插入列表中,在程序中通过for()循环找到了插入的位置。此时,pxIterator将会指向XItemValue为1的列表项。
总共需要6步,对应步骤如下:

在这里插入图片描述

4.4 列表项末尾插入

列表项末尾插入为vListInsertEnd()函数,函数原型如下:

/********************************************************
参数:pxList:列表项要插入的列表   
     pxNewListItem :要插入的列表项
返回:无
*********************************************************/
void vListInsertEnd( List_t * const 		pxList, 
                     ListItem_t * const 	pxNewListItem )

函数代码如下:

void vListInsertEnd( List_t * const pxList, ListItem_t * const pxNewListItem )
{
    ListItem_t * const pxIndex = pxList->pxIndex;
    
	/* 代码完整性检查 */
	listTEST_LIST_INTEGRITY( pxList );
	listTEST_LIST_ITEM_INTEGRITY( pxNewListItem );
	
	/*
	 *  列表项插入到末尾中,其末尾不是xListEnd的末尾,而是把xList看成表头,
	 *  然后新的列表项作为末尾,即XListEnd的后一项
     */
	pxNewListItem->pxNext = pxIndex;					①
	pxNewListItem->pxPrevious = pxIndex->pxPrevious;	②

	mtCOVERAGE_TEST_DELAY();

	pxIndex->pxPrevious->pxNext = pxNewListItem;		③
	pxIndex->pxPrevious = pxNewListItem;				④
	
	/* 列表项指向属于它的列表 */
	pxNewListItem->pvContainer = ( void * ) pxList;		⑤

	/* 列表项数量加1 */
	( pxList->uxNumberOfItems )++;						⑥
}

将列表项插入到列表的尾部,即插入到表头的前一项中,插入过程如下:
在这里插入图片描述

4.2 列表中列表项的删除

从列表中移除一个列表项使用uxListRemove()函数,其原型如下:

/********************************************************
参数:pxItemToRemove :要移除的列表项
返回:返回列表中列表项的数量
*********************************************************/
UBaseType_t uxListRemove( ListItem_t * const pxItemToRemove )

函数代码如下:

UBaseType_t uxListRemove( ListItem_t * const pxItemToRemove )
{

List_t * const pxList = ( List_t * ) pxItemToRemove->pvContainer;

	/* 移除列表项 */
	pxItemToRemove->pxNext->pxPrevious = pxItemToRemove->pxPrevious;	①
	pxItemToRemove->pxPrevious->pxNext = pxItemToRemove->pxNext;		②

	mtCOVERAGE_TEST_DELAY();

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

	/* 使列表项不属于任何一个列表 */
	pxItemToRemove->pvContainer = NULL;

	/* 列表项数量减1 */
	( pxList->uxNumberOfItems )--;

	/* 返回列表的列表项数量 */
	return pxList->uxNumberOfItems;
}

从列表中移除列表项步骤如下:
在这里插入图片描述

参考资料:

[1]: 正点原子:《STM32F407 FreeRTOS开发手册V1.1.pdf》
[2]: 野火:《FreeRTOS 内核实现与应用开发实战指南.pdf》

  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值