GPU内存管理

Heap

GPU驱动程序(DDK)使用heap管理设备虚拟内存和设备物理内存,应用启动时在应用层会创建
general svm heap、general heap、PDS code date heap、USC code heap等设备虚拟地址段(DEVMEM_HEAP),每个应用的设备虚拟地址都是独立的,类似CPU虚拟地址;同时,内核驱动模块探测到GPU设备时,会为设备的物理地址空间创建多个heap(PHYS_HEAP),包括PHYS_HEAP_GPU_LOCAL、PHYS_HEAP_GPU_PRIVATE、PHYS_HEAP_FW_MAIN、PHYS_HEAP_FW_CONFIG、PHYS_HEAP_FW_CODE等。关于heap的详细分析参看我写的另一篇文档《DDK heap的创建流程》。

RA

RA即resource arena,可以理解为资源池。heap使用RA管理内存,包括内存的申请、内存的释放等。每个物理地址heap(PHYS_HEAP)有一个RA,每个虚拟地址heap(DEVMEM_HEAP)有两个RA,详细见后面分析。firmware相关的FwMain设备虚拟地址空间、FwConfig设备虚拟地址空间也是通过RA进行管理。

RA通过伸展树(splay tree)管理内存。每个RA都有自己的伸展树。当调用RA_Create创建一个RA时,伸展树为空,随后可以调用RA_Add向RA中添加资源,添加资源后RA对应的伸展树就有了一个根节点,整个流程可以参见函数RA_Create_With_Span。

伸展树以标记(flag)为key进行组织,相同flag值属于同一个结点,不同的结点flag值不同。创建RA并添加资源时,传入的参数flag值为0,所以根节点的flag值为0。

flag表示内存的特性,例如表示使用的哪块物理内存(GPU Local、GPU Private、Fw Main、Fw Config、Fw code等),还表示GPU是否可读、可写该内存,CPU是否可读、可写该内存(可以通过配置MMU页表项实现)等等。

如下图一所示:RA对应的伸展树有node A、node B、node C、node D四个结点,它们的flag分别为flag A、flag B、flag C、flag D,它们的值都各不相同。 每个结点有一个free table(代码中写为buckets)。

之所以选择伸展树,是因为考虑到程序运行的局部性原理,即在一段时间内,整个程序的执行仅限于程序中的某一部分。而在伸展树中查找时,伸展树也会进行变换,使根节点的flag等于本次查找的flag值,一段时间内很可能多次分配同样flag值的资源,这样后续就直接从根节点分配而不需要查找子节点,节省了时间。

图1 splay tree 

RA的数据结构如下,其中成员per_flags_buckets指向伸展树根节点:

/* resource allocation arena */
struct _RA_ARENA_
{
	/* arena name for diagnostics output */
	IMG_CHAR name[RA_MAX_NAME_LENGTH];

	/* Spans / Imports within this arena are at least quantum sized
	 * and are a multiple of the uQuantum. This also has the effect of
	 * aligning these Spans to the uQuantum.
	 */
	RA_LENGTH_T uQuantum;

	/* import interface, if provided */
	PFN_RA_ALLOC pImportAlloc;

	PFN_RA_FREE pImportFree;

	/* Arbitrary handle provided by arena owner to be passed into the
	 * import alloc and free hooks
	 */
	void *pImportHandle;

	IMG_PSPLAY_TREE per_flags_buckets;

	/* resource segment list */
	BT *pHeadSegment;

	/* segment address to boundary tag hash table */
	HASH_TABLE *pSegmentHash;

	/* Lock for this arena */
	POS_LOCK hLock;

	/* Policies that govern the resource area */
	RA_POLICY_T ui32PolicyFlags;

	/* LockClass of this arena. This is used within lockdep to decide if a
	 * recursive call sequence with the same lock class is allowed or not.
	 */
	IMG_UINT32 ui32LockClass;

	/* Total Size of the Arena */
	IMG_UINT64	ui64TotalArenaSize;

	/* Size available for allocation in the arena */
	IMG_UINT64	ui64FreeArenaSize;

};

伸展树节点数据结构如下,其中成员psLeft指向该节点的左节点、成员psRight指向该节点的右节点,buckets为该节点的free list:

