操作系统心得-内存管理(持续更新中)

本文是笔者参考许多网站做的一些笔记心得来加深自己对操作系统的理解,非常感谢小林的网站给我一个整体的框架,我同时汇总一些相关的问题,方便日后查阅。如果有任何不清楚的地方,欢迎在评论区评论,笔者持续更新。

本文参考:

小林coding的网站
Linux内存管理
操作系统页面置换算法
Linux内核内存回收逻辑和算法(LRU)
操作系统高频面试题

基础知识

虚拟内存

操作系统会提供一种机制,将不同进程的虚拟地址和不同内存的物理地址映射起来,让每个进程都有互不干涉的虚拟地址空间(32位系统中每个进程操作4G虚拟内存),这里需要将「多进程」的思想理解起来。如果程序要访问虚拟地址的时候,由操作系统转换成不同的物理地址,这里操作系统帮我们做好了转换工作。

进程持有的虚拟地址会通过 CPU 芯片中的内存管理单元(MMU)的映射关系,来转换变成物理地址,然后再通过物理地址访问物理内存(也就是买的电脑标明的那种内存)。
在这里插入图片描述

MMU管理虚拟地址与物理地址之间的关系有两种方式:内存分段和内存分页。

内存分段

分段内存管理当中,地址是二维的,一维是段号,二维是段内地址;其中每个段的长度是不一样的,而且每个段内部都是从0开始编址的。由于分段管理中,每个段内部是连续内存分配,但是段和段之间是离散分配的,因此也存在一个逻辑地址到物理地址的映射关系,相应的就是段表机制。

段表机制下,可以通过虚拟地址的段选择因子,在段表内找到对应的物理内存段基地址,通过加上段内偏移量找到真正的物理内存地址,如下图所示。

在这里插入图片描述

分段的不足之处
  • 内存碎片的问题。
  • 内存交换的效率低的问题。

内存分段管理值得关注的问题是内存碎片的产生,这里我们关注三个对象,内部内存碎片(可以理解为分配了物理空间但没用完的),外部内存碎片(本来使用的但是现在不用的物理内存),磁盘。内存分段管理可以做到段根据实际需求分配,因此不会出现内部内存碎片。但是对应到物理内存上,可能会出现程序释放的可能性,留下了一段空闲内存,但是这段空闲内存又不足以容纳新的程序,从而产生一系列的外部内存碎片,如下图所示。

解决「外部内存碎片」的方式是内存交换,即内存和磁盘的swap。如下图所示,我们可以先把音乐所占的256M内存写出到外界磁盘中,然后重新装载进来拼接到游戏的512M内存后面,这样子就能腾出空闲的物理内存。在 Linux 系统里,也就是我们常看到的 「Swap 空间」,这块空间是从硬盘划分出来的,用于物理内存与硬盘的空间交换。由于内存和磁盘的访问速度过慢了,因此内存交换的效率也不高。
在这里插入图片描述

内存分页

分页是把整个虚拟空间按顺序划分成一个个固定尺寸大小的块,叫做,大小一般为4KB,物理空间同样按顺序划分成同等大小的块,叫做页框。虚拟地址到物理地址的转换是,MMU通过页表,先找到虚拟地址的页号对应的物理页号,然后加上偏移量,得到对应的物理地址。

分页地址由于分配单位是页,因此即使程序不满一页仍然会被分到一页,因此产生内部内存碎片。如果内存不够,分页也可以效率很高地从磁盘换入或者换出磁盘。更进一步地,每个进程创建加载时,内核只是为进程“创建”了虚拟内存的布局,具体就是初始化进程控制表中内存相关的链表,实际上并不立即就把虚拟内存对应位置的程序数据和代码拷贝到物理内存中,只是建立好了虚拟内存和物理内存的映射。等到运行程序需要用到的物理页在通过虚拟地址找不到,这可以从页表的标识位判断出来,就会产生「缺页异常」,将硬盘中的页换入。
在这里插入图片描述

分页是怎么解决分段的「外部内存碎片和内存交换效率低」的问题?

