Linux kernel 启动流程分析4

Linux kernel 启动流程分析4---第一阶段proc info的获取

一、概述

1.1、kernel启动流程第一阶段简单说明

kernel 启动流程第一阶段

+-----------------------------------+
|           入口: stext             |
+-----------------------------------+
               |
               v
+-----------------------------------+
| 设置为 SVC 模式, 关闭所有中断      |
+-----------------------------------+
               |
               v
+-----------------------------------+
| 获取 CPU ID, 提取相应的 proc_info |
+-----------------------------------+
               |
               v
+-----------------------------------+
| 验证 tags 或者 dtb                |
+-----------------------------------+
               |
               v
+-----------------------------------+
| 创建页表项                        |
+-----------------------------------+
               |
               v
+-----------------------------------+
| 配置 r13 寄存器, 设置跳转函数      |
+-----------------------------------+
               |
               v
+-----------------------------------+
| 使能 MMU (Memory Management Unit)  |
+-----------------------------------+
               |
               v
+-----------------------------------+
| 跳转到 start_kernel (第二阶段)   |
+-----------------------------------+

解释每一步:

  1. 入口:stext

    • 这是内核启动的第一个函数,ARM 架构的内核启动代码通常会在汇编文件中定义 ENTRY(stext) 作为入口点,CPU 会从此处开始执行内核的初始化代码。
  2. 设置为 SVC 模式,关闭所有中断

    • 在引导阶段,ARM 处理器默认是运行在 User 模式,但是为了执行特权操作,必须将处理器模式切换到 SVC 模式。同时,在此阶段关闭中断,避免在内核初始化期间中断干扰。
  3. 获取 CPU ID,提取相应的 proc_info

    • 通过获取 CPU 的标识符(通常是通过某种机制如 CPUID 指令),可以从设备树或者其他硬件资源中获取到与当前 CPU 相关的配置信息(如架构、特性等)。这些信息将用于后续的处理器配置。
  4. 验证 tags 或者 dtb

    • TagsDTB (Device Tree Blob) 是在引导过程中传递给内核的一些配置信息,内核需要验证这些信息的有效性。通过这些信息,内核可以识别硬件配置及设备信息。
  5. 创建页表项

    • 在内核启动时,需要设置虚拟内存管理(MMU)。此时,内核创建并初始化页表,确保各个虚拟地址正确映射到物理内存。
  6. 配置 r13 寄存器,设置跳转函数

    • 寄存器 r13 通常用于保存栈指针等关键信息。在此步骤中,内核会配置好栈指针,并设置跳转地址,以便在开启 MMU 后能够正确跳转到其他函数。
  7. 使能 MMU (Memory Management Unit)

    • 启用 MMU 以开启虚拟内存管理。通过 MMU,可以实现内存地址的映射,从而支持虚拟内存、进程隔离等功能。此时,通过设置相应的控制寄存器,开启 MMU。
  8. 跳转到 start_kernel (第二阶段)

    • 在完成上述所有硬件初始化后,内核会跳转到 start_kernel,这是内核的第二阶段,之后的任务会在此函数中完成,例如内核线程的创建、调度初始化等。


1.2、疑问

主要带着以下几个问题去理解

为什么要获取CPU ID和proc info?也就是说proc info存放了什么东西以至于有必要在第一阶段、打开MMU之前就去获取?
如何获取对应CPU的proc info?

1.3、对应代码实现

    __HEAD
ENTRY(stext)
    mrc p15, 0, r9, c0, c0      @ get processor id,用于获取CPU ID,具体参考第二节
    bl  __lookup_processor_type     @ r5=procinfo r9=cpuid,根据cpu id获取proc info,具体参考第一节和第三节。
    movs    r10, r5             @ invalid processor (r5=0)?,判断proc info是否存在
 THUMB( it  eq )        @ force fixup-able long branch encoding
    beq __error_p           @ yes, error 'p'

mrc p15, 0, r9, c0, c0 @ get processor id,用于获取CPU ID,
bl __lookup_processor_type @ r5=procinfo r9=cpuid,根据cpu id获取proc info。


二、procinfo

2.1、说明