typedef struct img_splay_tree
{
	/* left child/subtree */
    struct img_splay_tree * psLeft;

	/* right child/subtree */
    struct img_splay_tree * psRight;

    /* Flags to match on this span, used as the key. */
    IMG_PSPLAY_FLAGS_T uiFlags;
#if defined(PVR_CTZLL)
	/* each bit of this int is a boolean telling if the corresponding
	   bucket is empty or not */
    IMG_ELTS_MAPPINGS bHasEltsMapping;
#endif
	struct _BT_ * buckets[FREE_TABLE_LIMIT];
} IMG_SPLAY_TREE, *IMG_PSPLAY_TREE;

free table管理

free table用于管理空闲内存,free table有40个buckets[0]、buckets[1],...,buckets[39],每个结点指向一个链表,这些链表的结点由空闲内存块组成。当从内存池中分配内存时,用户需要指定内存flag,然后在splay tree中查找该flag,如果找到则从该结点关联的free list中查找空闲内存,在查找flag的过程中,splay tree会进行旋转操作,这个操作比较耗时。buckets[0]指向的链表中都是空闲内存大小为[^{}\mathbf{}2^{0}2^{1})字节的结点,buckets[1]指向的链表中都是空闲内存大小为[2^{1}2^{2})字节的结点,以此类推。BT(boundary tags)用于描述资源段,每个BT描述的资源都是连续的。

图2 free table

BT的数据结构如下:

/* boundary tags, used to describe a resource segment */
struct _BT_
{
	enum bt_type
	{
		btt_free,		/* free resource segment */
		btt_live		/* allocated resource segment */
	} type;      //表示该BT空闲还是已分配

	unsigned int is_leftmost;
	unsigned int is_rightmost;
	unsigned int free_import;

	/* The base resource and extent of this segment */
	RA_BASE_T base;
	RA_LENGTH_T uSize;

	/* doubly linked ordered list of all segments within the arena */
	struct _BT_ *pNextSegment;
	struct _BT_ *pPrevSegment;

	/* doubly linked un-ordered list of free segments with the same flags. */
	struct _BT_ *next_free;
	struct _BT_ *prev_free;

	/* A user reference associated with this span, user references are
	 * currently only provided in the callback mechanism
	 */
	IMG_HANDLE hPriv;

	/* Flags to match on this span */
	RA_FLAGS_T uFlags;

};
typedef struct _BT_ BT;

重要成员变量含义如下:

  • is_leftmost:指示该BT是否是某连续地址段的起始地址块。
  • is_rightmost:指示该BT是否是某连续地址段的结束地址块。如果is_leftmost和is_rightmost都为0,则指示该BT是某连续地址段的中间块。如果is_leftmost和is_rightmost都为非0,则指示该BT还没有被分裂(split)为几个BT。
  • free_import:如果为非0,则指示该BT从其他RA import(详细见下面分析)。
  • pNextSegment:指向下一个BT。一个RA的所有BT(无论空闲还是已经分配)都会加入结构体_RA_ARENA_成员pHeadSegment指向的链表,链表中的BT就是通过BT的成员变量pNextSegment和pPrevSegment连接。当需要从一个大BT中分配一块小的资源时,一个大BT被分裂为多个BT后,通过该指针可以找到被分裂的相邻BT,这些相邻BT组成的地址段连续;但是通过该指针连接的相邻BT地址不一定是连续的,只有is_leftmost被设置的BT和is_rightmost被设置的BT及它们之间的BT组成的资源是连续的,如下图3、图4所示。
  • pPrevSegment:指向前一个BT。详见上面成员变量pNextSegment分析。
  • next_free:指向下一个空闲BT。
  • prev_free:指向前一个空闲BT。

从RA分配资源

当从RA中分配资源时,需要根据分配资源的flag在伸展树中查找,如果伸展树的所有结点没有等于分配内存flag的值,则返回失败;否则,返回找到的结点,并在该结点的free table(buckets)中分配资源。在伸展树中查找时,伸展树也会进行变换,使根节点的flag等于本次查找的flag值,这是考虑到程序运行的局部性原理。

