freertos任务管理

1 篇文章 0 订阅
1 篇文章 0 订阅

TODO(未完待续)

  1. 核心调度器的调度实现部分介绍完成
  2. 时间片的处理介绍完成
  3. 任务切换处理介绍完成
  4. 空闲任务未完成
  5. 定时器任务未介绍完成
  6. 通信方式实现未介绍完成

freertos概述

freertos属于小系统实时操作系统,支持多任务实时操作系统。多任务通过链表实现连接存储,通过时间片完成任务切换。

freertos关键变量说明

PRIVILEGED_DATA static volatile UBaseType_t uxCurrentNumberOfTasks 	= ( UBaseType_t ) 0U;
PRIVILEGED_DATA static volatile TickType_t xTickCount 				= ( TickType_t ) 0U;
PRIVILEGED_DATA static volatile UBaseType_t uxTopReadyPriority 		= tskIDLE_PRIORITY;
PRIVILEGED_DATA static volatile BaseType_t xSchedulerRunning 		= pdFALSE;
PRIVILEGED_DATA static volatile UBaseType_t uxPendedTicks 			= ( UBaseType_t ) 0U;
PRIVILEGED_DATA static volatile BaseType_t xYieldPending 			= pdFALSE;
PRIVILEGED_DATA static volatile BaseType_t xNumOfOverflows 			= ( BaseType_t ) 0;
PRIVILEGED_DATA static UBaseType_t uxTaskNumber 					= ( UBaseType_t ) 0U;
PRIVILEGED_DATA static volatile TickType_t xNextTaskUnblockTime		= ( TickType_t ) 0U; /* Initialised to portMAX_DELAY before the scheduler starts. */
PRIVILEGED_DATA static TaskHandle_t xIdleTaskHandle					= NULL;			/*< Holds the handle of the idle task.  The idle task is created automatically when the scheduler is started. */
  1. pxCurrentTCB : 当前任务TCB描述符
  2. uxCurrentNumberOfTasks:当前就绪列表任务数量
  3. xTickCount: systick的中断计数
  4. uxTopReadyPriority: 当前运行就绪任务的优先级,即是目前最高的优先级。
  5. xSchedulerRunning: 调度器运行标志位
  6. uxPendedTicks: 悬挂时的ticks统计(也是通过systick的中断计数统计)
  7. xYieldPending:
  8. xNumOfOverflows:溢出列表对应的任务数
  9. uxTaskNumber:记录所有任务数
  10. xNextTaskUnblockTime:阻塞任务的下次到达时间,也对应延时任务列表中任务最小触发时间。
  11. xIdleTaskHandle:空闲任务句柄

任务类型

  1. idle任务
  2. 定时器任务
  3. 线程任务

任务管理列表

PRIVILEGED_DATA TCB_t * volatile pxCurrentTCB = NULL;
PRIVILEGED_DATA static List_t pxReadyTasksLists[ configMAX_PRIORITIES ];
PRIVILEGED_DATA static List_t xDelayedTaskList1;
PRIVILEGED_DATA static List_t xDelayedTaskList2;
PRIVILEGED_DATA static List_t * volatile pxDelayedTaskList;
PRIVILEGED_DATA static List_t * volatile pxOverflowDelayedTaskList;
PRIVILEGED_DATA static List_t xPendingReadyList;

