ubi 数据结构简单分析

1 篇文章 0 订阅

测试ubi镜像的方法:

sudo modprobe mtd
sudo modprobe mtdblock
sudo modprobe nandsim first_id_byte=0xec second_id_byte=0xa1 third_id_byte=0x00 fourth_id_byte=0x15
#其中fourth_id_byte,含义如下


sudo chmod 660 /dev/mtd0*
ls -lah /dev/mtd*
cat /proc/mtd
mtdinfo /dev/mtd0

sudo modprobe ubi
echo "add" > /sys/devices/virtual/misc/ubi_ctrl/uevent
sudo ubiformat /dev/mtd0 -f test.ubi -O 2048
sudo ubiattach /dev/ubi_ctrl -m 0  -O 2048

sudo mount -t ubifs /dev/ubi0_0 ./mount

对于squashfs格式的卷,mount命令如下:
sudo ubiblock -c /dev/ubi0_0    //生成/dev/ubiblock0_0
sudo mount -t squashfs -o loop /dev/ubiblock0_0 ./mount

以上是页大小为2048的UBI测试,如果是4096的页大小,改用命令:
sudo modprobe nandsim first_id_byte=0xec second_id_byte=0xd3 third_id_byte=0x10 fourth_id_byte=0xa6

------------------------------------------------------------------

UBIFS文件系统 :https://blog.csdn.net/shichaog/article/details/45932339

------------------------------------------------------------------

  ubi 是位于 ubifs 文件系统和mtd 层之间,负责ubi 卷管理

先看下两个 on-flash 数据结构 (ubi-media.h)
1. UBI erase counter header.

 struct ubi_ec_hdr { /* 64 byte */
    __be32 magic;
    __u8 version;
    __u8 padding1[3];
    __be64 ec; /* Warning: the current limit is 31-bit */
    __be32 vid_hdr_offset;
    __be32 data_offset;
    __u8 padding2[36];
    __be32 hdr_crc;
} __attribute__ ((packed));
 

其中主要字段:
ec             表示该逻辑块被擦除过的次数
vid_hdr_offset 表示vid 头的偏移,一般跟在ec header 后面
data_offset    表示用户数据的偏移位置

2. UBI volume identifier header
虽然叫卷标识头,但实际是描述logical 块的信息,
可以看ubi_eba_write_leb 中关于未映射logical block 处理部分的代码

struct ubi_vid_hdr {
    __be32 magic;
    __u8 version;
    __u8 vol_type;
    __u8 copy_flag;
    __u8 compat;
    __be32 vol_id;
    __be32 lnum; /* leb num !!! */
    __u8 padding1[4];
    __be32 data_size;
    __be32 used_ebs;
    __be32 data_pad;
    __be32 data_crc;
    __u8 padding2[4];
    __be64 sqnum;
    __u8 padding3[12];
    __be32 hdr_crc;
} __attribute__ ((packed));

其中主要字段:
vol_id          卷id号
used_ebs;       total number of used logical eraseblocks in this volume
lnum            逻辑块号
data_size       逻辑块包含字节数
data_crc        存储在该逻辑块上数据的CRC checksum
sqnum           该逻辑块的全局唯一串号

注释中提到了1个leb(逻辑块)对应2个peb(物理块的)情况
发生在两种情况,但主要都是块写操作过程中发生异常reset引起
所以引入data_crc ,和sqnum
当发生1对2情况,在选择peb 时的依据是:
1. 如果sqnum 大的块,data_crc 正确,那么选择sqnum 大的
2. 否则寻找sqnum 小的那个块
详细可看(ubi-media.h) 中大段注释


下面顺着ubi_init个过程,了解下所有相关的数据结构

首先 ubi_attach_mtd_dev 中会创建一个ubi_device,
主要字段介绍下:

struct ubi_device {
 /* 下面这两个 结构,表示 ubi device 是个char device
 并且 ,属于linux 2.6的设备模型,支持sysfs */
    struct cdev cdev;
    struct device dev;
    
    int ubi_num;
    char ubi_name[sizeof(UBI_NAME_STR)+5];
/*下面表示该ubi device 上有几个卷,以及ubi volume数组
  该数组最大外部卷数(128)+内部卷数(1) */    
    int vol_count;
    struct ubi_volume *volumes[UBI_MAX_VOLUMES+UBI_INT_VOL_COUNT];
    
