关于伙伴算法的学习

84 篇文章 0 订阅

学习地址:

https://blog.csdn.net/jy1075518049/article/details/43911183

本文学习自:

https://blog.csdn.net/jy1075518049/article/details/43911183

并不是我自己的原创

首先上代码:

list.h

//
// Created by zhanglei on 2019/12/27.
//

#ifndef TEST_LIST_H
#define TEST_LIST_H

struct list_head
{
    struct list_head *next,*prev;
};

static inline void INIT_LIST_HEAD(struct list_head* list)
{
    list->next = list;
    list->prev = list;
}

//插入节点
static inline void __list_add(struct list_head *new_list,struct list_head *prev, struct list_head *next)
{
    next->prev = new_list;
    new_list->next = next;
    new_list->prev = prev;
    prev->next = new_list;
}

//在链表头部插入节点
static inline void list_add(struct list_head *new_list, struct list_head *head)
{
    __list_add(new_list,head,head->next);
}

//删除任意节点
static inline void __list_del(struct list_head * prev, struct list_head * next)
{
    next->prev = prev;
    prev->next = next;
}


static inline void list_del(struct list_head *entry)
{
    __list_del(entry->prev, entry->next);
}


//后序(指针向后走)遍历链表
#define list_for_each(pos, head) \
    for (pos = (head)->next; pos != (head); pos = pos->next)

//前序(指针向前走)遍历链表
#define list_for_each_prev(pos, head) \
    for (pos = (head)->prev; pos != (head); pos = pos->prev)


#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)


//这个宏中某些可能只有GNU支持,我实验的环境是windows下qt5.2,很幸运,也支持
#define list_entry(ptr, type, member) ({			\
    const typeof( ((type *)0)->member ) *__mptr = (ptr);	\
    (type *)( (char *)__mptr - offsetof(type,member) );})

#endif

mem_manage.h

#ifndef MEM_MANAGE_H
#define MEM_MANAGE_H

#include "list.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MEM_NUM 11
#define BLOCK_BASE_SIZE 4096

#define TRUE 1
#define FALSE 0

#define random(x) (rand()%x+1)

#define cloth2cover(num) ((num & num-1) == 0) ? TRUE : FALSE

typedef unsigned int u32;

struct free_area_head
{
    u32 size;//链表内存大小
    u32 num;//相对应的内存块数
    struct list_head list;
};

struct free_chunk{
    int dir;//标识这块内存块是否被使用 1是被使用 0是没有被使用
    u32 size; //单位是kb
    char* addr;//内存地址
    struct list_head list;
};

enum BUDDY_TYPE
{
    NO_BUDDY = 0,
    LAST_LIST
};

//函数声明
int mem_init();
int mem_malloc(int size);
int mem_avail(int size);
int mem_free(int size);
#endif

mem_manage.c

#include "mem_manage.h"

struct free_area_head mem_arr[MEM_NUM];  //指向不同种类内存块的指针数组
char *pmem = NULL;

/**
 * @brief 内存初始化函数,包括申请内存,分割内存,挂入管理链表等操作
 * @return 成功返回0,失败返回-1
 */