任务存储链表类型:list_t

  1. 就绪任务链表:用于存储当前系统可以抢占和调度的任务。每个优先级存在一个链表,一个链表用于存储同一优先级的所有任务,通过xtaskCreate创建,在插入链表是不分先后,默认插入到链表末尾。
    就绪任务链表:pxReadyTasksLists[configMAX_PRIORITIES]
    最大优先级配置,最大优先级通过配置宏configMAX_PRIORITIES实现。
  2. 延时任务队列链表:用于存储执行延时调度的任务。通过vTaskDelay实现,即是osDealy函数。任务延时。延时任务队列链表分为两个,一个为延时任务队列链表,另外一个为溢出延时队列链表。
    延时任务队列链表:xDelayedTaskList1 --> pxDelayedTaskList
    溢出延时队列链表:xDelayedTaskList2 --> pxOverflowDelayedTaskList
    设置延时时,会通过延时使得当前任务被加入到延时列表中。使用唤醒时间(链表的xItemValue=当前时间计数 + 延时时间)作为key来决定插入链表的位置。最开始位置保持为延时时间节点最小的任务,链表开始到结束任务需要等待的时间依次增加。当任务的唤醒时间小于等于当前时间计数时,则任务添加到溢出延时任务列表,否则任务放入延时任务列表;当系统时间计数溢出时,延时任务列表和溢出延时任务列表交换。
    通过xNextTaskUnblockTime及链表延时任务的xItemValue决定延时任务是否执行。xNextTaskUnblockTime表示当前最近的延时任务触发的时间计数。而链表延时任务的xItemValue表示了对应任务的延时触发时间计数。
  3. xPendingReadyList
  4. xTasksWaitingTermination
  5. xSuspendedTaskList

任务内存分配

每个任务存在两片内存,一片内存为任务描述符,代码中使用TCB_t表示; 另外一片内存为任务栈pxStack。
freertos中根据内存的生长方向,将内存分为两种分配方式。

  • 向下生长
    在这里插入图片描述
  • 向上生长
    在这里插入图片描述

任务描述符

任务描述符:TCB_t --> tskTCB

  1. stack
    • 默认填充内容:0xa5UL
    • 4 * stack_size, 4字节对齐
    • 向上生长或向下生长(默认生长方向)
  2. xStateListItem :状态链表项:调度器通过把任务TCB中的状态列表项xStateListItem作为链表节点将任务添加到就绪、阻塞、挂起列表
    • 初始化0x5a5a5a5a
    • pvOwner = pxNewTCB
  3. xEventListItem:事件链表项:用作多任务建通信方式,如事件,队列等
    • 初始化0x5a5a5a5a
    • xItemValue = configMAX_PRIORITIES - uxPriority
    • pvOwner = pxNewTCB
typedef struct tskTaskControlBlock
{
    volatile StackType_t *pxTopOfStack;/*指向任务栈的栈顶位置*/
    ListItem_t xStateListItem;/*任务的状态列表项,用于表示任务状态(Ready, Blocked, Suspended ). */
    ListItem_t   xEventListItem;  /*任务的时间列表项,用于描述任务的触发事件*/
    UBaseType_t   uxPriority;   /* 当前任务的优先级 */
    StackType_t   *pxStack;   /* 任务栈内存指针 */
    char    pcTaskName[ configMAX_TASK_NAME_LEN ];/* 任务名字 */ 

    #if ( portSTACK_GROWTH > 0 )
    StackType_t  *pxEndOfStack;  /* 当栈内存正向生长时,该指针指向栈底位置,用于判断是否栈溢出 */
    #endif

    #if ( portCRITICAL_NESTING_IN_TCB == 1 )
    UBaseType_t  uxCriticalNesting; /*保存临界区嵌套深度*/
    #endif

    #if ( configUSE_TRACE_FACILITY == 1 )
    UBaseType_t  uxTCBNumber;  /* tcb的数量 */
    UBaseType_t  uxTaskNumber;  /* 任务数量 */
    #endif

    #if ( configUSE_MUTEXES == 1 )
    UBaseType_t  uxBasePriority;  /* 存任务的基础优先级 */
    UBaseType_t  uxMutexesHeld;
    #endif

    #if ( configUSE_APPLICATION_TASK_TAG == 1 )
    TaskHookFunction_t pxTaskTag;
    #endif

    #if( configGENERATE_RUN_TIME_STATS == 1 )
    uint32_t  ulRunTimeCounter; /*< Stores the amount of time the task has spent in the Running state. */
    #endif

} tskTCB;

任务栈