    spinlock_t volumes_lock;
    int ref_count;

    int rsvd_pebs; /*保留的物理块 */
    int avail_pebs; /* 有效物理块*/
    int beb_rsvd_pebs; /* 为处理坏块保留的peb*/
    
    /* 同上,但只是个百分比,由CONFIG_MTD_UBI_BEB_RESERVE 决定*/
    int beb_rsvd_level;

        /* ubi init 之后,有该标志的必须被resize */
    int autoresize_vol_id;
    
    /* 卷表(vtabl)中 slot数 ,和vtbl 的大小
    具体计算方法(ubi_read_volume_table):
    ubi->vtbl_slots = ubi->leb_size / UBI_VTBL_RECORD_SIZE;
    if (ubi->vtbl_slots > UBI_MAX_VOLUMES)
        ubi->vtbl_slots = UBI_MAX_VOLUMES;
    ubi->vtbl_size = ubi->vtbl_slots * UBI_VTBL_RECORD_SIZE;
    */
    int vtbl_slots;
    int vtbl_size;
    
    /* 放卷信息的结构,也是on flash 的,在内部卷中,
     将它读入内存 */
    struct ubi_vtbl_record *vtbl;
    struct mutex volumes_mutex;

    int max_ec;
    /* Note, mean_ec is not updated run-time - should be fixed */
    int mean_ec;

    /* EBA sub-system's stuff */
    /*EBA => Eraseblock Association */
    unsigned long long global_sqnum;
    spinlock_t ltree_lock;
    struct rb_root ltree;
    struct mutex alc_mutex;

    /* Wear-leveling sub-system's stuff */
    /* 已使用的pebs 的rb tree 根*/
    struct rb_root used;
    /* 空闲的pebs 的rb tree 根*/
    struct rb_root free;
    /* 需要擦写的pebs 的rb tree 根*/
    struct rb_root scrub;
    /*上面这写rb-tree 都是以pnum ,物理块号做权重*/
    
    /*用于磨损平衡保护的队列 ,联接被保护的物理块*/
    struct list_head pq[UBI_PROT_QUEUE_LEN];
    int pq_head;
    spinlock_t wl_lock;
    struct mutex move_mutex;
    struct rw_semaphore work_sem;
    int wl_scheduled;
    /* ubi_wl_entry ,peb 的写平衡entry,可能挂接到不同的地方*/
    struct ubi_wl_entry **lookuptbl;
    struct ubi_wl_entry *move_from;
    struct ubi_wl_entry *move_to;
    int move_to_put;
    struct list_head works;
    int works_count;
    struct task_struct *bgt_thread;
    int thread_enabled;
    char bgt_name[sizeof(UBI_BGT_NAME_PATTERN)+2];

    /* I/O sub-system's stuff */
    /* 从下面(io_init)可以看到,这些ubi 变量与 mtd的关系
    ubi->peb_size = ubi->mtd->erasesize;
    ubi->peb_count = mtd_div_by_eb(ubi->mtd->size, ubi->mtd);
    ubi->flash_size = ubi->mtd->size;
    ubi->min_io_size = ubi->mtd->writesize;
    ubi->hdrs_min_io_size = ubi->mtd->writesize >> ubi->mtd->subpage_sft;
    ubi->ec_hdr_alsize = ALIGN(UBI_EC_HDR_SIZE, ubi->hdrs_min_io_size);
    ubi->vid_hdr_alsize = ALIGN(UBI_VID_HDR_SIZE, ubi->hdrs_min_io_size);
    ubi->leb_start = ubi->vid_hdr_offset + UBI_EC_HDR_SIZE;
    ubi->leb_start = ALIGN(ubi->leb_start, ubi->min_io_size);
    ubi->leb_size = ubi->peb_size - ubi->leb_start;
    */
    long long flash_size;
    int peb_count;
    int peb_size; /* peb_size: physical eraseblock size */
    int bad_peb_count;
    int good_peb_count;
    int min_io_size;
    int hdrs_min_io_size;
    int ro_mode;
    