int mem_init(void)
{
    int i, j, total_size = 0;
    u32 trunk_size;
    struct free_chunk *tmp_chunk = NULL;

    for(i=0; i<MEM_NUM; i++)
    {
        memset(&mem_arr[i], 0, sizeof(struct free_area_head));
        //第0个链表代表4KB的内存块,第1个代表8KB,第2个代表16KB...以此类推
        mem_arr[i].size = (1<<i) * BLOCK_BASE_SIZE;  //单位是字节
        INIT_LIST_HEAD(&mem_arr[i].list);  //初始化每个链表头
    }

    //分配4KB的内存块11个,8KB的10个,16KB的9个....4MB的1个
    for(i=0; i<MEM_NUM; i++)
    {
        total_size += (i+1)*BLOCK_BASE_SIZE * (1<<(10-i));
    }

    //把这块内存分配出来
    pmem = (char *)malloc(total_size);
    if(pmem == NULL)
    {
        printf("err malloc mem\n");
        return -1;
    }
    else
    {
        printf("malloc mem success\n");
        printf("alloc mem init addr is %u\n\n", pmem);
    }


    /*
        这段代码可能不太好理解,需要把具体数带进去验算一下
        目的:把上面分配的一大段连续内存依次拆分为:
        4KB/8KB/16KB...4096KB          第1个序列
        4KB/8KB/16KB...2048KB          第2个序列
        4KB/8KB/16KB...1024KB          第3个序列
        4KB/8KB/16KB...512KB           第4个序列
        4KB/8KB/16KB...256KB           第5个序列
        4KB/8KB/16KB...128KB           第6个序列
        4KB/8KB/16KB...64KB            第7个序列
        4KB/8KB/16KB/32KB              第8个序列
        4KB/8KB/16KB                   第9个序列
        4KB/8KB                        第10个序列
        4KB                            第11个序列
        上面的内存段都是连续的,这样做的目的是让所有相同大小的内存块都不相邻
        两个嵌套之后,上面内存卡就依次联入相应的链表中了
    */

    for(i=MEM_NUM; i>0; i--)
    {
        //第一个连续内存  4KB/8KB/16KB...2048KB/4096KB
        for(j=0; j<i; j++)
        {
            //先把4KB挂在4KB的链表上
            trunk_size = BLOCK_BASE_SIZE*(1<<j);
            tmp_chunk = (struct free_chunk *)malloc(sizeof(struct free_chunk));
            tmp_chunk->size = trunk_size / 1024;  //以KB为单位
            tmp_chunk->addr = pmem;  //记录下这个地址
            tmp_chunk->dir = 0;  //初始化内存卡未被占用
            list_add(&tmp_chunk->list, &mem_arr[j].list); //插入链表
            mem_arr[j].num++;  //相应链表内存块数目加1
            pmem += trunk_size; //指针相应往后移动4KB/8KB/16KB...2048KB/4096KB
        }
    }

    //我们把各个链表相关内容打印出来看一下
    struct list_head *pos;
    struct free_chunk *tmp;

    //首先是每个链表的内存块大小和内存块数目
    for(i=0; i<MEM_NUM; i++)
    {
        printf("the %d list mem num is %d:", i, mem_arr[i].num);
        list_for_each(pos, &mem_arr[i].list)
        {
            tmp = list_entry(pos, struct free_chunk, list);
            printf("%d ", tmp->size);
        }
        printf("\n");
    }

    //怎么验证内存地址的正确性呢?
    printf("\nthe 4KB list chunk addr is:\n");
    //由于list_add是头部插入,所以这里按照从尾部到头部的顺序打印
    list_for_each_prev(pos, &mem_arr[0].list)
    {
        tmp = list_entry(pos, struct free_chunk, list);
        printf("%u ", tmp->addr);
    }
    printf("\n\n");

    /*
        malloc mem success
        alloc mem init addr is 19136544
        the 0 list mem num is 11:4 4 4 4 4 4 4 4 4 4 4
        the 1 list mem num is 10:8 8 8 8 8 8 8 8 8 8
        the 2 list mem num is 9:16 16 16 16 16 16 16 16 16
        the 3 list mem num is 8:32 32 32 32 32 32 32 32
        the 4 list mem num is 7:64 64 64 64 64 64 64
        the 5 list mem num is 6:128 128 128 128 128 128
        the 6 list mem num is 5:256 256 256 256 256
        the 7 list mem num is 4:512 512 512 512
        the 8 list mem num is 3:1024 1024 1024
        the 9 list mem num is 2:2048 2048
        the 10 list mem num is 1:4096
        the 4KB list chunk addr is:
        19136544 27521056 31711264 33804320 34848800 35368992
        35627040 35754016 35815456 35844128 35856416
        这些地址每次运行程序都不一样,根据实际情况来看
        得到上面打印结果,首先我们看到11个链表的数目和每个内存卡的大小是正确的
        然后如何验证地址呢?理解这个需要自己画下图,以第一个链表的前两个4KB块的
        首地址为例,这两个首地址应该隔着这么大一块地址空间:
        4+8+16+32+64+128+256+512+1024+2048+4096 = ?KB
        那么 27521056 - 19136544 = 8384512(byte) = 8188KB
        你可以验算一下,这两个值是相等的,同理,你可以验证第2个和第3个的差值
        看是否和理论上的值一样
    */
    return 0;
}


