AliOS-Things+STM32 (七) 进程管理(上)

本文深入剖析AliOS Things操作系统内核启动流程,详细解读krhino_init函数中的关键步骤,包括进程初始化、调度器配置及核心组件如tick链表、kobj链表的初始化。同时,分析了task_create函数的实现细节,以及动态内存进程释放机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在alios things启动过程中,krhino_init中,做完memory堆初始化,就会初始化进程相关的东西。在分析的过程中,默认RHINO_CONFIG_KOBJ_LIST,RHINO_CONFIG_KOBJ_DYN_ALLOC,RHINO_CONFIG_SCHED_RR都使能,RHINO_CONFIG_CPU_NUM为1。RHINO_CONFIG_SCHED_RR表示高优先级会抢占低优先级,高优先级运行期间,低优先级没法抢占,只能等到高优先级主动退出,对于同等优先级的进程,各个进程会轮流运行一定的时间片。
我们先看下krhino_init初始化过程:

kstat_t krhino_init(void)
{
	/*设置系统为RHINO_STOPPED状态*/
    g_sys_stat = RHINO_STOPPED;

	/*RHINO_CONFIG_CPU_NUM = 1跳过g_sys_lock和g_task_del_head初始化*/
#if (RHINO_CONFIG_CPU_NUM > 1)
    krhino_spin_lock_init(&g_sys_lock);
    klist_init(&g_task_del_head);
#endif
	/*初始化就绪队列g_ready_queue*/
    runqueue_init(&g_ready_queue);
	/*初始化tick链表,这个也和进程切换相关*/
    tick_list_init();
	
#if (RHINO_CONFIG_KOBJ_LIST > 0)
	/*初始化kobj链表*/
    kobj_list_init();
#endif
	/*RHINO_CONFIG_USER_HOOK 当前先不开启,后续章节分析*/
#if (RHINO_CONFIG_USER_HOOK > 0)
    krhino_init_hook();
#endif
	/*堆内存初始化,前两章已经分析过*/
#if (RHINO_CONFIG_MM_TLF > 0)
    k_mm_init();
#endif

#if (RHINO_CONFIG_KOBJ_DYN_ALLOC > 0)
	/*初始化进程资源释放相关的链表, 信号量以及释放管理进程*/
    klist_init(&g_res_list);
    krhino_sem_create(&g_res_sem, "res_sem", 0);
    /*dyn_mem_proc_task_start会创建一个dyn_mem_proc_task,调度器开始后就启动这个用来释放进程资源的进程
    这是创建的第一个task*/
    dyn_mem_proc_task_start();
#endif
	/*RHINO_CONFIG_CPU_NUM = 1*/
#if (RHINO_CONFIG_CPU_NUM > 1)
    for (uint8_t i = 0; i < RHINO_CONFIG_CPU_NUM; i++) {
        krhino_task_cpu_create(&g_idle_task[i], "idle_task", NULL, RHINO_IDLE_PRI, 0,
                               &g_idle_task_stack[i][0], RHINO_CONFIG_IDLE_TASK_STACK_SIZE,
                               idle_task, i, 1u);
    }
#else
	/*创建idle进程,调度器开始后正式启动idle进程,这是创建的第二个task*/
    krhino_task_create(&g_idle_task[0], "idle_task", NULL, RHINO_IDLE_PRI, 0,
                       &g_idle_task_stack[0][0], RHINO_CONFIG_IDLE_TASK_STACK_SIZE,
                       idle_task, 1u);
#endif

#if (RHINO_CONFIG_WORKQUEUE > 0)
	/*初始化workqueue,这是创建的第三个task*/
    workqueue_init();
#endif

#if (RHINO_CONFIG_TIMER > 0)
	/*初始化时钟,比较重要,后面分析,这是创建的第四个task*/
    ktimer_init();
#endif
	/*用来检查栈溢出,目前没使能不分析*/
    rhino_stack_check_init();

    return RHINO_SUCCESS;
}