    /* logical eraseblock size
     其实就是leb 中去掉vid hdr,ec hdr
     并且对齐到write size 后,剩余的部分*/
    int leb_size;
    int leb_start;
    int ec_hdr_alsize;
    int vid_hdr_alsize;
    int vid_hdr_offset;
    int vid_hdr_aloffset;
    int vid_hdr_shift;
    int bad_allowed;
    struct mtd_info *mtd;

    void *peb_buf1;
    void *peb_buf2;
    struct mutex buf_mutex;
    struct mutex ckvol_mutex;
    struct mutex mult_mutex;
};

 

ubi_init=>ubi_attach_mtd_dev=>attach_by_scanning
  =>ubi_scan

其中 相关数据结构

struct ubi_scan_info {
    struct rb_root volumes;
    struct list_head corr;
    struct list_head free;
    struct list_head erase;
    struct list_head alien;
    int bad_peb_count;
    int vols_found;
    int highest_vol_id;
    int alien_peb_count;
    int is_empty;
    int min_ec;
    int max_ec;
    unsigned long long max_sqnum; /* 和ubi_vid_hdr 的sqnum 有关*/
    int mean_ec;
    uint64_t ec_sum;
    int ec_count;
};

 

volumes    RB-tree 的根 (point to ubi_scan_volume 's struct rb_node rb)
corr       链接 数据无效的block 的list
           比如write copy 抛弃的,或者其他数据损坏的block
free       链接 已经写上ubi_ec_hdr的block 的list
erase      erase和free 的区别就是mtd 层意义上的Free ,没有写过ubi层的东西
           比如ubi_ec_hdr
alien      链接卷上保留的block 的list
bad_peb_count   
           卷上 坏块数
is_empty   记录卷对应的mtd 区是否是空闲的
max_ec,max_ec,mean_ec 写平衡用的,记录卷上块当前最大最小的擦写次数
           和
max_sqnum   卷上最大串号          

ec_sum,ec_count 是临时变量,用于process_eb 时,对used, free(no vid)的块进行
           erase count累加和记数,然后最后用来计算mean_ec 的
           mean_ec =    ec_sum/ec_count    
           scan 过程的最后会将 各seb 的 ec 设置为 si->mean_ec;
           因为一开始总为0,后面因为写平衡原因,各块应该差不多的ec count,
           设置为平均值比较好

 

下面这个结构是在scan mtd  时 将物理block 根据扫描状态
挂到(add_to_list)ubi_scan_info 的 corr,free,earse,alien list
上的 一个物理块扫描信息结构
scanning information about a physical eraseblock :

struct ubi_scan_leb {
    int ec;
    int pnum;
    int lnum;
    int scrub;
    unsigned long long sqnum;
    union {
        struct rb_node rb;
        struct list_head list;
/* 表示该ubi_scan_leb 可以连接到per-volume RB-tree 或者 eraseblock list !!! */
    } u;
};

scan 中获得的卷信息
scanning information about a volume:

struct ubi_scan_volume {
    int vol_id;
    int highest_lnum;
    int leb_count;
    int vol_type;
    int used_ebs;
    int last_data_size;
    int data_pad;
    int compat;
    struct rb_node rb; /* 向上连到scan info */
    struct rb_root root; /* root ,向下连到 scan leb info */
};

root   链接所有属于该卷的scan leb info (ubi_scan_leb)的根
rb     红黑树节点,连接到 ubi_scan_info 的 volumes

具体可见下图:

 

在图中的连接有rb tree ,和list
大概层次为 ubi_scan_info
           对应于多个卷,向下通过rb tree连接ubi_scan_volume,
           volume id 大小做为rb-tree 的权重
           或者通过list (free,erase,corr) 连接到非隶属于某volume
           的ubi_scan_leb 
          
           ubi_scan_volume
           对应于某一个卷,向下通过rb tree 连接到ubi_scan_leb
           erase count 做为rb tree  的权重


           ubi_scan_leb
           对应于某一个逻辑块扫描信息,可以通过union ,rb node
           或 list 连接到 ubi_scan_info 和ubi_scan_volume
          
所以当flash 第1次初始化,并且没有坏块,那么所有ubi_scan_leb
都将list 到ubi_scan_info  的  erase list 上

上面就是scan 过程中的所有扫描结构,这些结构的产生,都是由
ec_hdr,vid_hdr 两个on-flash 结构 ,做判断依据的

ubi_attach_mtd_dev =>attach_by_scanning => ubi_scan
执行完成回到attach_by_scanning 可以通过si (struct ubi_scan_info)
获得的信息去填充ubi的一些数据:
ubi->bad_peb_count = si->bad_peb_count;
ubi->max_ec = si->max_ec;
ubi->mean_ec = si->mean_ec;

 

ubi_attach_mtd_dev =>attach_by_scanning =>ubi_read_volume_table
接下来ubi_read_volume_table 过程中会涉及到 ubi_vtbl_record

struct ubi_vtbl_record {
        /* 卷上保留 物理块数*/
    __be32 reserved_pebs;
    __be32 alignment;
    /* 每个物理为对齐而保留的byte数*/
    __be32 data_pad;
    /* static or dynamic */
    __u8 vol_type;
    /* 卷记录开始更新,还没完成标志*/
    __u8 upd_marker;
    __be16 name_len;
    __u8 name[UBI_VOL_NAME_MAX+1];
    __u8 flags;
    __u8 padding[23];
    __be32 crc;
} __attribute__ ((packed));

这个结构也是个 on flash 结构,被写到一个叫UBI_LAYOUT_VOLUME_ID
的内部卷,
ubi_read_volume_table 有两个分支,首先执行ubi_scan_find_sv
查找UBI_LAYOUT_VOLUME_ID的scan volume (ubi_scan_volume)
1. 如果没找到 create_empty_lvol=>create_vtbl创建
  最先设置一个 empty_vtbl_record,只是设置他CRC为0xf116c36b,代表
  是个empty record
  然后ubi_scan_get_free_peb 获得free peb ,写入layout volume 头
  vid_hdr->vol_type = UBI_VID_DYNAMIC;
  vid_hdr->vol_id = cpu_to_be32(UBI_LAYOUT_VOLUME_ID);
 
  然后将整个empty vtbl ,  最大为128slots,写入,
  最后执行ubi_scan_add_used,将前面分配并写入的peb 添加到ubi_scan_info
  (如果有旧的ubi_scan_leb,这个过程会根据 1个leb 对应2个peb 情况,
   释放old seb 对应的原来那个peb )
  
2. 如果layout 卷已经存在,执行process_lvol  ,
   对这个卷用ubi_rb_for_each_entry(rb, seb, &sv->root, u.rb)
   进行编历,(layout volume 只包含vulome table及其备份)
   所以应该是两个leb(logical eraseblock),
   使用ubi_io_read_data,将leb0,leb1 ,全部读出来,经常检查,如果有
   需要,就进行修复
  
   注释中提到这两个块发生变化时的保存流程:
   * a. erase LEB 0;
   * b. write new data to LEB 0;
   * c. erase LEB 1;
   * d. write new data to LEB 1.
  

 

处理过ubi_vtbl_record之后进行init_volumes
ubi_attach_mtd_dev = >attach_by_scanning =>ubi_read_volume_table
  =>init_volumes  

在  init_volumes 过程中主要就是把 获得的
 on flash 的 volume 信息 (ubi_vtbl_record) 去初始化
 ubi_device中的
 struct ubi_volume *volumes[UBI_MAX_VOLUMES+UBI_INT_VOL_COUNT];
下面看下 ubi_volume 结构:

struct ubi_volume {
    struct device dev;
    struct cdev cdev;
    struct ubi_device *ubi;
    int vol_id;
    int ref_count;
    int readers;
    int writers;
    int exclusive;

/* 卷上保留的peb */
    int reserved_pebs;
    int vol_type;
    int usable_leb_size;
/* 卷上包含数据的leb */    
    int used_ebs;
/* 卷上包含数据的最后leb的实际字节数 */
    int last_eb_bytes;
/* 卷上包含数据的总节数*/    
    long long used_bytes;
    int alignment;
    int data_pad;
    int name_len;
    char name[UBI_VOL_NAME_MAX + 1];
    
/*执行UBI_IOCVOLUP 卷更新命令时,更新的bytes
转换成不带data_pad的usable_leb_size(leb_size-data_pad)
后的块数 */
    int upd_ebs;
    
/* UBI_IOCEBCH 相关 (ubi_start_leb_change)*/    
    int ch_lnum;
    int ch_dtype;
    long long upd_bytes;
    long long upd_received;
    void *upd_buf;

/* 这个很重要,leb 到peb 的映射表 */
    int *eba_tbl;
/* 一些标志*/    
    unsigned int checked:1;
    unsigned int corrupted:1;
    unsigned int upd_marker:1;
/*卷正在被更新*/    
    unsigned int updating:1;
    
    unsigned int changing_leb:1;
/* UBI_IOCSETPROP */    
    unsigned int direct_writes:1;
};

 

下面代码显示ubi_volume的相关信息如何从vtbl(ubi_vtbl_record)中获得的:

vol->reserved_pebs = be32_to_cpu(vtbl[i].reserved_pebs);
        vol->alignment = be32_to_cpu(vtbl[i].alignment);
        vol->data_pad = be32_to_cpu(vtbl[i].data_pad);
        vol->vol_type = vtbl[i].vol_type == UBI_VID_DYNAMIC ?
                    UBI_DYNAMIC_VOLUME : UBI_STATIC_VOLUME;
        vol->name_len = be16_to_cpu(vtbl[i].name_len);
        vol->usable_leb_size = ubi->leb_size - vol->data_pad;
        memcpy(vol->name, vtbl[i].name, vol->name_len);
        vol->name[vol->name_len] = '\0';
        vol->vol_id = i;

 

接下来ubi_scan_find_sv(si, i);       
从扫描信息ubi_scan_info 中根据volume id 找到volume scan info (sv)
接下来用sv (ubi_scan_volume)的信息更新vol(ubi_volume)

vol->used_ebs = sv->used_ebs;
  vol->used_bytes =
   (long long)(vol->used_ebs - 1) * vol->usable_leb_size;
  vol->used_bytes += sv->last_data_size;
  vol->last_eb_bytes = sv->last_data_size;

 上面这些操作,循环ubi->vtbl_slots次后,排除所有empty record
(判断条件为vtbl[i].reserved_pebs)为0,是因为empty_vtbl_record
  被设置为static 全局变量) 