获取 CPU ID 和提取相应的 proc_info(进程信息)通常涉及两个方面:

  1. 获取 CPU ID:即获取当前系统中 CPU 的标识符或者核编号。
  2. 提取 proc_info 说明:从 CPU 信息中提取进程相关信息,或者读取系统中与 CPU 相关的详细信息。

procinfo使用proc_info_list结构体,用来说明一个cpu的信息,包括这个cpu的ID号,对应的内核数据映射区的MMU标识等等。

2.2、数据结构定义


arch/arm/include/asm/procinfo.h

/*
 * Note!  struct processor is always defined if we're
 * using MULTI_CPU, otherwise this entry is unused,
 * but still exists.
 *
 * NOTE! The following structure is defined by assembly
 * language, NOT C code.  For more information, check:
 *  arch/arm/mm/proc-*.S and arch/arm/kernel/head.S
 */
struct proc_info_list {
	unsigned int		cpu_val;
	unsigned int		cpu_mask;
	unsigned long		__cpu_mm_mmu_flags;	/* used by head.S */
	unsigned long		__cpu_io_mmu_flags;	/* used by head.S */
	unsigned long		__cpu_flush;		/* used by head.S */
	const char		*arch_name;
	const char		*elf_name;
	unsigned int		elf_hwcap;
	const char		*cpu_name;
	struct processor	*proc;
	struct cpu_tlb_fns	*tlb;
	struct cpu_user_fns	*user;
	struct cpu_cache_fns	*cache;
};

重点关注的成员如下:
- cpu_val:cpu对应的硬件id号
- cpu_mask:cpu硬件id号的掩码
- __cpu_mm_mmu_flags:临时页表映射的内核空间的MMU标识
- __cpu_io_mmu_flags:IO映射区的MMU标识
- __cpu_flush:cpu setup函数的地址,后续在打开MMU过程时候会使用到

注意:
- 这里存在的MMU标识,也就是我们需要在打开MMU之前需要先获取procinfo的原因,因为打开MMU之前需要配置临时内核页表,而配置临时内核页表需要这里的MMU标识来进行设置。在后续创建临时内核页表的文章中会进行说明。
这里回答了“proc info存放了什么东西以至于有必要在第一阶段、打开MMU之前就去获取?”的疑问。

- cpu id和procinfo是一一对应的关系,所以可以通过cpu id来获取到对应的procinfo结构体。

2.3、存放位置


所有CPU的proc info都会被存放到.init.proc.info段中
arch/arm/kernel/vmlinux.lds.S
 

SECTIONS
{
        .init.proc.info : {
                ARM_CPU_DISCARD(PROC_INFO)
        }
}
#define PROC_INFO                                                       \
        . = ALIGN(4);                                                   \
        VMLINUX_SYMBOL(__proc_info_begin) = .;                          \
        *(.proc.info.init)                                              \   
        VMLINUX_SYMBOL(__proc_info_end) = .;

通过查看Systemp.map可以看到.init.proc.info段里面放了这些cpu的procinfo

8041a800 T __proc_info_begin
8041a800 t __v7_ca5mp_proc_info
8041a834 t __v7_ca9mp_proc_info
8041a868 t __v7_ca8_proc_info
8041a89c t __v7_cr7mp_proc_info
8041a8d0 t __v7_ca7mp_proc_info
8041a904 t __v7_ca12mp_proc_info
8041a938 t __v7_ca15mp_proc_info
8041a96c t __v7_b15mp_proc_info
8041a9a0 t __v7_ca17mp_proc_info
8041a9d4 t __krait_proc_info
8041aa08 t __v7_proc_info
8041aa3c T __proc_info_end

2.4、示例


arm体系是armv7,cortex-A8架构,对应procinfo定义于proc-v7.S中。
arch/arm/mm/proc-v7.S

    .section ".proc.info.init", #alloc
    /*
     * ARM Ltd. Cortex A8 processor.
     */
    .type    __v7_ca8_proc_info, #object
__v7_ca8_proc_info:
    .long    0x410fc080
    .long    0xff0ffff0
    __v7_proc __v7_ca8_proc_info, __v7_setup, proc_fns = ca8_processor_functions
    .size    __v7_ca8_proc_info, . - __v7_ca8_proc_info