分配资源时选择buckets有如下两种策略,在创建RA(函数RA_Create)时可以指定采用哪种策略:

  • best fit:选择接近请求大小且满足请求大小的空闲BT,但是性能没有assured fit好,花费的时间可能更长。具体算法:对请求大小size计算对数,对数的底为2,得到结果index_low(对数结果向下取整),在buckets[index_low]指向的链表中顺序查找,请求分配资源时还会有基地址对齐要求,当BT的基地址向后偏移到满足对齐要求后,且BT剩余的大小大于等于请求大小则该BT满足要求,返回该BT,并退出查找。因为有对齐要求,所以buckets[index_low]未必能找到满足要求的BT。当buckets[index_low]未找到时,则查找buckets[index_low+1]的链表,直到查找完buckets[39]。注意,buckets指向的链表可能为空。
  • assured fit:RA默认策略。从能满足请求大小和对齐要求的空闲BT查找,相对于best fit,拥有更好的性能。对(请求大小size+对齐要求alignment-1)计算对数,对数的底为2,得到结果index_high(对数结果向下取整)。在buckets[index_high+1]指向的链表中顺序查找,当BT的基地址向后偏移到满足对齐要求后,且BT剩余的大小大于等于请求大小则该BT满足要求,返回该BT,并退出查找,这里从index_high+1开始查找是因为取对数是向下取整(如果对数不是整数),所以buckets[index_high]不一定能满足要求,而index_high+1就一定能满足要求,如果buckets[index_high+1]为NULL,则从buckets[index_high+2]查找,直到buckets[39];如果都为NULL,则buckets向下查找,先查找buckets[index_high],如果该buckets指向的BT链表没有满足要求的,则查找buckets[index_high-1],直到buckets[index_low],根据上面best fit策略的分析,buckets[index_low]里面可能会有满足要求的BT,但是buckets[index_low-1]指向的BT链表肯定都太小不满足要求。

优缺点对比:assured fit比best fit能更快找到满足要求的BT,拥有更好的时间性能,但是它比best fit更先将更大块的内存split,造成大块连续内存更少,更容易出现分配大块连续内存失败的情况。

找到空闲BT后,会将该BT起始地址开始到对齐地址结束的内存split并根据大小放入相应的buckets(如果起始地址已经满足对齐要求则不需要split),然后从对齐地址开始直到满足请求大小处结束split并分配出去,最后分配出去的地址段后面可能还会有一部分空闲内存,该内存也会被split并根据大小放入相应的buckets。

如下图3、图4所示,当从空闲BT A分配内存时,因为地址对齐前段被split出了空闲BT A0,然后中段被split出BT A1并被分配出去,最后剩余后段空闲BT A2。被分配前BT A的is_leftmost和is_rightmost都为1,BT A与相邻结点资源不连续(相邻结点为BT A指针pNextSegment、pPrevSegment指向的结点),如果BT A与相邻结点资源连续且都为空闲,则必然会合并为更大的空闲资源。BT A被split前其成员变量is_leftmost和is_rightmost都为1,都split后低地址段BT A0的成员变量is_leftmost为1、is_rightmost为0,被分配出去的中段BT A1的成员变量is_leftmost和is_rightmost都为0,高段BT A2成员变量is_leftmost为0、is_rightmost为1。同时,BT A从链表buckests[Z]中移除,BT A split后根据BT A0、BT A2的大小分别插入链表buckets[X]、buckets[Y],而被分配出去的BT A1因为不是空闲资源,所以不需要插入buckets链表。空闲资源插入buckets链表时有如下两种策略,在创建R(函数RA_Create)时可以指定采用哪种策略:

  • fast:默认策略,直接插入链表首节点,如下图4所示。
  • optimal:链表根据BT资源大小排序,头结点资源最小,尾结点资源最大,插入链表时需要根据BT资源大小插入合适位置,保持排序。

fast策略相对于optimal策略速度更快,但是当分配资源时,遍历buckets链表返回第一个满足条件的BT。如果buckets链表排序,则返回的是该链表中满足条件的BT当中资源大小最小的,这样可以降低碎片(资源大小更小的意味着更低概率被split),且减少更大的BT被分配,从而保证后面分配更大连续资源时能被满足。