栈内存必须按4字节对齐。根据宏定义配置可通过pvPortMalloc申请,也可静态指定。
xTaskCreate函数动态申请内存,而其形参的栈大小usStackDepth对应的实际内存为4*usStackDepth字节,同时初始化将栈内存中的内容按字节全部填充为0xa5UL。当任务切换时,使用任务的栈顶的内存存储芯片寄存器用于保存现场,除去现场保存部分,剩余的栈内存用于任务应用的栈内存所需。
指针pxStack指向堆栈的起始位置,任务创建时会分配指定数目的任务堆栈,申请堆栈内存函数返回的指针就被赋给该变量。pxTopOfStack指向当任务栈栈顶,随着进栈出栈,pxTopOfStack指向的位置是会变化的。随着任务的运行,堆栈可能会溢出,在堆栈向下增长的系统中,使用pxStack变量和pxTopOfStack检查堆栈是否溢出;如果在堆栈向上增长的系统中,使用pxEndOfStack和pxTopOfStack来诊断是否堆栈溢出。

任务寄存器的存储位置

任务创建申请内存后,从栈顶位置(pxTopOfStack)开始依次向下存储芯片寄存器内容用于保存任务切换现场。参考如下表。

栈位置对应寄存器初始数据说明
0x0NULL
-0x4XPSR0x01000000
-0x8PCpxCode任务函数指针用于给PC寄存器
-0xcLRprvTaskExitError
-0x10R12
-0x14R3
-0x18R2
-0x1cR1
-0x20R0pvParameters任务参数
-0x24portINITIAL_EXEC_RETURN0xfffffffd
-0x28R11
-0x2cR10
-0x30R9
-0x34R8
-0x38R7
-0x3cR6
-0x40R5
-0x44R4
-0x48R7
其他任务应用栈区
  1. 上述表中数据空的位置表示为无具体含义
  2. 栈顶内存对于现场存储的具体分配取决于实际的MCU控制芯片,上述表中数据参考于ARM_CM4芯片

优先级处理

在任务描述符中使用uxPriority表示任务优先级,该值越大,则表示任务优先级越高。任务的最高优先级是通过FreeRTOSConfig.h文件中的configMAX_PRIORITIES配置,可以使用的任务优先级范围是0~configMAX_PRIORITIES-1,当设置任务优先级大于等于configMAX_PRIORITIES时,则任务优先级被设置为configMAX_PRIORITIES-1。其中0优先级任务有系统设置为idle任务,即是空闲任务。由于每一个优先级会定义一个任务就绪列表,故configMAX_PRIORITIES的值越大,也就意味着任务就绪列表数组越大,占用资源越多。

uxTopReadyPriority用于统计就绪列表最高任务优先级,在将任务加入就绪列表是根据加入任务优先级调整该值,并在后续任务切换时选择最高优先级使用。

freertos内核使用了arm芯片提供的3个中断,分别是penSV、SVC和systick中断,这禅个硬件的中断优先级会设置为芯片最低的任务优先级,以确保其他硬件中断能够的到完整执行不被打断,同时也避免中断中发生任务切换导致终端中出现宕机。
freertos任务调度中断的函数

#define vPortSVCHandler           SVC_Handler
#define xPortPendSVHandler        PendSV_Handler
#define xPortSysTickHandler       SysTick_Handler
  1. penSV : 用于实现任务切换
  2. SVC : 用于发起任务切换,但并不是绝对使用。只有当在中断中手动调度时使用该调度。
  3. systick中断 :通过时间片实现最高优先级任务切换,或者osDelay时间到达触发任务切换。
    中断的实现将在下面具体介绍。

freertos调用流程

系统启动初始化

freertos启动
系统时基初始化
硬件初始化
时钟初始化
复位/关闭看门狗
vTaskStartScheduler
创建任务
  1. 时钟初始化:分系统时钟初始化,滴答定时器初始化,定时器初始化。这里存在两个定时器初始化,一个用于freertos,一个用于芯片外设库。
    外设时钟定时器的时钟由外设需求决定,如stm32的时钟为1ms
    freertos使用定时器作为系统操作时基和节拍。决定系统响应的速率(实时的颗粒度)以及任务切换的最小时间片
  2. 创建任务时并不会立刻执行相应的任务,只会将任务加入到就序列表,在调用vTaskStartScheduler之前或者在vTaskEndScheduler之后都不会执行创建任务。任务是否执行由xSchedulerRunning变量决定,该变量默认false,只有当vTaskStartScheduler运行时才调整为true,当vTaskEndScheduler运行时将其调整为false。在vTaskStartScheduler到vTaskEndScheduler之间创建任务,会将任务直接添加到就绪列表,并判断当前优先级是否高于uxTopReadyPriority,来决定任务是否立马调度执行。
  3. vTaskStartScheduler会初始空闲任务(prvIdleTask),定时器任务(prvTimerTask,通过configUSE_TIMERS配置使用),初始化xNextTaskUnblockTime和xTickCount的值,初始化svc中断、penSV中断、定时器中断,并通过"svc 0"触发svc中断完成第一个任务调度。只要存在任务,且不停止调度器,则该函数不会继续向下执行,永久停留在这里,并该函数不会退出。