内存分页由于内存空间都是预先划分好的,也就不会像内存分段一样,在段与段之间会产生间隙非常小的内存,这正是分段会产生外部内存碎片的原因。而采用了分页,页与页之间是紧密排列的,所以不会有外部碎片。

但是,因为内存分页机制分配内存的最小单位是一页,即使程序不足一页大小,我们最少只能分配一个页,所以页内会出现内存浪费,所以针对内存分页机制会有内部内存碎片的现象。

如果内存空间不够,操作系统会把其他正在运行的进程中的「最近没被使用」的内存页面给释放掉,也就是暂时写在硬盘上,称为换出(Swap Out)。一旦需要的时候,再加载进来,称为换入(Swap In)。所以,一次性写入磁盘的也只有少数的一个页或者几个页,不会花太多时间,内存交换的效率就相对比较高。

更进一步地,分页的方式使得我们在加载程序的时候,不再需要一次性都把程序加载到物理内存中。我们完全可以在进行虚拟内存和物理内存的页之间的映射之后,并不真的把页加载到物理内存里,而是只有在程序运行中,需要用到对应虚拟内存页里面的指令和数据时,再加载到物理内存里面去。

分页机制下,虚拟地址和物理地址是如何映射的?

在分页机制下,虚拟地址分为两部分,页号和页内偏移。页号作为页表的索引,页表包含物理页每页所在物理内存的基地址,这个基地址与页内偏移的组合就形成了物理内存地址,见下图。
在这里插入图片描述
总结一下,对于一个内存地址转换,其实就是这样三个步骤:

  1. 把虚拟内存地址,切分成页号和偏移量;
  2. 根据页号,从页表里面,查询对应的物理页号;
  3. 直接拿物理页号,加上前面的偏移量,就得到了物理内存地址。
分页的空间缺陷

在 32 位的环境下,计算机可以操作的虚拟地址空间共有 4GB,假设一个页的大小是 4KB(212),那么就需要大约 100 万 (220) 个页,每个「页表项」需要 4 个Byte大小来存储,那么整个 4GB 空间的映射就需要有 4MB 的内存来存储页表。

在多进程的情况下,每个进程都有自己的虚拟地址空间映射关系,就是都有自己的页表,100 个进程的话,就需要 400MB 的内存来存储页表,这是非常大的内存了,更别说 64 位的环境了。

多级页表

一个页的大小是 4KB(212),每个「页表项」需要 4 个Byte大小来存储,那么一个页能存储1024个页表项(210),我们这里用1024(210)个表项作为一级页表,每个表项可以查找1024(210)个二级页表。如此一来,一级页表就可以覆盖整个 4GB 虚拟地址空间,我们的进程也可以通过一级页表操作整个虚拟空间,非多级页表的页表项是1对1,多级页表的一级页表项是1对1024。页表在访问时,本质上都是找到页表起始地址+在该地址上的偏移量,多级页表就需要多次转换。

由于程序的「局部性原理」,大多数程序并不需要使用到所有4GB虚拟空间,一级页表项很多都是空的,根本没有分配。如果某个一级页表的页表项没有被用到,也就不需要创建这个页表项对应的二级页表了,即可以在需要时才创建二级页表。假设只有 20% 的一级页表项被用到了,那么页表占用的内存空间就只有 4KB(一级页表,全部都要涵盖) + 20% * 4MB(二级页表)= 0.804MB,是一笔巨大的节约。

把二级页表推广到多级页表也是如此,都是归功于「局部性原理」。
在这里插入图片描述

快表

**多级页表解决了空间上的问题,但是却多了几道转换步骤,带来了时间上的开销。**程序具有「局部性」,频繁访问的程序段局限在某个存储区域上,于是可以在CPU芯片中加入一个专门存放程序最常访问的页表项的 Cache,它就叫快表(TLB)。有了 TLB 后,那么 CPU 在寻址时,会先查 TLB,直接一步找到对应的物理内存地址,如果没找到,才会继续查常规的页表。

在这里插入图片描述

段页式内存管理

内存分段和内存分页并不是对立的,它们是可以组合起来在同一个系统中使用的,那么组合起来后,通常称为段页式内存管理。

