进程地址空间

The Process Address Space
第十二章,我们讨论了内存管理,内核是如何管理物理内存的。除了管理自己的内存之外,内核还需要管理用户空间进程使用的内存。This memory叫做进程地址空间,which is the representation of memory given to each user-space process on the system.Linux是虚拟内存的操作系统,因此内存资源是虚拟化的among the processes on the system.每个进程的内存都以为自己可以访问到系统的全部物理内存。更重要的是,甚至是单一进程的地址空间都比物理内存大。本章我们讨论内核是如何管理进程地址空间的。
15.1 Address Spaces
进程地址空间包含进程可寻址的虚拟内存和进程允许使用的虚拟内存地址。每个进程都拥有一个32位或者64位的地址空间,这个size的大小取决于处理器架构。A memory address in one process’s address space is completely unrelated to that same memory address in another process’s address space.Both processes can have different data at the same address in their respective address spaces.Alternatively,进程能够选择和其他进程共享地址空间。We know these processes as threads.
一个内存地址就是在地址空间的给出的某个值,比如说0x4021f000.这个特殊的值指明了32位处理器上面的一个特定的字节。虽然进程能够寻址到4G内存范围(32位处理器地址空间而言),但是他并没有权限去访问所有地址空间。地址空间有趣的部分是内存地址之间的区间范围,比如说0x08048000-0x0804c000,that进程有权限访问的。合法地址范围叫做内存区域。虽然在内核中,进程能够动态添加和删除内存区域到进程地址空间。
进程仅仅能够访问到有效内存区域的内存地址。内存区域有相关的权限,比如说可读、可写和可执行,that和进程拥有的权限。如果进程访问无效的内存区域的内存地址,或者it accesses a valid area in an invalid manner,内核会kill掉这个进程,并输出信息segmentation fault.
内存区域可能包含下列信息:
 代码段(text section): 可执行文件代码
 数据段(data section): 可执行文件的已初始化全局变量(静态分配的变量和全局变量)。
 bss段:程序中未初始化的全局变量,零页映射(页面的信息全部为0值)。
 进程用户空间栈的零页映射(进程的内核栈独立存在并由内核维护)
 每一个诸如C库或动态连接程序等共享库的代码段、数据段和bss也会被载入进程的地址空间
 任何内存映射文件
 任何共享内存段
 任何匿名的内存映射(比如由malloc()分配的内存)