/**
 * @brief 得到内存数组索引,实际上就是求size是2的几次幂
 */
static int get_index(int size)
{
    int i, tmp = 0;
    size /= 4;

    for(i=0; tmp < size; i++)
    {
        tmp = 1<<i;
        if(tmp == size)
        {
            return i;
        }
    }

    return -1;  //实际上经过前面判断不可能执行到这
}


/**
 * @brief 比较核心的函数,完成功能如下
 * 1.判断需要拆分的内存块是目标内存块的多少倍,从而知道应该拆分为几块
 * 2.把被拆分的大内存块从相应链表执行上断链操作
 * 3.把拆分的新内存块链入目标内存链表中并置dir位为0(未占用)
 * 4.选取一块返回,申请成功
 *
 * @param dst_index 目的链表索引值,即本想要申请的内存块所在链表索引值
 * @param src_index 源链表索引值,即需要拆分的内存块所在的链表索引值
 * @param block 需要拆分内存块
 */
static void separate_block(int dst_index, int src_index, struct free_chunk *block)
{
    int i;
    char *pmem;
    u32 block_num, dst_size;
    block_num = (1<<(src_index - dst_index));  //2^差值 倍

    list_del(&block->list);  //把被拆分的大内存块从相应链表执行上断链
    mem_arr[src_index].num--;  //内存块数目-1
    printf("%d list separate 1 block\n", src_index);

    //拆分为block_num块
    pmem = block->addr;  //记录首地址
    dst_size = mem_arr[dst_index].size;  //目的内存块大小

    //拆分并入链
    struct free_chunk *tmp_chunk = NULL;

    printf("%d list increase %d block\n", dst_index, block_num);
    for(i=0; i<block_num; i++)
    {
        tmp_chunk = (struct free_chunk *)malloc(sizeof(struct free_chunk));
        tmp_chunk->size = dst_size / 1024;  //以KB为单位
        tmp_chunk->addr = pmem;  //记录下这个地址
        tmp_chunk->dir = 0;  //初始化内存卡未被占用
        list_add(&tmp_chunk->list, &mem_arr[dst_index].list); //插入链表
        mem_arr[dst_index].num++;  //相应链表内存块数目加1
        pmem += dst_size; //指针相应往后移动dst_size字节
    }

    //经过上面的循环,拆分入链操作就完成了,下面只需要把拆分的内存块选一块返回即可
    struct list_head *pos;
    struct free_chunk *tmp;

    //肯定能找到的
    list_for_each(pos, &mem_arr[dst_index].list)
    {
        tmp = list_entry(pos, struct free_chunk, list);
        if(tmp->dir == 0)
        {
            printf("malloc success,addr = %u\n", tmp->addr);
            tmp->dir = 1; //标记内存块为占用
            return;
        }
    }
}

/**
 * @brief 内联函数,判断输入的size是否合法
 */
inline int mem_avail(int size)
{
    if(size<4 || size>(4<<10))  //最小4KB,最大4096KB
    {
        printf("size must > 4 and <= 4096\n");
        return FALSE;
    }
    else if(!cloth2cover(size))  //必须是2的幂
    {
        printf("size must be 2^n\n");
        return FALSE;
    }
    else
    {
        return TRUE;
    }
}