段页式内存管理实现的方式:

  • 先将程序划分为多个有逻辑意义的段,也就是前面提到的分段机制;
  • 接着再把每个段划分为多个页,也就是对分段划分出来的连续空间,再划分固定大小的页;
    这样,地址结构就由段号、段内页号和页内位移三部分组成。
    在这里插入图片描述
    用于段页式地址变换的数据结构是每一个程序一张段表,每个段又建立一张页表,段表中的地址是页表的起始地址,而页表中的地址则为某页的物理页号。

段页式地址变换中要得到物理地址须经过三次内存访问:

  1. 第一次访问段表,得到页表起始地址;
  2. 第二次访问页表,得到物理页号;
  3. 第三次将物理页号与页内位移组合,得到物理地址。
Linux内存布局

Intel 处理器的发展历史

早期 Intel 的处理器从 80286 开始使用的是段式内存管理。但是很快发现,光有段式内存管理而没有页式内存管理是不够的,这会使它的 X86 系列会失去市场的竞争力。因此,在不久以后的 80386 中就实现了页式内存管理。也就是说,80386 除了完成并完善从 80286 开始的段式内存管理的同时还实现了页式内存管理。

但是这个 80386 的页式内存管理设计时,没有绕开段式内存管理,而是建立在段式内存管理的基础上,这就意味着,页式内存管理的作用是在由段式内存管理所映射而成的地址上再加上一层地址映射。

由于此时由段式内存管理映射而成的地址不再是“物理地址”了,Intel 就称之为“线性地址”(也称虚拟地址)。于是,段式内存管理先将逻辑地址映射成线性地址,然后再由页式内存管理将线性地址映射成物理地址。

在这里插入图片描述

这里说明下逻辑地址和线性地址:

  • 程序所使用的地址,通常是没被段式内存管理映射的地址,称为逻辑地址;
  • 通过段式内存管理映射的地址,称为线性地址,也叫虚拟地址;

逻辑地址是「段式内存管理」转换前的地址,线性地址则是「页式内存管理」转换前的地址。

逻辑地址的形式是这样的:「段基址 :偏移值」 ,并不是一个具体的值,而是一个包含段基址和偏移值的对数。

将段基址(映射部分段表找到段号对应的页表起始地址)与偏移值(未映射部分段内页号+页内位移)相加,就得到线性地址(虚拟地址)了。再交由 CPU 的 MMU 将线性地址映射为物理地址(页表起始地址+段内页号得到物理页号+页内偏移量),然后就可以交给地址总线去进行读写了。

Linux 采用了什么方式管理内存?

Linux内存采用的是「段页式的虚拟内存技术」,这主要是上面 Intel 处理器发展历史导致的,因为 Intel X86 CPU 一律对程序中使用的地址先进行段式映射,然后才能进行页式映射。既然 CPU 的硬件结构是这样,Linux 内核也只好服从 Intel 的选择。

Linux 系统中的每个段都是从 0 地址开始的整个 4GB 虚拟空间(32 位环境下),也就是所有的段的起始地址都是一样的(前面的段表映射的页表起始地址都是一样的)。这意味着,Linux 系统中的代码,包括操作系统本身的代码和应用程序代码,所面对的地址空间都是线性地址空间(虚拟地址),这种做法相当于屏蔽了处理器中的逻辑地址概念,段只被用于访问控制和内存保护。

页是信息的物理单位,实现离散化分配,提高了内存的使用率。段则是信息的逻辑单位,满足用户管理一段相对完整的信息,提供访问控制和内存保护。段机制是Linux在「组织虚内存」,而页机制则是在「实现虚拟内存」。

在实现虚拟内存的过程中,当进程要求运行的时,不是将他的全部信息装入内存,而是将其一部分先装入物理内存,另一部分暂时留在磁盘中,进程在运行过程中,要使用信息不在物理内存时,发生中断,由操作系统将他们调入物理内存,以保证进程的正常运行。但是进程仍然会认为自己已经将程序完全调入进来了,拥有连续可用的内存。
在这里插入图片描述