vTaskStartScheduler
xPortStartScheduler
xTickCount=0
xSchedulerRunning=true
xNextTaskUnblockTime设置为最大值
创建prvTimerTask任务
创建prvIdleTask任务
xPortStartScheduler
prvStartFirstTask
使能VPF
uxCriticalNesting=0
启动定时器中断
启动MPU
设置penSV和systick同样优先级
  1. 设置penSV和systick同样优先级,svc更高一级优先级
  2. uxCriticalNesting表示临界区嵌套层数
prvStartFirstTask
进入svc中断
触发svc中断
启动SV

通过汇编指令svc 0是的程序触发svc中断。

任务创建

xTaskCreate
prvInitialiseNewTask
内存申请
prvInitialiseNewTask
taskYIELD_IF_USING_PREEMPTION
prvAddTaskToReadyList
pxPortInitialiseStack
prvInitialiseTaskLists
prvAddNewTaskToReadyList

taskYIELD_IF_USING_PREEMPTION即是portYIELD_WITHIN_API的宏定义。主要目的用于触发pendSV中断。

#ifndef portYIELD_WITHIN_API
	#define portYIELD_WITHIN_API portYIELD
#endif

#define portSVC_YIELD						1
#define portYIELD()				__asm{ SVC portSVC_YIELD } /* portYIELD_FROM_ISR使用 */

#define portSY_FULL_READ_WRITE		( 15 )
#define portYIELD_WITHIN_API() 													\
{																				\
	/* Set a PendSV to request a context switch. */								\
	portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;								\
																				\
	/* Barriers are normally not required but do ensure the code is completely	\
	within the specified behaviour for the architecture. */						\
	__dsb( portSY_FULL_READ_WRITE );											\
	__isb( portSY_FULL_READ_WRITE );											\
}

任务的时间片处理

xPortSysTickHandler
false
true
开启中断
xTaskIncrementTick
关闭中断
触发penSV中断

触发penSV中断的实现:portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT
关闭中断(portDISABLE_INTERRUPTS):__asm volatile( “cpsid i” )
开启中断 (portENABLE_INTERRUPTS) : __asm volatile( “cpsie i” )

xTaskIncrementTick函数用于判断是否需要发生任务调度,主要判断3部分功能是否调度。
uxSchedulerSuspended
延时调度 : xNextTaskUnblockTime和xTickCount
同优先级任务调度 : configUSE_PREEMPTION和configUSE_TIME_SLICING
执行tick_hook函数 : configUSE_TICK_HOOK

抢占调度:configUSE_PREEMPTION xYieldPending
函数返回值表示是否需要发生任务切换

xTaskIncrementTick
false
true
true
true
false
false
结束
抢占调度
xYieldPending
configUSE_PREEMPTION
执行tick_hook函数
同优先级任务调度
延时调度
uxSchedulerSuspended
uxPendedTicks自加
执行tick_hook函数
0
非0
结束
vApplicationTickHook
uxPendedTicks

通过宏configUSE_TICK_HOOK控制

延时调度
configUSE_PREEMPTION==1
0
非0
大于等于xNextTaskUnblockTime
小于xNextTaskUnblockTime
链表为空
链表非空
小于xItemValue
大于等于xItemValue
true
false
大于等于pxCurrentTCB优先级
小于pxCurrentTCB优先级
xTickCount
taskSWITCH_DELAYED_LISTS
xTickCount
计算xTickCount
开始
pxDelayedTaskList
结束
xNextTaskUnblockTime = portMAX_DELAY
xNextTaskUnblockTime = xItemValue
xTickCount
xItemValue = pxTCB->xStateListItem.xItemValue
pxTCB=pxDelayedTaskList的第一个节点
prvAddTaskToReadyList
从xEventListItem中移除
任务在xEventListItem中
从xStateListItem中移除该任务节点
xSwitchRequired = pdTRUE
pxTCB优先级