/**
* @brief 模拟内核申请内存过程
* @param size 申请内存大小,单位为KB
* @return 成功返回0,失败返回-1
*/
int mem_malloc(int size)
{
    int index, i;

    if(!mem_avail(size))
    {
        return -1;
    }

    /*
      下面是伙伴算法内存申请的过程,思想如下:
      1.首先根据size大小去相应的连表上去查找有没有空闲的内存块
        如果有,那么直接返回地址,并把相应内存块的的dir标志置位
      2.如果没有,那么去它上一级链表中找空闲块,比如4KB没有,那就去8KB找
        如果8KB也没有,就去16KB找...
      3.如果上一级链表中找到了空闲块,那么把这个空闲块从上一级链表中分类
        拆分为相应大小的内存卡后链入查找的链表中
      4.如果直到4MB的内存卡都没有空闲块,那么返回错误,提示内存不足
      这段代码的难点在穷尽的查找比比size大的链表操作
    */
    index = get_index(size);

    printf("first find %d list\n", index);

    struct list_head *pos;
    struct free_chunk *tmp;

    list_for_each(pos, &mem_arr[index].list)
    {
        tmp = list_entry(pos, struct free_chunk, list);
        if(tmp->dir == 0)  //找到了一块
        {
            printf("malloc success,addr = %u\n", tmp->addr);
            tmp->dir = 1; //标记内存块为占用
            return 0;
        }
    }

    //如果执行到这里,那么说明对应的内存块链表没有找到空闲内存块
    printf("the %d list has no suitable mem block\n", index);

    //我们从比它大的内存块中再去查找,最大就是4MB的链表了
    for(i=index+1; i<MEM_NUM; i++)
    {
        printf("we will find %d list\n", i);

        list_for_each(pos, &mem_arr[i].list)
        {
            tmp = list_entry(pos, struct free_chunk, list);
            if(tmp->dir == 0)  //找到了一块
            {
                printf("find a free block from %d list,addr = %u\n", i, tmp->addr);
                //把这块大的内存块拆分为小的,并进行分配处理
                separate_block(index, i, tmp);
                return 0;
            }
        }
    }

    //如果执行到这里,那么说明相应大小的内存块无法分配成功
    printf("can't malloc mem\n");
    return -1;
}

/**
 * @brief 判断两个地址是否相邻
 * @param compare_addr 比较的地址
 * @param target_addr  目标地址
 * @param size  链表上内存块的大小
 * @return 相邻:TRUE 不相邻:FALSE
 */
static int inline is_neighbor(u32 compare_addr, u32 target_addr, u32 size)
{
    //这里是无符号数,不能用绝对值
    if(compare_addr > target_addr)
    {
        if(compare_addr - target_addr == size)
            return TRUE;
        else
            return FALSE;
    }
    else
    {
        if(target_addr - compare_addr == size)
            return TRUE;
        else
            return FALSE;
    }
}

/**
 * @brief 从索引值为index的链表上查找block的伙伴内存块并返回
 * @param block
 * @param index
 * @return
 */
struct free_chunk *find_buddy(struct free_chunk *block, int index)
{
    //伙伴内存块:大小相同,地址相邻,并且也没有被占用
    struct list_head *pos;
    struct free_chunk *tmp;

    list_for_each(pos, &mem_arr[index].list)
    {
        tmp = list_entry(pos, struct free_chunk, list);
        if(tmp->dir == 0)  //没有被占用才有比较的资格
        {
            if(is_neighbor(tmp->addr, block->addr, block->size*1024))
            {
                return tmp;
            }
        }
    }

    //到这里就是没找到
    return NULL;
}


/**
 * @brief 递归的查找伙伴并释放内存
 * @param block 需要释放的内存块
 * @param index 内存块所在的链表索引
 */
int recursive_free(struct free_chunk *block, int index)
{
    struct free_chunk *buddy;

    if(index > MEM_NUM)  //递归到了4MB的链表上
    {
        printf("max index list\n");
        return LAST_LIST;
    }

    buddy = find_buddy(block, index);  //在本链表上为它找一个“伙伴内存块”
    if(buddy == NULL)  //这个内存块没有”伙伴“
    {
        printf("this block has no buddy\n");
        block->dir = 0;  //释放它既可
        return NO_BUDDY;
    }
    else
    {
        printf("this block find a buddy\n");
        //两个内存块从原来链表断链
        list_del(&block->list);
        list_del(&buddy->list);
        mem_arr[index].num -= 2;  //少了两块
        printf("%d list decrease 2 block\n", index);

        //合并为新的内存块并入链下一级链表
        int new_addr = (block->addr < buddy->addr) ? block->addr:buddy->addr;

        struct free_chunk *tmp_chunk = NULL;
        tmp_chunk = (struct free_chunk *)malloc(sizeof(struct free_chunk));
        tmp_chunk->size = block->size * 2;  //是原来内存块的两倍大
        tmp_chunk->addr = new_addr;  //记录下这个地址
        tmp_chunk->dir = 0;  //初始化内存块未被占用
        index++;
        list_add(&tmp_chunk->list, &mem_arr[index].list); //插入链表
        mem_arr[index].num++;  //相应链表内存块数目加1
        printf("%d list increase 1 block\n", index);

        //循环这个过程,在上一级链表中查找伙伴,直到找不到伙伴或者到了4MB链表
        recursive_free(tmp_chunk, index);
    }
}