这些信息都存在与本身进程的确定区域,不能够互相覆盖。
15.2 The Memory Descriptor
内核使用叫做内存描述的数据结构来表示进程的地址空间。该数据结构包含进程地址空间的所有信息。进程描述符通过struct mm_struct表示,定义在<linux/mm_type.h>中。Let’s look at the memory descriptor,with comments added describing each field:
struct mm_struct {
struct vm_area_struct mmap; / list of memory areas /
struct rb_root mm_rb; /
red-black tree of VMAs */
struct vm_area_struct mmap_cache; / last used memory area /
unsigned long free_area_cache; /
1st address space hole */
pgd_t pgd; / page global directory /
atomic_t mm_users; /
address space users /
atomic_t mm_count; /
primary usage counter /
int map_count; /
number of memory areas /
struct rw_semaphore mmap_sem; /
memory area semaphore /
spinlock_t page_table_lock; /
page table lock /
struct list_head mmlist; /
list of all mm_structs /
unsigned long start_code; /
start address of code /
unsigned long end_code; /
final address of code /
unsigned long start_data; /
start address of data /
unsigned long end_data; /
final address of data /
unsigned long start_brk; /
start address of heap /
unsigned long brk; /
final address of heap /
unsigned long start_stack; /
start address of stack /
unsigned long arg_start; /
start of arguments /
unsigned long arg_end; /
end of arguments /
unsigned long env_start; /
start of environment /
unsigned long env_end; /
end of environment /
unsigned long rss; /
pages allocated /
unsigned long total_vm; /
total number of pages /
unsigned long locked_vm; /
number of locked pages /
unsigned long saved_auxv[AT_VECTOR_SIZE]; /
saved auxv /
cpumask_t cpu_vm_mask; /
lazy TLB switch mask /
mm_context_t context; /
arch-specific data /
unsigned long flags; /
status flags /
int core_waiters; /
thread core dump waiters */
struct core_state core_state; / core dump support /
spinlock_t ioctx_lock; /
AIO I/O list lock /
struct hlist_head ioctx_list; /
AIO I/O list /
};
15.2.1 Allocating a Memory Descriptor
和指定任务相关的内存描述符存储在任务进程描述符的mm变量中(进程描述符定义为task_struct,在<linux/sched.h>)。因此,current->mm是当前进程的内存描述符。在fork()的过程中,函数copy_mm()拷贝父进程的内存描述符给子进程。Mm_struct结构体变量是通过函数allocate_mmm() in kernel/fork.c从mm_cachep slab cache分配而来。通常来说,每一个进程都会接收一个唯一的mm_struct和一个唯一的进程地址空间。
多个进程可能会选择共享他们的地址空间with their children by means of the CLONE_VM flag to clone(). 这样的process is called a thread.回顾第三章“进程管理”,这可能是普通进程和so-called thread in linux的主要区别吧;the Linux kernel does not otherwise differentiate between them. Threads are regular processes to the kernel that merely share the certain resources.
In the case that CLONE_VM is specified, allocate_mm() is not called, and the process’s mm field is set to point to the memory descriptor of its parent via this logic in copy_mm():
if (clone_flags & CLONE_VM) {
/

  • current is the parent process and
  • tsk is the child process during a fork()
    */
    atomic_inc(&current->mm->mm_users);
    tsk->mm = current->mm;
    }
    15.2.2 Destroying a Memory Descriptor
    When 进程 associated with a specific address space 退出的时候,函数exit_mm()会被调用。该函数会执行一些housekeeping and更新一些statistics. 然后调用mmput(),which减少内存描述符mm_users的计数。如果用户计数达到0的话,函数mmdrop()会被调用执行去减少mm_count计数。如果这个计数最终减到了0的话,函数free_mm()会被调用,将结构体mm_struct返回给slab cache via kmem_cache_free()函数,因为内存描述符没有使用用户了。
    15.2.3 The mm_struct and Kernel Threads
    内核线程没有进程地址空间,因此他们也没有相对应的内存描述符。Thus,内核线程进程描述符的mm field是NULL。这是内核线程的定义----没有用户context的进程。
    不存在地址空间is fine,因为这样内核线程是访问不到用户空间的内存。因为内核线程没有用户空间的pages,他们也不会摧毁自己的内存描述符和页表。Despite this,内核线程需要一些数据,比如说页表,甚至是访问内核内存。为了给内核线程提供这些需要的数据,不浪费内存在内存描述符和页表,不浪费处理器cycle去切换到一个新的地址空间whenever a kernel thread begins running, 内核线程使用了内存描述符of whatever task ran previously.
    无论进程什么时候被调度,进程mm field参考的进程地址空间都会被loaded。进程描述符中的active_mm field is then updated to refer to the new address space.内核线程没有地址空间,并且mm field是NULL的。因此,当内核线程被调度的时候,内核notice that mm是NULL 并keep s the previous process’s address space loaded.然后内核更新内核线程的进程描述符的active_mm field to refer to the previous previous process’s memory descriptor.在需要的时候,内核线程可以使用前一个进程的页表。因为内核线程不能访问用户空间的内存,他们只能使用内核内存空间获取到的信息,which is the same for all processes.
    15.3 Virtual Memory Areas
    结构体vm_area_struct代表了内存区域。定义在<linux/mm_types.h>。在Linux内核中,内存区域通常来说称为虚拟内存区域。
    vm_area_struct结构描述给定地址空间中连续间隔上的单个内存区域。内核视每一个内存区域为一个独立的内存object。每一个内存区域都拥有特定的属性,比如说权限和一系列的相关操作。在这种方式下,每一个VMA结构体都代表着不同类型的内存区域,比如说内存映射的文件或者进程用户空间的stack。这和VFS采用的面向对象的方法是类似的。如下是该结构体,并附有注释描述每一项:
    struct vm_area_struct {
    struct mm_struct vm_mm; / associated mm_struct /
    unsigned long vm_start; /
    VMA start, inclusive /
    unsigned long vm_end; /
    VMA end , exclusive */
    struct vm_area_struct vm_next; / list of VMA’s /
    pgprot_t vm_page_prot; /
    access permissions /
    unsigned long vm_flags; /
    flags /
    struct rb_node vm_rb; /
    VMA’s node in the tree /
    union { /
    links to address_space->i_mmap or i_mmap_nonlinear */
    struct {
    struct list_head list;
    void *parent;
    struct vm_area_struct head;
    } vm_set;
    struct prio_tree_node prio_tree_node;
    } shared;
    struct list_head anon_vma_node; /
    anon_vma entry */
    struct anon_vma anon_vma; / anonymous VMA object */
    struct vm_operations_struct vm_ops; / associated ops /
    unsigned long vm_pgoff; /
    offset within file */
    struct file vm_file; / mapped file, if any */
    void vm_private_data; / private data */
    };
    Recall that在进程地址空间中,每一个内存描述符都和独立的间隔相关。Field vm_start是这个间隔中间的初始(the lowest)地址,field vm_end是这个间隔中最后(highest)地址后的第一个字节。也就是说,vm_start是inclusive start,vm_end是exclusive end of the memory internal.因此,vm_end-vm_start就是这片内存区域的长度,which exists over the internal [vm_start, vm_end].相同地址空间不同内存区域的Internals是不能重叠的。
    The vm_mm field指向VMA相关的mm_struct。Note that每一个VMA对于mm_struct而言都是一一对应的,which it is associated.因此,即使两个不同的进程映射相同的文件到分别的地址空间,每一个进程都拥有唯一的vm_area_struct来辨别他们的内存区域。相反地,两个进程(共享地址空间)也共享所有的vm_area_struct结构体。
    15.3.1 VMA Flags
    The vm_flags field包含的bit flags,定义在<linux/mm.h>,指明了所在内存区域的page行为及提供信息。不像特定的物理page相关的权限一样,VMA flags包含的信息包括内存区域相关的each page,或者整个内存区域,and not specific individual pages。表格15.1指明了a listing of the possible vm_flags values.
    15.3.2 VMA Operations
    结构体中The vm_ops指向了给定内存区域的操作函数列表,which the kernel can invoke to manipulate the VMA.结构体vm_area_struct作为一个generic object代表着任意类型的内存区域,并且该操作函数列表描述了具体的在该object上操作的方法。
    The operations table is represented by struct vm_operations_struct and is defined in <linux/mm.h>:
    struct vm_operations_struct {
    void (*open) (struct vm_area_struct *);
    void (*close) (struct vm_area_struct *);
    int (*fault) (struct vm_area_struct *, struct vm_fault *);
    int (*page_mkwrite) (struct vm_area_struct *vma, struct vm_fault *vmf);
    int (*access) (struct vm_area_struct *, unsigned long ,
    void *, int, int);
    };
    下面对这些operations进行详细的描述:
     Void open(struct vm_area_struct *area)
    当指定的内存区域被添加到一片地址空间的时候,该函数会被调用
     Void close(struct vm_area_struct *area)
    当指定的内存区域从一片地址空间被移除的时候,该函数会被调用
     Int fault(struct vm_area_struct *area, struct vm_fault *vmf)
    当发生page fault handler(when a page that is not present in physical memory is accessed)发生的时候,该函数被调用
     Int page_mkwrite(struct vm_area_struct *area, struct vm_fault *vmf)
    当一片只读的内存区域被写入而发生page fault handler的时候,该函数被调用
     Int access(struct vm_area_struct *vma, unsigned long address, void *buf, int len, int write)
    当get_user_pages失败的时候,函数access_process_vm()调用它。
    15.3.3 Lists and Trees of Memory Areas
    As discussed,内存区域可以通过内存描述符中的mmap和mm_rb两种方式访问。这两种数据结构分别指向了内存描述符相关的所有内存区域。事实上,他们都包含了指向vm_area结构体的指针,仅仅是方式不同。
    第一field,mmap,以单向链表的方式将所有的内存区域对象连接起来。每个结构体vm_area_struct通过vm_next field将每个元素连接进表中。The areas通过降序地址的方式sorted。第一个内存区域是mmap指向的vm_area_struct。最后一个结构体指向NULL。
    第二field,mm_rb,通过rb-tree将所有的内存区域对象连接起来。Rb-tree的根节点是mm_rb.在该地址空间的每一个vm_area_struct结构体通过vm_rb连接到该tree。
    A red-black tree是一种平衡的二进制树。红黑树中的每个元素叫做node。初始node叫做树的根节点。大多数节点有两个children:a left child and a right child.对于任何节点而言,左边的元素的值都是小于右边的元素的值。Futhermore,根据两个原则,每个节点都被分配了一个颜色(红或者黑,因此得名红黑树):红色节点的children是黑色,every path through the tree from a node to a leaf must contain the same number of black nodes.根节点永远是红色的。搜索,插入和删除的操作,在树上,都是log(n)的 复杂度的操作。
    链表通常用于需要遍历每一个元素的时候。红黑树通常用于locating a specific memory area in the address space。In the manner,内核提供了redunbant数据结构提供optimal的性能,无论内存区域中的操作办法。
    15.3.4 Memory Areas in Real Life
    Let’s look at a particular process’s address space and the memory areas inside.该任务使用了/proc文件系统和pmap(1)属性。该例子是一个简单的用户空间的程序,which does nothing of the value,except act as an example:
    Int main(int argc, char *argv[])
    {
    Return 0;
    }
    Take note of a few of the memory areas in this process’s address space.首先,你知道这里有text段、数据段和bss段。假设进程是动态链接进C库的,these three memory areas also exist for libc.so and again for ld.so.Finally,there is also the process’s stack.
    来自/proc//maps的输出列出了进程空间的内存区域:

上述的数据展现形式是:
Start-end permission offset major:minor inode file
The pmap(1) utility以可读的形式展现了这些信息:

头三行是代码段、数据段和BSS段of the libc.so, the C library.接下来的两行是我们的可执行文件的代码段和数据段。家下来的三行是ld.so, the dynamic linker,的代码段、数据段和BSS段。最后一行是进程的stack。
Note how the text section是全部可读和可执行的,which is what you expect for object code.换句话说,the data section and bss (which both contain global variables) are marked readable and writable, but not executable. 堆栈自然是可读的、可写的和可执行的,否则用处不大。
整个地址空间占用了1340KB,但是仅40KB是可写入和private的。如果一个内存区域是共享的,不可写入的,内核仅仅保存一份copy of the backing file in the memory.这通常来说是shared mappings的common sense,但是不可写入的特性can come as a bit of a surprise.如果你认为不可写入的映射是不可能改变的(或者说该映射是只读的形式),把镜像加载进内存一次也是安全的。因此,C库需要占用1212KB的物理内存and 不是进程数量的多倍。因为该进程可以访问1340KB的数据和代码,但是仅消耗40KB的物理内存,通过这种共享的方式而节省的空间是很有效的。
Note the memory areas without a mappd file on device 00:00 and inode zero. This is the zero page, which is a mapping that consists of all zeros. By mapping the zero page over a writable memory area, the area is in effect “initialized” to all zeros. This is important in that it provides a zeroed memory area, which is expected by the bss. Because the mapping is not shared, as soon as the process writes to this data, a copy is made and the value updated from zero.
和进程相关的每一个内存区域都对应着一个vm_area_struct结构体。因为进程不是线程,它拥有一个独立的mm_struct结构体referenced from its task_struct.
15.4 Manipulating Memory Areas
内核经常会操作到一片内存区域,比如说指定的地址是否在VMA上存在。这些操作是频繁的并形成了mmap() routine的基础,which is covered in the next section.定义了一些列的函数来辅助这些工作。
These functions are declared in <linux/mm.h>.
15.4.1 find_vma()
内核提供了函数,find_vma(),来查找给定内存地址的VMA。该函数定义在mm/mmap.c:
Struct vm_area_struct * find_vma(struct mm_struct *mm, unsigned long addr);
该函数在给定的地址空间查找第一个vm_end大于addr参数的内存区域。换句话说,该函数查找第一片包含addr或者起始地址大于addr的内存区域。如果这样的内存区域不存在的话,该函数会返回NULL.否则的话,返回指向vm_area_struct的指针。由于返回的VMA的起始地址大于addr,the given address does not necessarily lie inside the returned VMA. The result of the find_vma() is cached in the mmap_cache field of the memory descriptor. Because of the probability of an operation on one VMA being followed by more operations on that same VMA, the cached results have a decent hit rate(about 30-40% percent in practice). Checking the cached result is quick.如果给定的地址不在cache中,你必须搜索跟该内存描述符相关的内存区域来查找match。This is done via the red-black tree.
Mmap_cache的初始化检查来测试cached的VMA是否包含给定的地址。Note that简单的检查VMA的vm_end大于addr是不能确定这是第一个VMA,大于addr的。因此,for the cache to be useful here, the given addr must lie in the VMA—thankfully, this is just the sort of scenario in which consecutive operations on the same VMA would occur.
如果该cache在指定的VMA中不存在,该函数就必须搜索red-black tree。如果当前VM的vm_end大于addr,该函数遵循左边child;否则的话,它遵循right child. The function terminates as soon as a VMA is found that contains addr.如果这样的VMA没有被发现的话,该函数会继续遍历该树,并返回第一个VMA that it found that starts after addr.如果没有VMA发现的话,返回NULL.
15.4.2 find_vma_prev()
函数find_vma_prev()和find_vma()工作方式类似,但是它返回的是the last VMA before addr.该函数也定义在mm/mmap.c, and declared in <linux/mm.h>.
Struct vm_area_struct *find_vma_prev(struct mm_struct *mm, unsigned long addr,
Struct vm_area_struct **pprev)
变量pprev存储了指向the VMA preceding addr的指针。
15.4.3 find_vma_intersection()
函数find_vma_intersection()返回第一个VMA that overlaps a given address interval.函数定义在<linux/mm.h>,because it is in line:
static inline struct vm_area_struct *
find_vma_intersection(struct mm_struct *mm,
unsigned long start_addr,
unsigned long end_addr)
{
struct vm_area_struct *vma;
vma = find_vma(mm, start_addr);
if (vma && end_addr <= vma->vm_start)
vma = NULL;
return vma;
}
第一项参数是要搜查的地址空间,start_addr是the start of the interval, and end_addr是the end of the interval.
Obviously,如果find_vma()返回NULL,find_vma_intersection()也会返回NULL.但是,如果find_vma()返回有效的VMA, find_vma_intersection()会返回相同的VMA if it does not start after the end of the given address range. 如果the returned memory area does start after the end of the given address range,该函数返回NULL.
15.5 mmap() and do_mmap(): Creating an Address Interval
函数do_mmap()是内核用来创建新的线性地址空间间隔的。Saying that该函数创建一个新的VMA在技术上说是不正确的,因为如果创建的地址空间间隔和已经存在的地址间隔是相邻的话,and如果他们共享相同的权限,这两个间隔会被合并。如果这是不可能的话,一个新的VMA就被创建了。In any case, do_mmap()是用来添加地址建个到一个进程的地址空间—whether that means expanding an existing memory area or creating a new one.
函数do_mmap() is declared in <linux/mm.h>:
Unsigned long do_mmap(struct file * file, unsigned long addr,
Unsigned long len, unsigned long prot,
Unsigned long flag, unsigned long offset)
该函数maps the file specified by file at offset offset for length len.参数file可以使NULL的,offset可以使zero,in which case the mapping will not be bcaked by a file. In that case, this is called an anonymous mapping. 如果a file and offset are provided, the mapping is called a file-backed mapping.
参数addr指明了初始化地址,从何处开始搜索free的间隔。
参数prot指明了该内存区域的pages权限。可能的权限flags定义在<asm/mman.h>并且每个支持的架构都是不一致的,although in practice each architecture defines the flags listed in Table 15.2.
参数flags指明了对应的remaining VMA flags的flag。这些flags指明了type和改变了mapping的行为。They are also defined in <asm/mman.h>. See Table 15.3.
如果上述提到德参数都是无效的,do_mmap()函数会返回一个负值。否则的话,在虚拟内存中的一段合适的间隔会被返回来。如果可能的话,这个间隔会和相邻的内存区域合并。否则的话,一个新的vm_area_struct结构体会从cm_area_cachep slab cache中分配而来,and新的内存区域被添加到地址空间的链表,通过vma_link()添加到内存区域的红黑树中。Next,内存描述符中的total_vm field被更新。Finally,函数返回新创建的地址间隔的初始地址。
函数do_mmap()被导出到用户空间 via mmap()系统调用。系统调用mmap() is defined as:
Void * mmap(void *start,
Size_t length,
Int prot,
Int flags,
Int fd,
Off_t pgoff)
该系统调用命名为mmap2(),因为it is the second variant of mmap(). The original mmap() took an offset in bytes as the last parameter; the current mmap2() receives the offset in pages. This enables larger files with larger offsets to be mapped. The original mmap(), as specified by POSIX, is available from the C library as mmap(), but is no longer implemented in the kernel proper, whereas the new version is available as mmap2(). Both library calls use the mmap() system call, with the original mmap() converting the offset from bytes to pages.
15.6 munmap() and do_munmap():Removing an Address Interval
函数do_munmap()从指定的进程地址空间移除地址间隔。该函数declared in <linux/mm.h>:
Int do_munmap(struct mm_struct *mm, unsigned long start, size_t len)
第一项参数指明了地址空间from which the interval starting at address start of the length len bytes is removed. On success, zero is returned. Otherwise, a negative error code is returned.
系统调用munmap()被导出到用户空间as a means to enable processes to remove address intervals from their address space: it is the complement of the mmap() system call:
Int munmap(void *start, size_t length)
系统调用is defined in mm/mmap.c and act as a simple wrapper to do_munmap()。
15.7 Page Tables
Although应用操作映射到物理内存的虚拟内存,处理器直接操作物理地址空间。因此,当应用访问虚拟地址空间的时候,在处理器处理这个请求之前,它必须先转换为物理地址空间。执行这个操作的就是页表。页表的工作是把虚拟内存分成多个块。每一个块都有属于自己的index在表中。The table points to another table or the associated physical page.
在Linux中,页表包含三级。这种多级关系使得在64位机器上面enables a sparsely populated address space.如果页表在32位机器上面也仅通过一个单一的static数组来实现的话,那它的数值会变得非常大。Linux在某些硬件上不支持三级关系的架构中也是用三级页表(比如,某些硬件仅仅使用两级关系或者通过hash来实现)。使用三级关系是一种“greatest common denominator”—使用简化的实现方法可以简化Linux内核页表。
顶级的页表叫做PGD(page global directory),which包含了an array of pgd_t types.在大多数架构中,the pgd_t变量是一个 unsigned long.在PGD目录中,该级目录指向下一级目录PMD。
第二季目录是PMD(page middle directory),which是an array of pmd_t types.该级目录指向下一级目录PTE。
The final level称为简化页表,包含着pte_t类型的页表目录。该级页表指向物理pages。
在大多数架构中,页表的查找是通过硬件来实现的。In normal operations,硬件能够负责页表的使用。但是内核去建立这层关系。Figure-15.1显示了使用页表来查找物理地址的流程。

每个进程都拥有自己的页表(threads share them, of course).内存描述符中的pgd field指向进程的page global table.操作和遍历页表,要求page_table_lock,which is located inside the associated memory descriptor.
Page table data structures are quite architecture-dependent and thus are defined in <asm/page.h>.
由于每次在虚拟内存中访问到page都需要对应到相应的物理内存,所以说页表的性能表现就变得非常重要了。不幸的是,在内存中查找所有的地址仅能做到这么快了。为了优化它,大多数处理器都完成了TLB(translation lookaside buffer)模块,which is act as硬件cache用来存储虚拟地址和物理地址之间的映射。当访问到虚拟地址的时候,处理器首先检查该映射是否cached在TLB中。如果是命中的,物理地址就立刻返回。否则的话,如果Miss了,该页表 are consulted for the corresponding physical address.
Nonetheless,页表的管理是内核中极其重要的部分。内核2.6中对此区域的更改包括将页表的部分内容从高内存中分配出去。将来还有可能引入copy-on-write机制到页表中。In that scheme,页表将会在父进程和子进程之间共享across fork()。当父进程或子进程试图修改特定的页表目录的时候,a copy would be created, and the two processes would no longer share the entry.共享页表将会降低fork()中拷贝页表的开销。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值