通过.section “.proc.info.init”将后面的数据结构定义在了.proc.info.init段中,最终在连接过程中被连接到.init.proc.info段中,也就是__proc_info_begin和__proc_info_end之间的位置中。
__v7_proc是一个宏,其定义如下
 

.macro __v7_proc name, initfunc, mm_mmuflags = 0, io_mmuflags = 0, hwcaps = 0, proc_fns = v7_processor_functions
    ALT_SMP(.long    PMD_TYPE_SECT | PMD_SECT_AP_WRITE | PMD_SECT_AP_READ | \
            PMD_SECT_AF | PMD_FLAGS_SMP | \mm_mmuflags)
    ALT_UP(.long    PMD_TYPE_SECT | PMD_SECT_AP_WRITE | PMD_SECT_AP_READ | \
            PMD_SECT_AF | PMD_FLAGS_UP | \mm_mmuflags)
    .long    PMD_TYPE_SECT | PMD_SECT_AP_WRITE | \
        PMD_SECT_AP_READ | PMD_SECT_AF | \io_mmuflags
    initfn    \initfunc, \name
    .long    cpu_arch_name
    .long    cpu_elf_name
    .long    HWCAP_SWP | HWCAP_HALF | HWCAP_THUMB | HWCAP_FAST_MULT | \
        HWCAP_EDSP | HWCAP_TLS | \hwcaps
    .long    cpu_v7_name
    .long    \proc_fns
    .long    v7wbi_tlb_fns
    .long    v6_user_fns
    .long    v7_cache_fns
.endm

根据数据结构定义,__v7_ca8_proc_info对应如下结果
- cpu_val:0x410fc080
- cpu_mask:0xff0ffff0
- __cpu_mm_mmu_flags:PMD_TYPE_SECT | PMD_SECT_AP_WRITE | PMD_SECT_AP_READ | \
PMD_SECT_AF | PMD_FLAGS_UP | \mm_mmuflags
- __cpu_io_mmu_flags: PMD_TYPE_SECT | PMD_SECT_AP_WRITE | \
PMD_SECT_AP_READ | PMD_SECT_AF | \io_mmuflags
- __cpu_flush : __v7_setup


三、如何获取CPU ID

3.1、原理


arm体系上可支持最多16个协处理器。
arm体系将CPU ID(处理器标识符,主标识符)存放在协处理器cp15的c0寄存器中。

ARM 系统中的协处理器 (Co-Processor)

在 ARM 体系结构中,协处理器用于提供扩展功能,如内存管理、缓存控制、调试、系统控制等。ARM 支持最多 16 个协处理器(协处理器编号 0 到 15)。这些协处理器能够通过特定的指令与主处理器进行交互,通常这些指令以 MRC(从协处理器读取数据)和 MCR(向协处理器写入数据)的形式使用。

CP15 协处理器和 C0 寄存器

在 ARM 架构中,CP15 是一个特殊的协处理器,它包含多个寄存器,用于控制和访问与处理器相关的各种状态。特别地,C0 寄存器包含了处理器的标识符信息。

获取 CPU ID

CP15 协处理器的 C0 寄存器通常存储了 CPU 的相关信息,包括处理器的 ID。使用 ARM 汇编语言,可以通过 MRC 指令读取 CP15 协处理器的 C0 寄存器,以获取 CPU 标识符。

例子:读取 CPU ID

在 ARM 中,可以使用以下汇编指令来读取 CP15 协处理器的 C0 寄存器中的 CPU 标识符。

MRC p15, 0, r0, c0, c0, 0  ; 从协处理器 CP15 的 C0 寄存器读取 CPU ID
  • p15 表示协处理器编号为 15,通常是系统的协处理器。
  • r0 是一个通用寄存器,用于存放 C0 寄存器中的值。
  • c0, c0, 0 指示 CP15 协处理器的 C0 寄存器。

执行这个指令后,CPU 标识符将被加载到 r0 寄存器中,您可以进一步分析该值以获取 CPU 的标识信息。

进一步的 CPU 信息

除了 CPU ID,C0 寄存器还可能存储其他与处理器特性相关的信息(如 CPU 型号、版本等)。在一些 ARM 架构中,还可能存在其他协处理器寄存器,用于存储更多关于处理器的详细信息(如核心数、缓存大小等)。