/**
* @brief 模拟内核释放内存过程
* @param size 释放内存大小,单位为KB
* @return 成功返回0,失败返回-1
*/
int mem_free(int size)
{
    int index, i;

    if(!mem_avail(size))
    {
        return -1;
    }

    /*
      下面是伙伴算法释放内存的过程,思想如下:
      1.首先根据size大小去相应的连表上去查找第一个被占用的内存块
      2.判断这个内存块是否有伙伴(大小相同,地址相邻)
      3.如果没有,直接把dir位清0即可
      4.如果有,那么把这两个内存块分别从所在的链表上断链,然后入链到上一级链表中
      5.到上一级链表中继续 2 3 4 操作,直到某一级链表没有伙伴为止
    */
    index = get_index(size);

    printf("first find %d list\n", index);

    struct list_head *pos;
    struct free_chunk *tmp;

    list_for_each(pos, &mem_arr[index].list)
    {
        tmp = list_entry(pos, struct free_chunk, list);
        if(tmp->dir == 1)  //找到了第一块被占用的内存
        {
            printf("find an occupy block,addr = %u\n", tmp->addr);
            recursive_free(tmp, index);
            return 0;
        }
    }

    //如果执行到这里,那么说明对应的内存块链表没有占用内存块
    printf("the %d list has no occupy mem block\n", index);
    return -1;
}

 

这里面mem_init做了什么呢?

我们就是这样用mem_int初始化完成一个内存矩阵,11个4k ,

 

the 0 list mem num is 11:4 4 4 4 4 4 4 4 4 4 4
the 1 list mem num is 10:8 8 8 8 8 8 8 8 8 8
the 2 list mem num is 9:16 16 16 16 16 16 16 16 16
the 3 list mem num is 8:32 32 32 32 32 32 32 32
the 4 list mem num is 7:64 64 64 64 64 64 64
the 5 list mem num is 6:128 128 128 128 128 128
the 6 list mem num is 5:256 256 256 256 256
the 7 list mem num is 4:512 512 512 512
the 8 list mem num is 3:1024 1024 1024
the 9 list mem num is 2:2048 2048
the 10 list mem num is 1:4096

 

就是这样一个链表,我们把所有的总尺寸用malloc申请一个连续的内存地址

pmem = (char *)malloc(total_size);

当我们使用mm_malloc他会把我们申请的尺寸通过get_index,把我们的申请size映射到内存区域中对应的index上,然后循环对应内存吃中的list链表,把链表上的每一个单元通过list_entry来映射到块

 

#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)


//这个宏中某些可能只有GNU支持,我实验的环境是windows下qt5.2,很幸运,也支持
#define list_entry(ptr, type, member) ({			\
    const typeof( ((type *)0)->member ) *__mptr = (ptr);	\
    (type *)( (char *)__mptr - offsetof(type,member) );})

当我们在这个区域上发现第一个没有被使用的块我们就申请成功了

 

再分析最后一个核心函数

mem_free

还是通过get_index找到尺寸映射的区域,循环链表,找到第一个备用的块

list_for_each(pos, &mem_arr[index].list)
{
    tmp = list_entry(pos, struct free_chunk, list);
    if(tmp->dir == 1)  //找到了第一块被占用的内存
    {
        printf("find an occupy block,addr = %u\n", tmp->addr);
        recursive_free(tmp, index);
        return 0;
    }
}

然后进行递归free,发现一个没有被使用的大小相同的而且相邻的块

static int inline is_neighbor(u32 compare_addr, u32 target_addr, u32 size)
{
    //这里是无符号数,不能用绝对值
    if(compare_addr > target_addr)
    {
        if(compare_addr - target_addr == size)
            return TRUE;
        else
            return FALSE;
    }
    else
    {
        if(target_addr - compare_addr == size)
            return TRUE;
        else
            return FALSE;
    }
}