接下来添加 layout volume 相关的ubi_volume ,这个ubi_volume 放到
ubi->volumes数组中 ubi->vtbl_slots 下标后
(ubi->volumes[vol_id2idx(ubi, vol->vol_id)] = vol;)
最后更新 整个ubi 的reserved pebs 和avail_pebs
代码如下:

ubi->rsvd_pebs += reserved_pebs;
 ubi->avail_pebs -= reserved_pebs;

 

ubi_attach_mtd_dev = >attach_by_scanning =>ubi_read_volume_tabl
  =>check_scanning_info

check 主要是 扫描所有vtbl_slots个static  + 1个内部卷(layout),
使用ubi_scan_find_sv 查找sv(ubi_scan_volume),对于sv 存在,而该vol_id对应的
ubi_volume不存在的情况,通过 ubi_scan_rm_volume 做下面两步
 1. delete sv 's seb rb-tree (&struct ubi_scan_leb objects),
 2 .delete sv from si->volumes rb-tree
(si =>sv=>seb !!! 注意这样一个层次)

做ubi_scan_rm_volume的还有一种情况是vol->reserved_pebs 为0

 
ubi_init=>ubi_attach_mtd_dev=>attach_by_scanning
  =>ubi_wl_init_scan
 

其中数据结构: 