void runqueue_init(runqueue_t *rq)
{
    uint8_t prio;

    rq->highest_pri = RHINO_CONFIG_PRI_MAX;

    for (prio = 0; prio < RHINO_CONFIG_PRI_MAX; prio++) {
        rq->cur_list_item[prio] = NULL; //所有优先级的链表先置空
    }
}
#define RHINO_CONFIG_PRI_MAX                 62
#define NUM_WORDS            ((RHINO_CONFIG_PRI_MAX + 31) / 32)
typedef struct {
    klist_t  *cur_list_item[RHINO_CONFIG_PRI_MAX]; //一共有62个进程链表, 每个链表挂不同优先级的进程
    uint32_t  task_bit_map[NUM_WORDS];	//每个bit位置1,代表一种优先级的进程存在。
    uint8_t   highest_pri;  //队列上进程的最高优先级
} runqueue_t;

这里我们单独看下dyn_mem_proc_task这个进程:

void dyn_mem_proc_task(void *arg)
{
	/*申明cpsr,后边RHINO_CRITICAL_ENTER/RHINO_CRITICAL_EXIT调用需要*/
    CPSR_ALLOC();

    size_t      i;
    kstat_t     ret;
    res_free_t *res_free;
    res_free_t  tmp;

    (void)arg;

    while (1) {
    	/*无限睡眠等待g_res_sem资源被释放*/
        ret = krhino_sem_take(&g_res_sem, RHINO_WAIT_FOREVER);
        if (ret != RHINO_SUCCESS) {
            k_err_proc(RHINO_DYN_MEM_PROC_ERR);
        }
		
        while (1) {
        	/*保存当前中断状态,并关中断*/
            RHINO_CRITICAL_ENTER();
            /*查看g_res_list是否为空, 为空则跳出while(1),否则直到释放完g_res_list上的资源*/
            if (!is_klist_empty(&g_res_list)) {
            	/*不为空,则将g_res_list上的对象取下来,放到res_free上*/
                res_free = krhino_list_entry(g_res_list.next, res_free_t, res_list);
                klist_rm(&res_free->res_list);
                /*恢复之前的中断状态*/
                RHINO_CRITICAL_EXIT();
               	/*释放res_free资源*/
#if (RHINO_CONFIG_USER_SPACE > 0)
                proc_free((ktask_t *)(res_free->res[1]));
#endif
                memcpy(&tmp, res_free, sizeof(res_free_t));
                for (i = 0; i < tmp.cnt; i++) {
                    krhino_mm_free(tmp.res[i]);
                }
            } else {
            	/*恢复之前的中断状态*/
                RHINO_CRITICAL_EXIT();
                break;
            }
        }
    }
}

#define RHINO_CPU_INTRPT_DISABLE()  do{cpsr = cpu_intrpt_save();}while(0)
#define RHINO_CPU_INTRPT_ENABLE()   do{cpu_intrpt_restore(cpsr);}while(0)
; Functions:
;     size_t cpu_intrpt_save(void);
;     void cpu_intrpt_restore(size_t cpsr);
;******************************************************************************
cpu_intrpt_save
	/*加载特殊寄存器的值(当前中断状态)到通用寄存器R0保存*/
    MRS     R0, PRIMASK
    /*关中断*/
    CPSID   I
    BX      LR

cpu_intrpt_restore
	/*将通用寄存器R0(之前的中断状态)恢复到特殊寄存器*/
    MSR     PRIMASK, R0
    /*之所以这边没有CPSIE I来开启中断,是因为这里要的是中断状态恢复原状,
    由于R0存的是之前的中断状态,如果之前是开启的,那么现在恢复到开启状态,
    如果之前是关闭的,那么现在恢复后依然关闭*/
    BX      LR

在上面krhino_init整个过程中,一共调用了四次task_create,分别创建了dyn_mem_proc_task, idle_task, DEFAULT-WORKQUEUE,timer_task,我们先看下这个重要的task_create:

/*
ktask_t *task: 出参,用来记录task初始化后的各种task参数
const name_t *name:入参,设定的task名
void *arg:入参:task entry函数执行时的入参
uint8_t prio:入参:task优先级
tick_t ticks:入参:当设置SCHED_RR调度方式,同一优先级进程的时间片执行时长。
cpu_stack_t *stack_buf:入参: task自己可用的栈buffer地址(已分配)
size_t stack_size:入参: task自己可用的栈长度(已分配)
task_entry_t entry:入参:task执行的函数
uint8_t autorun:入参:是否自动执行,设置1的话,task初始化后进入就绪K_RDY状态,设置0的话,task初始化后进入K_SUSPENDED状态。
uint8_t mm_alloc_flag:入参:K_OBJ_DYN_ALLOC/K_OBJ_STATIC_ALLOC,设置栈是动态申请的还是静态分配的,涉及到是否回收(系统task的堆栈一般都静态分配)
uint8_t cpu_num: 入参: CPU亲缘性,指定哪个cpu
uint8_t cpu_binded:入参:是否绑定cpu
*/
static kstat_t task_create(ktask_t *task, const name_t *name, void *arg,
                           uint8_t prio, tick_t ticks, cpu_stack_t *stack_buf,
                           size_t stack_size, task_entry_t entry, uint8_t autorun,
                           uint8_t mm_alloc_flag, uint8_t cpu_num, uint8_t cpu_binded)
{
    CPSR_ALLOC();

    cpu_stack_t *tmp;
    uint8_t      i = 0;

    NULL_PARA_CHK(task);
    NULL_PARA_CHK(name);
    NULL_PARA_CHK(entry);
    NULL_PARA_CHK(stack_buf);

    if (stack_size == 0u) {
        return RHINO_TASK_INV_STACK_SIZE;
    }

    if (prio >= RHINO_CONFIG_PRI_MAX) {
        return RHINO_BEYOND_MAX_PRI;
    }

    RHINO_CRITICAL_ENTER();

    INTRPT_NESTED_LEVEL_CHK();

    /* idle task is only allowed to create once */
    /*设定某一个CPU的idle task,对于每个CPU只能设定一个idle task*/
    if (prio == RHINO_IDLE_PRI) {
        if (g_idle_task_spawned[cpu_num] > 0u) {
            RHINO_CRITICAL_EXIT();
            return RHINO_IDLE_TASK_EXIST;
        }

        g_idle_task_spawned[cpu_num] = 1u;
    }

    RHINO_CRITICAL_EXIT();

    memset(task, 0, sizeof(ktask_t));

#if (RHINO_CONFIG_SCHED_RR > 0)
	/*设定每个task的可用时间片总时长*/
    if (ticks > 0u) {
        task->time_total = ticks;
    } else {
        task->time_total = RHINO_CONFIG_TIME_SLICE_DEFAULT;
    }
	/*设定每个task的时间片个数,以及调度策略*/
    task->time_slice   = task->time_total;
    task->sched_policy = KSCHED_RR;
#endif
	/*如果autorun>0,设置成就绪状态,否则先挂起*/
    if (autorun > 0u) {
        task->task_state = K_RDY;
    } else {
        task->task_state    = K_SUSPENDED;
        task->suspend_count = 1u;
    }
	
	/*设置堆栈地址*/
    /* init all the stack element to 0 */
    task->task_stack_base = stack_buf;
    tmp = stack_buf;
	/*清空堆栈*/
    memset(tmp, 0, stack_size * sizeof(cpu_stack_t));

    task->task_name     = name;
    task->prio          = prio;
    task->b_prio        = prio;
    task->stack_size    = stack_size;
    task->mm_alloc_flag = mm_alloc_flag;
    task->cpu_num       = cpu_num;
#if (RHINO_CONFIG_USER_SPACE > 0)
    task->mode             = 0;
    task->pid              = 0;
    task->task_ustack_base = 0;
    task->task_group       = 0;
#endif

	/*冗余代码,不知道做什么??*/
    cpu_binded = cpu_binded;
    i = i;

#if (RHINO_CONFIG_CPU_NUM > 1)
    task->cpu_binded = cpu_binded;
#endif

	/*检查task堆栈是否会有溢出问题*/
#if (RHINO_CONFIG_TASK_STACK_OVF_CHECK > 0)
	/*task堆栈由高地址往低地址增长*/
#if (RHINO_CONFIG_CPU_STACK_DOWN > 0)
    tmp  = task->task_stack_base;
    /*将堆栈最低的地址打上堆栈溢出magic标记0xdeadbeafu*/
    for (i = 0; i < RHINO_CONFIG_STK_CHK_WORDS; i++) {
        *tmp++ = RHINO_TASK_STACK_OVF_MAGIC;
    }
#else
    tmp  = (cpu_stack_t *)(task->task_stack_base) + task->stack_size - RHINO_CONFIG_STK_CHK_WORDS;
    for (i = 0; i < RHINO_CONFIG_STK_CHK_WORDS; i++) {
        *tmp++ = RHINO_TASK_STACK_OVF_MAGIC;
    }
#endif
#endif
    /*初始化堆栈,并设置到task_stack上, cpu_task_stack_init分析见下面*/
    task->task_stack = cpu_task_stack_init(stack_buf, stack_size, arg, entry);

#if (RHINO_CONFIG_USER_HOOK > 0)
    krhino_task_create_hook(task);
#endif
	/*目前没有支持*/
    TRACE_TASK_CREATE(task);

    RHINO_CRITICAL_ENTER();

#if (RHINO_CONFIG_KOBJ_LIST > 0)
	/*将task->task_stats_item挂到g_kobj_list链表的task_head上管理,g_kobj_list结构里管理着所有task,mutex等资源*/
    klist_insert(&(g_kobj_list.task_head), &task->task_stats_item);
#endif
	/*autorun 为正数,表示初始化完后直接设置就绪,可以运行。*/
    if (autorun > 0u) {
    	/*将task加到就绪队列上*/
        ready_list_add_tail(&g_ready_queue, task);
        /* if system is not start,not call core_sched */
        /*如果当前系统为RHINO_STOPPED状态,也就是调度器未启动状态,先不做调度。*/
        if (g_sys_stat == RHINO_RUNNING) {
            RHINO_CRITICAL_EXIT_SCHED();
            return RHINO_SUCCESS;
        }
    }

    RHINO_CRITICAL_EXIT();
    return RHINO_SUCCESS;
}