Linux页面置换算法

常见的页面置换算法可以看这篇文章操作系统页面置换算法,将操作系统基本的页面置换算法进行了解释。

Linux采用的页面置换算法是一种改进地LRU算法–最近最少使用(LRU)页面的衰老算法,详细的实现可以参考这篇文章Linux内核内存回收逻辑和算法(LRU),Linux操作系统对 LRU 的实现主要是基于一对双向链表:active 链表inactive 链表,同时引入了两个页面标志符 PG_activePG_referenced 用于标识页面的活跃程度,PG_active 用于表示页面当前是否是活跃的,如果该位被置位,则表示该页面是活跃的。PG_referenced 用于表示页面最近是否被访问过,每次页面被访问,该位都会被置位。那些最近最少使用的页面会被逐个放到 inactive 链表的尾部。进行页面回收的时候,Linux 操作系统会从 inactive 链表的尾部开始进行回收。
在这里插入图片描述

进程的虚拟内存分布

在 Linux 操作系统中,虚拟地址空间的内部又被分为内核空间和用户空间两部分,不同位数的系统,地址空间的范围也不同。比如最常见的 32 位和 64 位系统,如下所示:
在这里插入图片描述
通过这里可以看出:

  • 32 位系统的内核空间占用 1G,位于最高处,剩下的 3G 是用户空间;
  • 64 位系统的内核空间和用户空间都是 128T,分别占据整个内存空间的最高和最低处,剩下的中间部分是未定义的。

虽然每个进程都各自有独立的虚拟内存,但是每个虚拟内存中的内核地址,其实关联的都是相同的物理内存。这样,进程切换到内核态后,就可以很方便地访问内核空间内存,它能够控制计算机的硬件资源,例如协调CPU资源,分配内存资源。

内核空间是由内核负责映射,不会跟着进程变化;内核空间地址有自己对应的页表,用户进程各自有不同的页表。
在这里插入图片描述

进程的内核态虚拟内存分布

在这里插入图片描述
固定映射区(Fixing Mapping Region):该区域和4G的顶端只有4k的隔离带,其每个地址项都服务于特定的用途,如ACPI_BASE等。

永久内存映射区(PKMap Region):该区域可访问高端内存。访问方法是使用alloc_page(_GFP_HIGHMEM)分配高端内存页或者使用kmap函数将分配到的高端内存映射到该区域。

动态内存映射区(Vmalloc Region):该区域由内核函数vmalloc来分配,特点是:虚拟空间连续,但是对应的物理空间不一定连续。vmalloc分配的虚拟地址所对应的物理页可能处于低端内存,也可能处于高端内存。

直接映射区(Direct Memory Region):虚拟空间中从3G开始往上最大896M的区间,为直接内存映射区,该区域的虚拟地址和物理地址存在线性转换关系:虚拟地址=3G+物理地址。

进程的用户态虚拟地址分布

在这里插入图片描述
虚拟地址从低到高对应:

  • 程序文件段(Text区域):代码段是用来存放可执行文件的操作指令,也就是说是它是可执行程序在内存中的镜像。代码段需要防止在运行时被非法修改,所以是不可写的。
  • 已初始化数据段(Data区域):数据段用来存放已初始化的全局变量,换句话说就是存放程序静态分配的变量和全局变量。Example: static char *pszUserName=“jiangxuzhao”
  • 未初始化数据段(BSS区域):包含了未初始化的全局变量,在内存中bss段全部置零。Example: static char *pszUserName
  • 堆:堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张,从低地址开始向上增长);当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)。
  • mmap 内存映射区:用于文件映射(包括动态库)和匿名映射。常见的就是使用 mmap 分配的虚拟内存区域。
  • 栈:栈是用户存放程序临时创建的局部变量,也就是说我们函数括弧“{}”中定义的变量(但不包括static声明的变量,static意味着在数据段中存放变量)。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。栈的大小是固定的,一般是 8 MB。当然系统也提供了参数,以便我们自定义大小;

一张图总结进程的虚拟内存分布:
在这里插入图片描述