struct ubi_wl_entry {
    union {
/* link 到free 或者used RB tree*/    
        struct rb_node rb;
/* link 到写平衡保护队列(ubi_device 的
struct list_head pq[UBI_PROT_QUEUE_LEN];)*/        
        struct list_head list;
    } u;
    int ec; /*erase count */
    int pnum; /* phiscal block number */
};

ubi_wl_init_scan 是通过si(ubi_scan_info)来初始化,磨损平衡子系统的
1.先对si->erase 上每个peb 分配1个ubi_wl_entry ,放到ubi->lookuptbl[e->pnum]中
  以pnum 为下标,然后调度erase work (对erase list 上的seb 继续erase 操作)
  具体操作可以看下erase_worker,正真执行是在ubi_thread
 
2.对si->free list 上的每个seb, 分配并初始ubi_wl_entry, 然后将该ubi_wl_entry
  插入ubi_devic 的free RB tree

3.对si上corrupted  list 的每个seb进行处理,基本同si->erase

4.最后扫描 si->volumes,获得各sv(ubi_scan_volume),然后遍历sv 上
  挂seb(ubi_scan_leb)的RB tree,然后对每个seb分配1个ubi_wl_entry
  然后根据seb的scrub 判断是否需要擦除,如果需要,就挂到ubi_device
  的scrub RB tree 上,否则挂到used RB tree上
 