taskSWITCH_DELAYED_LISTS: 实现延时任务列表和溢出延时任务列表指针交互,同时复位xNextTaskUnblockTime值

taskSWITCH_DELAYED_LISTS
prvResetNextTaskUnblockTime
链表空
链表非空
结束
assert
pxDelayedTaskList
开始
xNumOfOverflows++
pxDelayedTaskList和pxOverflowDelayedTaskList指针互换
xNextTaskUnblockTime = portMAX_DELAY
pxDelayedTaskList
xNextTaskUnblockTime = pxDelayedTaskList第一个任务的xStateListItem.xItemValue
同优先级任务调度
链表长度大于1
链表长度不大于1
结束
xSwitchRequired = pdTRUE
运行任务优先级就绪链表
开始

编译条件:configUSE_PREEMPTION == 1 ,configUSE_TIME_SLICING==1

–> f{configUSE_PREEMPTION & xYieldPending} --true–> 抢占调度

xTickCount : 表示进入systick的节拍数,不表示具体的时间,只是一个相对的时间。如果systick中断触发为1ms,则对应xTickCount的值就是毫秒计数。但如果systick中断的触发不是1ms,则xTickCount只表示对应进入中断次数,不再对应毫秒数。同理,由于freertos中所有与时间有关的动作都以此计数作为时间判断,故所有时间都不一定为毫秒级,都是通过该值作为可控时间粒度进行系统度量。
又由于systick是硬件中断优先级配置为最低,也就意味着当发生其他硬件中断时,其他中断执行的时间越短,这里时间片的精度也就越高。同样则也意味着系统的时间片永远不是精确的,始终会受到应用层代码的影响。
由于freertos更加偏向于小型控制器操作系统,常规使用systick即为1ms一次进入中断,这并不是对所有系统都是友好的。当前控制器芯片的主频范围可能从十几兆到上G,对于一些成本很低的产品,则控制器的主频很低,这时systick的1ms中断对于系统将变成很大的开销,这并不是开发中希望的;对于一些特殊应用要求也是类似,为了追求更好的性能,则不希望在系统的切换上花费过多的开销。

任务delay的实现

vTaskDelay
false
true
结束
portYIELD_WITHIN_API
xAlreadyYielded
xAlreadyYielded = xTaskResumeAll
prvAddCurrentTaskToDelayedList
vTaskSuspendAll
开始

vTaskSuspendAll:表示暂停任务调度器,通过uxSchedulerSuspended=1来实现

prvAddCurrentTaskToDelayedList
INCLUDE_vTaskSuspend==1
INCLUDE_vTaskSuspend==0
等于portMAX_DELAY
true
不等于portMAX_DELAY
false
小于xTickCount
不小于xTickCount
小于xNextTaskUnblockTime
小于xTickCount
不小于xTickCount
小于xNextTaskUnblockTime
不小于xNextTaskUnblockTime
不小于xNextTaskUnblockTime
从pxCurrentTCB->xStateListItem移除任务
开始
结束
pxCurrentTCB->xStateListItem.xItemValue=xTimeToWake
xTimeToWake = xTickCount + xTicksToWait
portRESET_READY_PRIORITY
pxCurrentTCB->xStateListItem插入pxOverflowDelayedTaskList
xTimeToWake
xNextTaskUnblockTime = xTimeToWake
xTimeToWake
pxCurrentTCB->xStateListItem插入pxDelayedTaskList
xTimeToWake
xCanBlockIndefinitely
xTicksToWait
pxCurrentTCB->xStateListItem.xItemValue=xTimeToWake
xTimeToWake = xTickCount + xTicksToWait
pxCurrentTCB->xStateListItem插入pxDelayedTaskList
xTimeToWake
xNextTaskUnblockTime = xTimeToWake

任务的切换过程