最后,它们都需要按照低地址到高地址的顺序插入RA的成员变量指针pHeadSegment指向的链表,且以BT A1的基地址作为key、BT A1的结构体_BT_地址为value插入到RA的成员变量pSegmentHash指向的hash表,这样做的目的是释放资源函数RA_Free传入的参数为RA的结构体_RA_ARENA_地址和需要释放资源的基地址,有了该hash表就可以快速通过基地址找到结构体_BT_的地址,从而进行释放操作,只有已分配的资源才会加入该hash表。

图3 free BT A被split前

图4 free BT A被split后

向RA释放资源

向RA释放资源就是申请资源的逆向过程,主要步骤如下:

  1. 以要释放资源的基地址作为key,在RA的成员变量pSegmentHash指向的hash表中找到BT,并将其从该hash表中移除。
  2. 如果该BT的成员变量is_leftmost为0,且其前面相邻BT空闲(BT成员变量pPrevSegment指向的结点,简称left BT),根据上面分析它们资源肯定连续,则将两个BT合并为一个空闲BT。具体步骤是,将left BT从buckets链表中移除,RA成员变量pHeadSegment指向的链表中将两个相邻BT合并为一个新BT,新BT的基地址为left BT的资源的基地址,新BT的成员变量is_leftmost设置为left BT的成员变量is_leftmost值,新BT的资源大小为两个BT资源大小总和。如果该BT的成员变量is_leftmost为1,则表明该BT为连续资源的起始段,不能和前面相邻节点合并。
  3. 同理,如果该BT的成员变量is_rightmost为0,且其后面相邻BT空闲(BT成员变量pNextSegment指向的结点),则进行合并;否则不合并。
  4. 如果合并后的BT或待释放的BT的成员变量is_leftmost和is_rightmost都为1,且该BT是import的(BT成员变量free_import为真),则将该BT从RA成员变量pHeadSegment指向的链表中删除,并将import资源释放。import流程见下面详解。
  5. 如果上面的条件不满足,则不走import资源释放流程。根据合并后的BT或待释放的BT的资源大小插入当前RA的相应buckets链表,需要根据fast和optimal策略插入合适位置。

上面第4步和第5步是互斥的,只能根据条件走其中一个流程。

从其他RA import资源

前面分析了从RA申请资源,如果RA资源不足分配失败,则调用该RA结构体_RA_ARENA_成员变量pImportAlloc指向的函数,该函数指针在函数RA_Create创建RA时设置,如果调用RA_Create时对应传入的参数为NULL,则函数RA_Create会设置pImportAlloc为一个直接返回失败的函数。在系统中创建device memory、sync checkpoint和sync prim对应的RA时会设置自己的import函数,创建其他RA时不会设置。下面以device memory为例说明import流程。

应用启动时,应用层(非内核)会创建多个device memory heap,包括SVM heap、General heap、USC Code heap等,随后应用就可以从这些heap中分配内存。这个heap对应的数据结构如下:

struct DEVMEM_HEAP_TAG
{
	/* Name of heap - for debug and lookup purposes. */
	IMG_CHAR *pszName;

	/* Number of live imports in the heap */
	ATOMIC_T hImportCount;

	/* Base address and size of heap, required by clients due to some
	 * requesters not being full range
	 */
	IMG_DEV_VIRTADDR sBaseAddress;
	DEVMEM_SIZE_T uiSize;

	DEVMEM_SIZE_T uiReservedRegionSize; /* uiReservedRegionLength in DEVMEM_HEAP_BLUEPRINT */

	/* The heap manager, describing if the space is managed by the user, an RA,
	 * kernel or combination */
	IMG_UINT32 ui32HeapManagerFlags;

	/* This RA is for managing sub-allocations within the imports (PMRs)
	 * within the heap's virtual space. RA only used in DevmemSubAllocate()
	 * to track sub-allocated buffers.
	 *
	 * Resource Span - a PMR import added when the RA calls the
	 *                 imp_alloc CB (SubAllocImportAlloc) which returns the
	 *                 PMR import and size (span length).
	 * Resource - an allocation/buffer i.e. a MemDesc. Resource size represents
	 *            the size of the sub-allocation.
	 */
	RA_ARENA *psSubAllocRA;
	IMG_CHAR *pszSubAllocRAName;