进程实现虚拟内存管理

进程内存管理的对象是进程使用的虚拟内存区域(virtual memory areas,即VMA)。

相关数据结构之间的关系:
在这里插入图片描述
Linux内核通过一个被称为进程描述符的task_struct 结构体来管理进程,这个结构体包含了一个进程所需的所有信息。task_struct中有一个结构体被称为内存描述符的mm_struct,描述了一个进程的整个虚拟地址空间。每个进程正是因为都有自己的mm_struct,才使得每个进程都有自己独立的虚拟的地址空间。mm_struct中的pgd域为页表的指针,就是前面讲的一级页表。

每一段已经分配的虚拟内存区域都会以一个vm_area_struct结构体表示,而组织这些结构一共有两种形式:其中mm_struct->mm_rb是所有vm_area_struct组成的红黑树,而mm_struct->mmap是所有vm_area_struct组成的链表,vm_area_struct这个数据结构被双重管理主要是为了加速查找速度(空间换时间)。

mm_struct中的mmap指针指向的vm_area_struct链表的每一个节点就代表进程的一段虚拟地址空间,即一段VMA。一段VMA最终可能对应ELF可执行程序的数据段、代码段、堆、栈、或者动态链接库的某个部分。可以理解成进程管理着很多逻辑分段,和前面「Linux段页式虚拟内存技术」一致。

mm_struct内存描述符基本字段:
在这里插入图片描述
vm_area_structs主要字段:
在这里插入图片描述

Linux实现物理内存管理

先看看硬件层面上的UMA和非UMA:

  • NUMA计算机(non-uniform memeory access):
    每个CPU拥有各自的本地内存,这样每个CPU都能以较快的速度访问本地的内存,各个CPU通过总线链接起来,也可以访问其它CPU的内存,不过速度会慢些。
    在这里插入图片描述
  • UMA计算机(uniform memory access)
    无论多处理器还是单处理器都只能访问一整块内存。
    在这里插入图片描述
    linux内核需要用一种体系结构无关的方式来表示内存。于是Linux在逻辑上将物理内存划分为三级结构,分别使用pg_data_t,zone和page这三种数据结构分别描述节点、区、物理页框。
    在这里插入图片描述
  • 内存节点(pg_data_t)
    NUMA计算机中每个CPU的物理内存称为一个内存节点,通过pg_data_t数据结构描述,系统内所有节点形成一个双链表
    UMA模型下的物理内存只对应一个内存节点,整个物理内存形成一个节点,因此上述节点链表只有一个元素
  • 内存管理区(zone)
    各个内存节点划分为若干个区,也是对物理内存的进一步划分。物理内存通过下面几个宏来标记不同的区:
    ZONE_DMA:标记适合DMA范围的物理内存区
    ZONE_NORMAL:可以直接线性映射到内核空间的物理内存区(0~896M的物理内存区)
    ZONE_HIGHMEM:超出内核空间大小的物理内存区(即高于896M的物理内存区)
  • 页框(page)
    内核使用page结构体来描述一个物理页框,该结构也称为页描述符。
    页框代表物理内存的最小单位,每个物理页框都关联这样一个结构体,所有的页描述符都存放在mem_map数组中

在每个内存管理区(Zone)内,页框由伙伴系统Buddy来分配,伙伴算法负责大块连续物理内存的分配和释放,以页框为基本单位,可以避免外部碎片,可以分配20~29大小的物理内存块。
在这里插入图片描述
我们知道经过页机制我们可以不必要求有大块连续物理内存,我们可以通过页机制东拼西凑出一块内存,虽然物理内存不连续,但是通过页机制,程序员感觉在操作一块连续的虚拟内存。但我们更倾向于分配连续的物理页框,这有利于页表的管理。那么为了能够分配连续的物理内存,就需要解决页外碎片(外部碎片)的问题,在Linux中采用伙伴算法来解决。