例如,ARM 的 MIDR(Main ID Register)寄存器通常用于存储处理器的 ID 信息,您可以通过读取 MIDR 寄存器来获得处理器的 ID。

示例:读取 MIDR(处理器主标识寄存器)

MRC p15, 0, r0, c0, c0, 5  ; 从 CP15 协处理器的 C0 寄存器的第5位置读取 MIDR


3.2、协处理器cp15

3.2.1、CP15 协处理器的功能

CP15 协处理器主要用于以下功能:

  1. 内存管理:包括页表、MMU(内存管理单元)的配置等。
  2. 缓存管理:控制缓存的启用与禁用、缓存的清除和刷新等。
  3. 异常控制:管理中断和异常的处理机制。
  4. 系统控制:处理器和系统的控制寄存器,包括操作模式、频率等设置。
  5. 调试与性能监控:提供调试支持和性能计数器等功能。

3.2.2、CP15 协处理器的寄存器和操作

CP15 协处理器包含多个寄存器,允许系统开发人员对 ARM 处理器的多个重要功能进行配置和管理。常见的寄存器包括 C0C15,它们的功能各不相同,具体功能可以通过 ARM 体系结构文档来查看。下面是一些重要的寄存器和操作。

1. C0 寄存器(ID 寄存器)

C0 寄存器通常存储处理器的标识信息,如 CPU 的型号、版本、核心类型等。它可以帮助开发者识别当前处理器的具体特性。

  • 通过 MRC 指令读取:MRC p15, 0, r0, c0, c0, 0
    • p15:协处理器 15。
    • c0, c0, 0:读取 C0 寄存器。