freertos中任务切换使用的是penSV和svc。
其中调度器在需要时直接发起pendSV中断请求,然后再penSV中完成栈的pop和push,以及前一个任务的现场保存。而svc更多用于现场用户主动调度。

pendSV
xPortPendSVHandler
使用FPU
未使用FPU
使用FPU
将r14,r4-r11存入r0,即psp
将s16-s31保存到r0中,即psp,同时r0自行生长
r14
r3读取当前运行任务TCB
R0=任务堆栈指针PSP的值
开始
结束
通过r14返回该现场上次调用函数
isb
将新任务栈顶指针赋值给psp
将r0加载到s16-s31中
将r0数据加载到r14,r4-r11存入,同时r0自行生长
恢复栈现场
获取pxCurrentTCB的栈顶指针
退出临界区
vTaskSwitchContext
进入临界区
将最新的任务堆栈指针保存到任务TCB的pxTopOfStack字段中

vTaskSwitchContext之前代码在进行即将运行结束的任务进行现场保存,包含FPU寄存器,pc等控制器寄存器,栈指针的寄存器保存,用于下次再运行该任务时恢复现场使用,即是vTaskSwitchContext之后的动作。vTaskSwitchContext之后的代码与之前代码执行是对称的,执行内容相同,但操作方向和时序相反,主要作用用于取出先前该任务执行结束时保存的现场,并将其赋予相应的寄存器,让控制器能够继续按上次运行结束的状态运行。
需要注意的是vTaskSwitchContext前后的pxCurrentTCB值是不同的,vTaskSwitchContext前的pxCurrentTCB指向任务切换前运行的任务,该任务即将切出不运行;vTaskSwitchContext后的pxCurrentTCB指向任务切换后运行的任务,该任务即是将要运行的任务。vTaskSwitchContext主要作用即是切换任务,将现在运行的任务关闭,找到即将要运行的任务并将任务tcb赋予pxCurrentTCB变量。
isb : 指令执行屏障,确保先前指令执行完成

vTaskSwitchContext
taskSELECT_HIGHEST_PRIORITY_TASK
configUSE_NEWLIB_REENTRANT == 1
true
false
uxTopPriority对应列表为空
0
非0
uxTopPriority对应列表非空
结束
xYieldPending = pdTRUE
uxSchedulerSuspended
开始
xYieldPending = pdFALSE
taskCHECK_FOR_STACK_OVERFLOW
计算总运行时间
_impure_ptr = &pxCurrentTCB->xNewLib_reent
assert
uxTopPriority
pxReadyTasksLists
uxTopPriority--
uxTopReadyPriority = uxTopPriority
pxCurrentTCB = 当前优先级就绪列表下一个任务
  1. taskSELECT_HIGHEST_PRIORITY_TASK用于获取就绪列表数组中最高优先级任务,同时更新最高优先级记录(uxTopReadyPriority)
  2. 计算总运行时间(ulTotalRunTime): 通过定义更精确的时间基准来统计运行时间,该时间可后续用于计算CPU使用率和其他一些状态。通过宏configGENERATE_RUN_TIME_STATS和portALT_GET_RUN_TIME_COUNTER_VALUE控制选择具体的接口,且接口portALT_GET_RUN_TIME_COUNTER_VALUE和portALT_GET_RUN_TIME_COUNTER_VALUE由用户自定义如何计算时间。
svc
vPortSVCHandler
结束
通过r14寄存器返回任务运行现场
开启中断
isb
psp指向任务数据栈顶
将栈顶寄存器值存到r4-r11,r14中,并r0沿取值方向生长
r0 = 栈顶地址
r1 = 栈地址
r3 = pxCurrentTCB地址
开始

上述过程与pendSV中断处理函数中vTaskSwitchContext执行的内容基本相同都是用于现场恢复。svc中断时通过软件触发的,通常用于开始运行任务调度器时才会调度该函数,因为调度器开始前本就没必要进行现场数据保存。

定时器任务(后续添加)

xTimerCreateTimerTask

configTIMER_TASK_PRIORITY

idle任务(后续添加)

消息队列(后续添加)

事件(后续添加)

锁(后续添加)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值