5. 执行ensure_wear_leveling,判断是否要进行写平衡处理(wear_leveling_worker)
   判断的条件是,ubi_device used RB-tree 上最左边(ec 最小的)和
   ubi_device free RB tree 上接ec近于WL_FREE_MAX_DIFF的节点
   两个node 之间ec的差值大于等于UBI_WL_THRESHOLD
  
   wear_leveling_worker 的工作就是将used 上ec 大的,copy 到 free 上ec
   小的block 上

  
关于写平衡数据结构关联图:

上图主要描述了ubi_device,ubi_volume,ubi_wl_entry
的关系,
1.ubi_device 上free,used,scrub,上根据不同情况的3个rb tree root
  各ubi_wl_entry根据其状态free ,used or scrub挂到各自的rb tree上
 
2.  ubi_device 上pq 是个保护队列,比如free 到used 的 rb tree的移动
    过程中需要保护下, 这时就先挂到pq上,具体原因可见 wl.c的
    UBI wear-leveling sub-system 部分注释
3.   lookuptbl 是个ubi_wl_entry的指针数组,为了快速查找ubi_wl_entry,
    每个ubi_wl_entry pointer都在lookuptbl数组里,以pnum 做下标
   
4.  ubi_volume 通过 ubi_device 类型指针ubi回指向ubi_device
    ubi_volume 中另一个比较重要的就是LEB 和 PEB 的映射表
     int *eba_tbl

 

ubi_init=>ubi_attach_mtd_dev=>attach_by_scanning
  =>ubi_eba_init_scan
  

这个函数作用,如其注释,就是init eraseblock Association 子系统
主要完成对ubi_volume上 int *eba_tbl (LEB->PEB mapping)的
初始化工作

1.先分配eba_tbl
2.然后对小于reserved_pebs物理块号的设置为unmap
3.遍历sv root, 获得seb lnum ,pnum ,来对eab_tbl 进行初始化

 

ubi_init=>ubi_attach_mtd_dev=>attach_by_scanning
   =>ubi_scan_destroy_si

  
   把前面为scan 所生成的si, sv,seb 全部释放,这些数据
只是为生成上面ubi_device,ubi_volume 结构中数据,所使用的中间变量
   
 
ubi_init=>ubi_attach_mtd_dev
  =>uif_init

该函数完成用户接口初始化
主要两个层次
    ubi_device
    1.init ubi device cdev 的操作为ubi_cdev_operations
      主要是ioctrl
    2.以char dev 注册 ubi device  到系统
    3.ubi_sysfs_init 实现ubi device sysfs 接口部分属性
   
    ubi_volme
     对 ubi->vtbl_slots个volum 进行
     1.volem cdev 的操作设置为ubi_vol_cdev_operations
     2.以char dev 注册 ubi volume dev  到系统
     3.volume_sysfs_init 实现ubi volume sysfs 接口部分属性


ubi_init=>ubi_attach_mtd_dev
最后ubi_attach_mtd_dev 中创建了  ubi_thread
该内核线程,用来执行ubi_device 上的works,主要就是后台擦除,和wear level
处理

 

到这里整个ubi init 基本完成,大体可以看出
 每个mtd partition 可以attach 到一个ubi device上,
 在每个ubi device上又可以创建很多ubi volume,
 而每个ubi volume又被作为一个mtd device 保存于mtd table 中    

(上面提到的写平衡和磨损平衡是同一个意思)

  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值