	/* The psQuantizedVMRA is for the coarse allocation (PMRs) of virtual
	 * space from the heap.
	 *
	 * Resource Span - the heap's VM space from base to base+length,
	 *                 only one is added at heap creation.
	 * Resource - a PMR import associated with the heap. Dynamic number
	 *            as memory is allocated/freed from or mapped/unmapped to
	 *            the heap. Resource size follows PMR logical size.
	 */
	RA_ARENA *psQuantizedVMRA;
	IMG_CHAR *pszQuantizedVMRAName;

	/* We also need to store a copy of the quantum size in order to feed
	 * this down to the server.
	 */
	IMG_UINT32 uiLog2Quantum;

	/* Store a copy of the minimum import alignment */
	IMG_UINT32 uiLog2ImportAlignment;

	/* The parent memory context for this heap */
	struct DEVMEM_CONTEXT_TAG *psCtx;

	/* Lock to protect this structure */
	POS_LOCK hLock;

	/* Each "DEVMEM_HEAP" has a counterpart in the server, which is
	 * responsible for handling the mapping into device MMU.
	 * We have a handle to that here.
	 */
	IMG_HANDLE hDevMemServerHeap;

	/* This heap is fully allocated and premapped into the device address space.
	 * Used in virtualisation for firmware heaps of Guest and optionally Host
	 * drivers. */
	IMG_BOOL bPremapped;
};

typedef struct DEVMEM_HEAP_TAG DEVMEM_HEAP;
  • 成员变量psSubAllocRA为一个RA,该RA设置pImportAlloc为SubAllocImportAlloc、设置pImportFree为SubAllocImportFree,但是并未向该RA中添加资源。
  • 成员变量psQuantizedVMRA为一个RA,该RA未设置pImportAlloc、pImportFree(RA_Create设置直接返回失败的函数),创建RA后会调用RA_Add向该RA添加heap对应的设备虚拟地址段资源。

创建好后的device memory heap如下图5所示,可见device memory heap中并没有物理内存资源。

图5 import资源前

当应用从DEVMEM_HEAP分配内存时,GPU UMD驱动是从psSubAllocRA指向的RA中分配(对应函数RA_Alloc),而起始的时候psSubAllocRA中并没有资源,从psSubAllocRA分配失败后,走import流程如下:

  1. 分配内存时传下的参数flag有从哪个physical heap分配内存的信息,从该physical heap对应的RA中分配物理内存,分配后如下图6所示。physical heap可以是LMA即GDDR,也可以是UMA即host RAM。这里假设是LMA,如果physical heap是LMA,则结构体PHYS_HEAP的成员指针变量pvImplData指向结构体PHYSMEM_LMA_DATA,构体PHYSMEM_LMA_DATA中成员psRA指向RA;如果physical heap是UMA,则成员指针变量pvImplData为NULL。这里假设physical heap为LMA。如果flag中未指定physical heap则使用default heap(GPU_LOCAL,如果flag中指定CPU mappable且未指定GPU cache coherent)或GPU_PRIVATE(如果flag中未指定CPU mappable)或GPU_COHERENT(如果flag中指定CPU mappable并指定GPU cache coherent)。
  2. 从DEVMEM_HEAP结构体成员变量psQuantizedVMRA指向的RA中分配与物理内存同样大小的虚拟内存。
  3. 建立页表,将分配的物理内存和虚拟内存进行映射。
  4. 最后将import的资源添加到psSubAllocRA指向的RA中。具体步骤如下:
  • 创建BT,将BT资源基地址设置为从psQuantizedVMRA中分配的虚拟内存基地址,BT资源大小等于物理内存/虚拟内存大小,BT成员变量uFlags为分配内存时传入的flag。
  • 以BT成员uFlags为key创建splay tree结点,并将结点插入splay tree。后面再import资源的时候如果BT成员变量uFlags的值等于splay tree中某结点的key,则不需要再创建splay tree结点,直接将新创建的BT插入splay tree该结点的free table即可。
  • 将BT插入psSubAllocRA成员变量pHeadSegment指向的链表,成为该链表首节点。
  • 将BT插入创建的splay tree结点的free table(即buckets)。
  • 设置BT的成员free_import为1。