2. C1 寄存器(Control Register

C1 寄存器是控制寄存器,用于启用或禁用一些系统级别的功能,如 MMU(内存管理单元)和缓存的控制。

  • 通过 MRC 指令读取:MRC p15, 0, r0, c1, c0, 0
  • 通过 MCR 指令写入:MCR p15, 0, r0, c1, c0, 0
3. C2 寄存器(Memory Management Control Register

C2 寄存器控制内存管理的一些功能,如页面表配置。

4. C7 寄存器(Cache Operations Register

C7 寄存器用于缓存的控制操作,例如缓存的清除和无效化操作。对于缓存管理,使用 MRCMCR 指令来执行缓存操作。

5. C13 寄存器(Performance Monitor

C13 寄存器与性能监控相关,可以用于性能计数器的配置,帮助开发者跟踪和分析处理器的执行效率。

CP15 常用操作

  1. 启用 MMU(内存管理单元)
MRC p15, 0, r0, c1, c0, 0  ; 读取控制寄存器 C1
ORR r0, r0, #0x1           ; 启用 MMU(设置 C1 寄存器的 bit0)
MCR p15, 0, r0, c1, c0, 0  ; 写回更新后的控制寄存器 C1
  1. 启用缓存

启用数据缓存:

MRC p15, 0, r0, c1, c0, 0  ; 读取控制寄存器 C1
ORR r0, r0, #0x4           ; 启用数据缓存
MCR p15, 0, r0, c1, c0, 0  ; 写回更新后的控制寄存器 C1
  1. 清除缓存
MCR p15, 0, r0, c7, c5, 0  ; 清除数据缓存
  1. 读取处理器 ID

读取 C0 寄存器(通常用于获取处理器 ID 和特性):

MRC p15, 0, r0, c0, c0, 0  ; 读取处理器 ID 信息

3.3、协处理器指令说明


(1)MCR 指令:ARM寄存器到协处理器寄存器的数据传送

   MCR{<cond>} <p>,<opcode_1>,<Rd>,<CRn>,<CRm>{,<opcode_2>}
   MCR{<cond>} p15,0,<Rd>,<CRn>,<CRm>{,<opcode_2>}

(2)MRC指令: 协处理器寄存器到ARM寄存器的数据传送

MRC{<cond>} <p>,<opcode_1>,<Rd>,<CRn>,<CRm>{,<opcode_2>}
MRC{<cond>} p15,0,<Rd>,<CRn>,<CRm>{,<opcode_2>}

  •     cond为指令执行的条件码。当忽略时指令为无条件执行。
  •     opcode_1为协处理器将执行的操作的操作码。对于CP15协处理器来说,< opcode_1>永远为0b000,当< opcode_1>不为0b000时,该指令操作结果不可预知。
  •     Rd作为源寄存器的ARM寄存器,其值将被传送到协处理器寄存器中。
  •     CRn作为目标寄存器的协处理器寄存器,其编号可能是C0,C1,…,C15。
  •     CRm和opcode_2两者组合决定对协处理器寄存器进行所需要的操作,如果没有指定,则将为为C0,opcode_2为0,否则可能导致不可预知的结果。

3.4、获取cpu id的指令


通过上述,通过mrc指令从p15中的c0寄存器中获取CPU ID,并且获取获取CPU ID的参数如下:
p->p15,
opcode_1->0
Rd->r9(我们要存放在r9寄存器其中)
CRn->c0
CRm->c0
最终获取cpu id的指令如下:

mrc    p15, 0, r9, c0, c0        @ get processor id

四、如何获取cpu对应的procinfo

4.1、原理

1.内核初始化时的 CPU 探测

内核初始化时会首先探测硬件,特别是 CPU 的数量、型号等信息。Linux 内核会通过 CPUID 指令(在 x86 架构上)或其他架构特有的机制来识别和初始化 CPU。内核会收集每个 CPU 的信息,并将这些信息存储到一个结构体中。

2.proc_info 结构体与 .init.proc.info

在内核启动期间,CPU 相关的所有信息通常存储在特定的内存区域,这些区域可以通过 .init.proc.info 来访问。该区域包含了内核在启动时构建的 CPU 信息数据。

proc_info 结构体可能会包含如下信息:

  • CPU 类型
  • CPU 核数
  • 支持的特性(SSE、AVX 等)
  • CPU 的频率和缓存信息
  • 核心 ID、物理核心数、线程数等

这些信息通常在内核启动时通过静态方式(例如在编译时)或者动态方式(通过 cpuid 和其他机制)收集并存储。

struct proc_info {
    int cpu_id;
    unsigned long cpu_val;
    /* 其他与 CPU 相关的信息 */
};
3.__proc_info_begin__proc_info_end

这两个符号通常定义在内核的某些初始化代码中,表示 proc_info 数据段的起始和结束位置。这段内存区域包含了 CPU 的信息,并且这些信息是在内核初始化阶段静态地设置的。

  • __proc_info_begin:表示 proc_info 数据段的起始位置。
  • __proc_info_end:表示 proc_info 数据段的结束位置。

这两个符号通常被定义为全局符号,编译器会将它们放置在适当的位置(例如 .init.proc.info 段中)。

4.通过 CPU ID 查找 proc_info

内核通过 CPU ID 来定位与特定 CPU 相关的 proc_info 结构体。在内核启动时,每个 CPU 的信息都会存储在 proc_info_list 中。proc_info_list 是一个链表或数组,存储了每个 CPU 的 proc_info 结构体。内核会根据 CPU 的 ID 从 proc_info_list 中找到对应的 CPU 信息。

在启动过程中,内核会依次遍历 __proc_info_begin__proc_info_end 之间的 proc_info_list。当内核获取到当前 CPU 的 ID 后,它会遍历这个区间内的所有 proc_info 结构体,找到匹配的 cpu_id,并返回对应的 proc_info

例如,内核可能会执行以下步骤:

void init_cpu_info(void) {
    int i;
    for (i = 0; i < num_cpus; i++) {
        // 获取当前CPU的ID
        int cpu_id = get_cpu_id(i);

        // 在proc_info_list中查找与CPU ID匹配的proc_info结构体
        for (proc_info *info = __proc_info_begin; info < __proc_info_end; info++) {
            if (info->cpu_id == cpu_id) {
                // 找到匹配的proc_info结构体
                process_cpu_info(info);
                break;
            }
        }
    }
}

在这个过程中,内核会检查每个 proc_info 结构体的 cpu_id 字段,并与当前的 CPU ID 进行比较。如果匹配,就找到了对应的 proc_info 结构体,从而获得该 CPU 的相关信息。

4.2、代码实现


- 首先将__proc_info_begin的区间信息存放在__lookup_processor_type_data位置上
arch/arm/kernel/head-common.S

/*
* Look in <asm/procinfo.h> for information about the __proc_info structure.
*/
    .align    2
    .type    __lookup_processor_type_data, %object
__lookup_processor_type_data:
    .long    .
    .long    __proc_info_begin
    .long    __proc_info_end
    .size    __lookup_processor_type_data, . - __lookup_processor_type_data

    在stext中调用__lookup_processor_type来获取cpu对应的proc info
    其中r9存放的是cpu id,r5存放的是获取到的proc info的地址

    __HEAD
ENTRY(stext)
    bl  __lookup_processor_type     @ r5=procinfo r9=cpuid

    __lookup_processor_type实现如下
    arch/arm/kernel/head-common.S

/*
* Read processor ID register (CP#15, CR0), and look up in the linker-built
 * supported processor list.  Note that we can't use the absolute addresses
* for the __proc_info lists since we aren't running with the MMU on
 * (and therefore, we are not in the correct address space).  We have to
* calculate the offset.
*
 *    r9 = cpuid
* Returns:
 *    r3, r4, r6 corrupted
 *    r5 = proc_info pointer in physical address space
 *    r9 = cpuid (preserved)
*/
__lookup_processor_type:
    adr    r3, __lookup_processor_type_data
    ldmia    r3, {r4 - r6}
    sub    r3, r3, r4            @ get offset between virt&phys
    add    r5, r5, r3            @ convert virt addresses to
    add    r6, r6, r3            @ physical address space
1:    ldmia    r5, {r3, r4}            @ value, mask
    and    r4, r4, r9            @ mask wanted bits
    teq    r3, r4
    beq    2f
    add    r5, r5, #PROC_INFO_SZ        @ sizeof(proc_info_list)
    cmp    r5, r6
    blo    1b
    mov    r5, #0                @ unknown processor
2:    ret    lr
ENDPROC(__lookup_processor_type)

解析如下:
(1)获取proc_info区间的连接地址

    adr    r3, __lookup_processor_type_data
    ldmia    r3, {r4 - r6}

通过上述
r3存放的是__lookup_processor_type_data的真实的物理地址,也就是在RAM上的位置
r4存放的是__lookup_processor_type_data的连接地址
r5存放的是__proc_info_begin的连接地址
r6存放的是__proc_info_end的连接地址
(2)计算出proc_info区间的内存地址

    sub    r3, r3, r4            @ get offset between virt&phys
    add    r5, r5, r3            @ convert virt addresses to
    add    r6, r6, r3            @ physical address space

因为此时MMU是没有打开的,所以并不能直接使用连接地址来访问对应区域,而需要计算出对应区域的内存地址。
首先计算出__lookup_processor_type_data的真实物理地址(r4)和连接地址(r3)的偏移,
然后根据偏移计算出__proc_info_begin(r5)和__proc_info_end(r6)的真实物理地址,也就是内存地址。
通过上述步骤之后
r5存放的是__proc_info_begin的物理地址,也就是内存地址
r6存放的是__proc_info_end的物理地址,也就是内存地址
可以直接r5和r6访问到proc info的区间。
(3)提取结构体信息并进行比较

1:    ldmia    r5, {r3, r4}            @ value, mask
    and    r4, r4, r9            @ mask wanted bits
    teq    r3, r4
    beq    2f

“ldmia r5, {r3, r4}”获取[__proc_info_begin-__proc_info_end]中第一个proc_info_list结构体的,因为cpu_val和cpu_mask被存放在proc_info_list结构体的前16个字节,所以可以直接这样获取。
r3上就存放了cpu_val,r4上存放了cpu_mask。
将cpu_mask(r4)和获得的cpuid(r9)进行掩码后和cpu_val(r3)进行比较,相等则返回退出,此时的r5上存放了对应的proc_info地址。否则进入下一个循环。
(4)获取下一个proc_info_list结构体,

    add    r5, r5, #PROC_INFO_SZ        @ sizeof(proc_info_list)
    cmp    r5, r6
    blo    1b
    mov    r5, #0                @ unknown processor

如果还没有到__proc_info_end(r6),则继续下一个循环。如果已经搜索到结尾,将r5设置为0(也就表示非法值)后直接退出。

通过上述步骤之后,就可以获取到了cpu id对应的proc_info_list结构体,并且其地址存放在r5寄存器中。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值