RHINO_INLINE void _ready_list_add_tail(runqueue_t *rq, ktask_t *task)
{
	/*判断这个task设定的优先级,在就绪队列上这个优先级的链表是否为空,为空就初始化这个队列*/
    if (is_ready_list_empty(task->prio)) {
        ready_list_init(rq, task);
        return;
    }
	/*不为空的话,将这个task直接放到queue上的该优先级的列表就行*/
    klist_insert(rq->cur_list_item[task->prio], &task->task_list);
}

RHINO_INLINE void ready_list_init(runqueue_t *rq, ktask_t *task)
{
    rq->cur_list_item[task->prio] = &task->task_list;
    klist_init(rq->cur_list_item[task->prio]);
    krhino_bitmap_set(rq->task_bit_map, task->prio);
	/*如果这个新增的优先级高于队列原来已有的最高优先级,那么将优先级赋值给rq->highest_pri*/
    if ((task->prio) < (rq->highest_pri)) {
        rq->highest_pri = task->prio;
    }
}

void *cpu_task_stack_init(cpu_stack_t *stack_base, size_t stack_size,
                          void *arg, task_entry_t entry)
{
    cpu_stack_t *stk;
    /*temp指向栈顶部(最高地址处)*/
    uint32_t temp = (uint32_t)(stack_base + stack_size);
	
    /* stack aligned by 8 byte */
    temp &= 0xfffffff8; 
    stk = (cpu_stack_t *)temp;
	/*由高地址到低地址,初始化以下寄存器*/
    /* task context saved & restore by hardware: */
    *(--stk) = (cpu_stack_t)0x01000000L; /* xPSR: EPSR.T = 1, thumb mode   */
    *(--stk) = (cpu_stack_t)entry;       /* Entry Point                    */
    *(--stk) = (cpu_stack_t)krhino_task_deathbed; /* R14 (LR)  entry函数执行完返回后的处理 */
    *(--stk) = (cpu_stack_t)0x12121212L; /* R12                            */
    *(--stk) = (cpu_stack_t)0x03030303L; /* R3                             */
    *(--stk) = (cpu_stack_t)0x02020202L; /* R2                             */
    *(--stk) = (cpu_stack_t)0x01010101L; /* R1                             */
    *(--stk) = (cpu_stack_t)arg;         /* R0 : argument     entry的入参  */

    /* task context saved & restore by software: */
    /* EXC_RETURN = 0xFFFFFFFDL
       Task begin state: Thread mode +  non-floating-point state + PSP */
    *(--stk) = (cpu_stack_t)0xFFFFFFFDL;

    *(--stk) = (cpu_stack_t)0x11111111L; /* R11 */
    *(--stk) = (cpu_stack_t)0x10101010L; /* R10 */
    *(--stk) = (cpu_stack_t)0x09090909L; /* R9  */
    *(--stk) = (cpu_stack_t)0x08080808L; /* R8  */
    *(--stk) = (cpu_stack_t)0x07070707L; /* R7  */
    *(--stk) = (cpu_stack_t)0x06060606L; /* R6  */
    *(--stk) = (cpu_stack_t)0x05050505L; /* R5  */
    *(--stk) = (cpu_stack_t)0x04040404L; /* R4  */

    return stk;
}