/**
 * @brief 从索引值为index的链表上查找block的伙伴内存块并返回
 * @param block
 * @param index
 * @return
 */
struct free_chunk *find_buddy(struct free_chunk *block, int index)
{
    //伙伴内存块:大小相同,地址相邻,并且也没有被占用
    struct list_head *pos;
    struct free_chunk *tmp;

    list_for_each(pos, &mem_arr[index].list)
    {
        tmp = list_entry(pos, struct free_chunk, list);
        if(tmp->dir == 0)  //没有被占用才有比较的资格
        {
            if(is_neighbor(tmp->addr, block->addr, block->size*1024))
            {
                return tmp;
            }
        }
    }

    //到这里就是没找到
    return NULL;
}

找到以后在从原来的区域的链表里拿下来,削减计数器

list_del(&block->list);
        list_del(&buddy->list);
        mem_arr[index].num -= 2;  //少了两块

最后合并小块,进入更大的块

//合并为新的内存块并入链下一级链表
int new_addr = (block->addr < buddy->addr) ? block->addr:buddy->addr;

struct free_chunk *tmp_chunk = NULL;
tmp_chunk = (struct free_chunk *)malloc(sizeof(struct free_chunk));
tmp_chunk->size = block->size * 2;  //是原来内存块的两倍大
tmp_chunk->addr = new_addr;  //记录下这个地址
tmp_chunk->dir = 0;  //初始化内存块未被占用
index++;
list_add(&tmp_chunk->list, &mem_arr[index].list); //插入链表
mem_arr[index].num++;  //相应链表内存块数目加1
printf("%d list increase 1 block\n", index);
一直循环一直到找不到伙伴或者找到4m才会结束
​​​​​​​//循环这个过程,在上一级链表中查找伙伴,直到找不到伙伴或者到了4MB链表
recursive_free(tmp_chunk, index);

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
假设系统的可利用空间容量为2m个字,则系统开始运行时,整个内存区是一个大小为2m的空闲分区。在系统运行过程中,由于不断的划分,可能会形成若干个不连续的空闲分区,将这些空闲分区根据分区的大小进行分类,对于每一类具有相同大小的所有空闲分区,单独设立一个空闲分区双向链表。这样,不同大小的空闲分区形成了k(0≤k≤m)个空闲分区链表。 当需要为进程分配一个长度为n的存储空间时,首先计算一个i值,使2i-1<n≤2i,然后在空闲分区大小为2i的空闲分区链表中查找。若找到,即把该空闲分区分配给进程。否则,表明长度为2i的空闲分区已经耗尽,则在分区大小为2i+1的空闲分区链表中寻找。若存在2i+1的一个空闲分区,则把该空闲分区分为相等的连个分区,这两个分区称为一对伙伴,其中的一个分区用于分配,而把另一个加入分区大小为2i的空闲分区链表中。若大小为2i+1的空闲分区不存在,则需要查找大小为2i+2的空闲分区,若找到则对其进行两次分割:第一次,将其分割为大小为2i+1的两个分区,一个用于分配,一个加入到大小为2i+1空闲分区链表中;第二次,将第一次用于分配的空闲分区分割为2i的两个分区,一个用于分配,一个加入到大小为2i空闲分区链表中。若仍然找不到,则继续查找大小为2i+3的空闲分区,以此类推。由此可见,在最坏的情况下,可能需要对2k的空闲分区进行k次分割才能得到所需分区。 与一次分配可能要进行多次分割一样,一次回收也可能要进行多次合并,如回收大小为2i的空闲分区时,若事先已存在2i的空闲分区时,则应将其与伙伴分区合并为大小为2i+1的空闲分区,若事先已存在2i+1的空闲分区时,又应继续与其伙伴分区合并为大小为2i+2的空闲分区,依此类推。 2.2 伙伴系统的需求 根据伙伴系统算法的思想,我们组对本系统的功能划分为3种: ⑴ 根据伙伴系统算法分配内存 ⑵ 根据伙伴系统算法回收内存 ⑶ 实时查看内存使用的情况
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值