注意,import后,分配的物理内存相关的BT还在PHYS_HEAP对应的RA的成员变量pHeadSegment链表中,分配的虚拟内存相关的BT还在psQuantizedVMRA对应的RA的成员变量pHeadSegment链表中,如下图6所示。import成功后,再次从psSubAllocRA分配资源,流程和前面讲的一样,应用就可以对分配的内存进行操作了;如果import失败,则应用分配内存失败。上面第一次从psSubAllocRA分配失败,然后import资源,最后再从psSubAllocRA分配成功只需要调用一次RA_Alloc函数完成。RA_Alloc如果涉及到import,则会产生递归调用,代码大致流程如下:

RA_Alloc(psSubAllocRA, ...)                       从psSubAllocRA分配资源
	->_AttemptAllocAligned                        从psSubAllocRA的splay tree中分配资源,分配成功则返回,分配失败则继续向下运行
	->_AttemptImportSpanAlloc
		->pArena->pImportAlloc 即调用函数 SubAllocImportAlloc
			.... 中间省略
				-> RA_Alloc                       从PHYS_HEAP对应的RA中分配物理
			->DevmemImportStructDevMap
				-> RA_Alloc(psQuantizedVMRA, ...) 从psQuantizedVMRA分配设备虚拟内存
				-> BridgeDevmmeIntMapPMR          将设备虚拟内存映射到物理内存,因为是在应用层调用RA_Alloc(psSubAllocRA...),需要陷入到内核由KMD完成映射
		->_InsertResourceSpan                     将import的资源添加到psSubAllocRA
	->_AttemptAllocAligned						  再次从psSubAllocRA的splay tree中分配资源,如果再分配失败,则整个分配流程失败

图 6 import资源后

import的资源大小大于等于请求分配的内存大小。在调用RA的pImportAlloc指向的函数前,需要计算import资源大小,需要考虑如下即个点:

  1. 请求资源大小及资源基地址对其,它们都是由应用传下来的。
  2. 超分配倍数,也是由应用传下来的,根据第1步计算的资源大小乘以超分配倍数得到一个值。这样做的目的是可以分配更大的资源,下一次应用申请资源时就不用再import资源,提高分配资源性能。
  3. 如果第2步的值不是psSubAllocRA成员uQuantum的倍数,则对第2步的值向上取整成为uQuantum的倍数,uQuantum是调用函数RA_Create创建RA时设置的,该值不是由应用传下来的,是根据系统page size(一般为4KB)或其他方式设置的。所以当应用分配4字节内存时,很可能import 4KB内存。

DDK(UMD和KMD)调用RA_Create创建RA,并调用RA_Add向RA中添加资源时,传入的flag都为0,所以它们的splay tree都只有一个结点且key值为0。后面调用RA_Alloc分配资源时,只有向DEVMEM_HEAP的成员psSubAllocRA调用RA_Alloc分配资源时,传入的flag可以不为0,其他所有调用RA_Alloc的地方传入的flag都为0。当调用RA_Alloc向DEVMEM_HEAP成员变量psSubAllocRA分配资源时,传入的flag可以不为0,但是import时调用RA_Alloc从PHYS_HEAP和psQuantizedVMRA分配资源时,传入的flag都为0,不然分配会失败,因为PHYS_HEAP和psQuantizedVMRA的splay tree都只有一个key为0的结点,import成功后psSubAllocRA的splay tree的结点的key值就等于RA_Alloc从psSubAllocRA分配资源时传入的flag值。sync prim和sync checkpoint也有import流程,但是调用RA_Alloc从它们的psSubAllocRA分配资源时,传入的flag都为0,所以import后psSubAllocRA的splay tree的结点的key值也为0。

释放import的资源

对于import的资源,释放的步骤与章节《向RA释放资源》的前面3步一样,第4步如果合并后的BT或待释放的BT的成员变量is_leftmost和is_rightmost都为1,且该BT是import的(BT成员变量free_import为真),则进行如下处理:

  1. 将该BT从RA(psSubAllocRA)成员变量pHeadSegment指向的链表中删除。
  2. 解除之前import的设备虚拟内存和设备物理内存的映射。
  3. 释放import设备虚拟内存到psQuantizedVMRA对应的RA。
  4. 释放import的物理内存到PHYS_HEAP的RA。

完成以上步骤后,就回到了图5所示的import前的状态。 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值