至此,task_create创建完成,如果在系统没有初始化完,未running状态,则新建的task不会走调度流程,如果在系统初始化完成后,task才会有机会按照优先级判定,执行一次调度。

由于之前分析过dyn_mem_proc_task,我们接着看另外三个idle_task, DEFAULT-WORKQUEUE,timer_task的task实体函数如何执行的。
idle_task在RHINO_CONFIG_CPU_NUM =1, RHINO_CONFIG_USER_HOOK=0,RHINO_CONFIG_PWRMGMT=0的情况下,基本就是个while(1)的死循环,我们在多核的时候分析。
DEFAULT-WORKQUEUE的创建和普通的workqueu并没有太大区别,后续workqueue章节再分析。
下面粗略看timer_task也就是定时器相关的初始化:

void ktimer_init(void)
{
	/*首先初始化g_timer_head*/
    klist_init(&g_timer_head);

	/*初始化timer_queue的buf queue,alios中的buf queu相当于freertos中的msg queue,用来做消息队列*/
    krhino_fix_buf_queue_create(&g_timer_queue, "timer_queue", timer_queue_cb,
                                sizeof(k_timer_queue_cb), RHINO_CONFIG_TIMER_MSG_NUM);
	/*定时器处理程序,这部分逻辑单独拿出一章做分析。*/
    krhino_task_create(&g_timer_task, "timer_task", NULL,
                       RHINO_CONFIG_TIMER_TASK_PRI, 0u, g_timer_task_stack,
                       RHINO_CONFIG_TIMER_TASK_STACK_SIZE, timer_task, 1u);
}

至此, krhino_init流程差不多了,后面再初始化了一个main_task,用来直接运行用户态的应用程序,之后就进入krhino_start()启动调度器,开始做进程调度。

int main(void)
{
    /*irq initialized is approved here.But irq triggering is forbidden, which will enter CPU scheduling.
    Put them in sys_init which will be called after aos_start.
    Irq for task schedule should be enabled here, such as PendSV for cortex-M4.
    */
    board_init();   //including aos_heap_set();  flash_partition_init();

    /*kernel init, malloc can use after this!*/
    krhino_init();

    /*main task to run */
    krhino_task_dyn_create(&g_main_task, "main_task", 0, OS_MAIN_TASK_PRI, 0, OS_MAIN_TASK_STACK, (task_entry_t)sys_init, 1);

    /*kernel start schedule!*/
    krhino_start();

    /*never run here*/
    return 0;
}