Buddy 算法将所有的空闲物理页分成 10 组,每组分别包含大小 1,2,4,8,16,32,64,128,256,512 个连续物理页。每一组用链表组织起来。
在这里插入图片描述
分配方法:

  1. 对于一个 2order 个连续页框大小的内存申请,伙伴系统首先查看 zone->free_area[order] 中是否有空闲的块。如果找到,则直接分配给请求对象。
  2. 如果没有, 查找 zone->free_area[order+1] 是否有空闲块,如果有: 则摘下一块,并且分成两等分,分配一份给请求对象,另一份插入到 zone->free_area[order] 中。
  3. 如果还没有, 则依次往更大连续物理内存分组寻找,直到满足需求。

回收方法:
回收算法根据提供的块大小,将块放到大小对应的链表中。如果放入过程中发现有空闲伙伴块, 则合并伙伴, 形成更大的块放到对应链表中。是否为伙伴块需要满足的条件:

  1. 两个块大小相同;
  2. 两个块地址连续;
  3. 两个块必须是同一个大块中分离出来的;

通过伙伴算法分配的页框都是4K的,但是内核使用的很多都是小对象,可能就几十字节,比如存放文件描述符、进程描述符、虚拟内存区域描述符等行为所需的内存都不足一页,这就产生了内部碎片。Linux为了解决内部碎片的问题,在内核实现了slab分配器,主要针对内核中经常分配并释放的对象。
在这里插入图片描述

以上所有用一张经典的图总结如下:
在这里插入图片描述

相关问题

1. 虚拟内存的作用?
  1. 使得进程的运行内存超过物理内存的限制。这主要是得益于程序的「局部性原理」,我们可以将程序中常用的部分放入内存,不常用的换出到硬盘的swap区域,当发生缺页时就执行缺页中断程序,从swap内换入。
    但是该运行内存还是应该在虚拟内存能申请下的前提下才能去试着超过物理内存,可以参考本章问题3.
  2. 易于多进程开发。每个进程的虚拟地址是相互独立的,都有各自的页表,这些页表是私有的,相同的虚拟地址也不会发生地址冲突。
  3. 提供了安全性。页表里的页表项中除了物理地址之外,还有一些标记属性的比特,比如控制一个页的读写权限,标记该页是否存在等,不直接访问到物理内存,就能够进行上述检查。
2. 分页和分段的区别?
  1. 分页对程序员是透明的,但是分段需要程序员显式划分每个段。
  2. 分页的地址空间是一维地址空间,分段是二维的。这可以大致理解为分页虚拟地址给出,可以通过页的大小求出分页序号和页内偏移量,而分段则需要程序员显示划分。
  3. 页的大小不可变,段的大小可以动态改变。
  4. 分页主要用于实现虚拟内存,页大小固定的情况下提高了物理内存的利用率;分段主要是为了组织虚拟内存,使程序和数据可以被划分为逻辑上独立的地址空间并且有助于共享和保护。
3. 在 4GB 物理内存的机器上,申请 8G 内存会怎么样?

应用程序通过 malloc 函数申请内存的时候,实际上申请的是虚拟内存,此时并不会分配物理内存。但是虚拟内存在不同位数(32或者64)的操作系统上,也有上限。

当应用程序读写了这块虚拟内存,CPU 就会去访问这个虚拟内存, 这时会发现这个虚拟内存没有映射到物理内存, CPU 就会产生缺页中断,进程会从用户态切换到内核态,并将缺页中断交给内核的 Page Fault Handler (缺页中断函数)处理。

  • 在 32 位操作系统,因为进程最大只能申请 3 GB 大小的虚拟内存,所以直接申请 8G 内存,会申请失败。
  • 在 64位 位操作系统,因为进程最大只能申请 128 TB 大小的虚拟内存,即使物理内存只有 4GB,申请 8G 内存也是没问题,因为申请的内存是虚拟内存。如果这块虚拟内存被访问了,要看系统有没有 Swap 分区:
    • 如果没有 Swap 分区,因为物理空间不够,进程会被操作系统杀掉,原因是 OOM(内存溢出);
    • 如果有 Swap 分区,即使物理内存只有 4GB,程序也能正常使用 8GB 的内存,进程可以正常运行;
  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

互联网民工蒋大钊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值