kstat_t krhino_start(void)
{
    ktask_t *preferred_task;
	/*调度器未启动时,g_sys_stat 为RHINO_STOPPED状态*/
    if (g_sys_stat == RHINO_STOPPED) {
#if (RHINO_CONFIG_CPU_NUM > 1)
        for (uint8_t i = 0; i < RHINO_CONFIG_CPU_NUM; i++) {
            preferred_task            = preferred_cpu_ready_task_get(&g_ready_queue, i);
            preferred_task->cpu_num   = i;
            preferred_task->cur_exc   = 1;
            g_preferred_ready_task[i] = preferred_task;
            g_active_task[i]          = g_preferred_ready_task[i];
            g_active_task[i]->cur_exc = 1;
        }
#else
		/*preferred_task取出就绪队列上优先级最高的任务*/
        preferred_task = preferred_cpu_ready_task_get(&g_ready_queue, 0);
        /*赋值给当前CPU0的优先级最高的任务和运行任务。*/
        g_preferred_ready_task[0] = preferred_task;
        g_active_task[0] = preferred_task;
#endif

#if (RHINO_CONFIG_USER_HOOK > 0)
        krhino_start_hook();
#endif
		/*g_sys_stat 设置为运行状态,表示调度器正式启动*/
        g_sys_stat = RHINO_RUNNING;
        /*开始调度*/
        cpu_first_task_start();

        /* should not be here */
        return RHINO_SYS_FATAL_ERR;
    }

    return RHINO_RUNNING;
}
;******************************************************************************
; Functions:
;     void cpu_first_task_start(void);
;******************************************************************************
cpu_first_task_start
    ;set PendSV prority to the lowest
    LDR     R0, =SHPR3_PRI_14   //0xE000ED22
    LDR     R1, =PRI_LVL_PENDSV //0xFF
    STRB    R1, [R0]

    ;set Systick prority to the lowest
    LDR     R0, =SHPR3_PRI_15	//0xE000ED23
    LDR     R1, =PRI_LVL_SYSTICK //0xFF
    STRB    R1, [R0]		

    ;indicate PendSV_Handler branch to _pendsv_handler_nosave
    MOVS    R0, #0
    MSR     PSP, R0   /*将堆栈指针PSP赋值0*/

    ;make PendSV exception pending
    LDR     R0, =SCB_ICSR 	/*0xE000ED04*/
    LDR     R1, =ICSR_PENDSVSET 	/*0x10000000*/
    STR     R1, [R0]		/*执行PendSV 中断*/

    ;goto PendSV_Handler
    CPSIE   I
    B       .

在这里插入图片描述
所以接下来我们看PendSV_Handler

PendSV_Handler
    CPSID   I
    MRS     R0, PSP
    ;branch if cpu_first_task_start
    CMP     R0, #0 	/*由于之前PSP被我们设置成0,所以跳转到_first_task_restore*/
    BEQ     _first_task_restore 
...

_first_task_restore
    ;set MSP to the base of system stack
    LDR     R0, =SCB_VTOR		//0xE000ED08
    LDR     R0, [R0]			//R0取值为0xE000ED08上的值,为0x8000000(在SystemInit中,设置过SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET;)
    LDR     R0, [R0]			//R0取值为0x8000000上的值,为0x200017E0 ,这个是内存上向量表的起始地址,赋给MSP作为栈的首地址(栈向下增长)
    MSR     MSP, R0				//赋给MSP堆栈寄存器

    B       _pendsv_handler_nosave

    ALIGN
    END


_pendsv_handler_nosave
    LDR     R0, =g_active_task  			
    LDR     R1, =g_preferred_ready_task		
    LDR     R2, [R1]						
    STR     R2, [R0]			//将g_preferred_ready_task上的要运行的task赋给g_active_task 
    ;R0 = g_active_task->task_stack = context region
    LDR     R0, [R2]			//R0获得运行的task上task_stack的地址

    ;restore context
    LDM     R0, {R4-R11, LR}		//将运行的task的task_stack内容还原到CPU寄存器上
    ADDS    R0, R0, #0x24			//看回上面的cpu_task_stack_init函数,0x24=36,也就是R0跳过9个寄存器,
    								//将R0 直接指向到 (cpu_stack_t)arg位置。

    ;return stack = PSP
    MSR     PSP, R0					//将R0赋给PSP

    CPSIE   I						//使能中断
    ;hardware restore R0~R3,R12,LR,PC,xPSR
    BX      LR						
    /*跳转到LR,这里我们只恢复r4-r11寄存器,因为其它寄存器会,在BX LR时,
    根据返回线程模式的过程,根据上面R0的位置,自动恢复(r0-r3、r12、lr、pc和xpsr寄存器),
    LR最终指向task的entry函数,完成跳转。*/

在这里插入图片描述
那么到现在开始, 调度器开始运作,并将之前优先级最高的timer_task先运行了起来,那么当进程由于时间片用完或者让出CPU等的时候, 进程如何切换的呢?我们下一章再分析。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值