Linux文件系统一(用户文件系统fuse)

创作人QQ:851301776,邮箱:lfr890207@163.com
        欢迎大家一起技术交流,本博客主要是自己学习的心得体会,只为每天进步一点点!

个人座右铭:
         1.没有横空出世,只要厚积一定发。
         2.你可以学历不高,你可以不上学,但你不能不学习
 

一、fuse描述

        1.fuse是什么?一个用户态的进程,但是是守护进程启动之后一直运行在后台。

        2.fuse主要作用:监控某个文件或者文件夹的变化,可以对其他进程在此文件夹或者文件中写入或者读取的东西增加自定义信息。

        3.fuse对应的库:libfuse(Releases · libfuse/libfuse · GitHub)

二、fuse概述

        FUSE (用户态文件系统)是一个实现在用户空间的文件系统框架,通过 FUSE内核模块的支持,使用者只需要根据 fuse 提供的接口实现具体的文件操作就可以实现一个文件系统。

三、fuse的组成及以及功能实现

        FUSE 由三个部分组成,FUSE 内核模块,FUSE 库以及一些挂在工具。FUSE 内核模块实现了和 VFS 的对接,它看起来像一个普通的文件系统模块,另外 FUSE 内核模块实现了一个可以被用户空间进程打开的设备,当 VFS 发来文件操作请求之后,它将该请求转化为特定格式,并通过设备传递给用户空间进程,用户空间进程在处理完请求后,将结果返回给 FUSE 内核模块,内核模块再将其还原为Linux Kernel 需要的格式,并返回给 VFS。

         FUSE 库负责和内核空间的通信,它接收来自/dev/fuse 的请求,并将其转化为一系列的函数调用,并将结果写回到/dev/fuse。

四、FUSE运行结构图

 

五、fuse库接口

struct fuse_operations {

        int (*getattr) (const char *, struct stat *, struct fuse_file_info *fi);

        int (*readlink) (const char *, char *, size_t);

        int (*mknod) (const char *, mode_t, dev_t);

        int (*mkdir) (const char *, mode_t);

        int (*unlink) (const char *);

        int (*rmdir) (const char *);

        int (*symlink) (const char *, const char *);

        int (*rename) (const char *, const char *, unsigned int);

        int (*link) (const char *, const char *);

        int (*chmod) (const char *, mode_t, struct fuse_file_info *fi);

        int (*chown) (const char *, uid_t, gid_t, struct fuse_file_info *fi);

        int (*truncate) (const char *, off_t, struct fuse_file_info *fi);

        int (*open) (const char *, struct fuse_file_info *);

        int (*read) (const char *, char *, size_t, off_t, struct fuse_file_info *);

        int (*write) (const char *, const char *, size_t, off_t, struct fuse_file_info *);

        int (*statfs) (const char *, struct statvfs *);

        int (*flush) (const char *, struct fuse_file_info *);

        int (*release) (const char *, struct fuse_file_info *);

        int (*fsync) (const char *, int, struct fuse_file_info *);

        int (*setxattr) (const char *, const char *, const char *, size_t, int);

        int (*getxattr) (const char *, const char *, char *, size_t);

        int (*listxattr) (const char *, char *, size_t);

        int (*removexattr) (const char *, const char *);

        int (*opendir) (const char *, struct fuse_file_info *);

        int (*readdir) (const char *, void *, fuse_fill_dir_t, off_t,

        struct fuse_file_info *, enum fuse_readdir_flags);

        int (*releasedir) (const char *, struct fuse_file_info *);

        int (*fsyncdir) (const char *, int, struct fuse_file_info *);

        void (init) (struct fuse_conn_info *conn,struct fuse_config *cfg);

        void (*destroy) (void *);

        int (*access) (const char *, int);

        int (*create) (const char *, mode_t, struct fuse_file_info *);

        int (*lock) (const char *, struct fuse_file_info *, int cmd,struct flock *);

        int (*utimens) (const char *, const struct timespec tv[2], struct fuse_file_info *fi);

        int (*bmap) (const char *, size_t blocksize, uint64_t *idx);

        int (*ioctl) (const char *, int cmd, void *arg,

        struct fuse_file_info *, unsigned int flags, void *data);

        int (*poll) (const char *, struct fuse_file_info *,

        struct fuse_pollhandle *ph, unsigned *reventsp);

        int (*write_buf) (const char *, struct fuse_bufvec *buf, off_t off,struct fuse_file_info *);

        int (*read_buf) (const char *, struct fuse_bufvec **bufp,

        size_t size, off_t off, struct fuse_file_info *);

        int (*flock) (const char *, struct fuse_file_info *, int op);

        int (*fallocate) (const char *, int, off_t, off_t,struct fuse_file_info *);

};

六、fuse库编译

1.安装:

(1)./configure --prefix=/usr

(2)make

(3)make install

2.加载驱动

 加载fuse.ko模块“modprobe fuse”,然后在切换到example目录下编译fusexmp.c

七、内核源码解析

        内核源码在:内核源码/fs/fuse

     1.fuse内核模块被加载时,以下初始化例程会被调用,见fuse_init函数<inode.c>。

        (1)fuse_fs_init(); 注册fuse文件系统,创建fuse_inode高速缓存。

        (2)fuse_dev_init(); 创建fuse_req高速缓存,加载fuse设备驱动,用于用户空间与内核空间交换信息。

        (3)fuse_sysfs_init(); 在/sys/fs目录下增加fuse节点,在fuse节点下增加connections节点。

        (4)fuse_ctl_init(); 注册fuse控制文件系统

        2.fuse内核模块被卸载时,执行对应的清理工作,见fuse_exit函数<inode.c>

        (1)fuse_ctl_cleanup(); 注销fuse控制文件系统

        (2)fuse_sysfs_cleanup(); 移除fuse、connections节点。

        (3)fuse_fs_cleanup(); 注销fuse文件系统,释放fuse_inode高速缓存。

        (4)fuse_dev_cleanup(); 注销fuse设备驱动程序,释放fuse_req高速缓存。

        3. fuse_conn<fuse_i.h>代表一个fuse连接,当用户文件系统被挂载时生成该结构,当文件系统被卸载时释放该结构,其主要用于管理各个请求队列,内核会为所有挂载的文件系统维护一个fuse_conn的链表(fuse文件系统可能会被挂载多次)。

        4.fuse_conn的connected字段用于表示连接的状态,成功挂载后为1,当文件系统被卸载,连接被中断或是设备驱动被释放后,该字段为0,此时这个connection(挂载的文件系统)不能提供正常服务。在fuse_request_send中会检查该字段,只有连接正常fuse文件系统才会发送请求。        

        5.fuse中每个请求用一个fuse_req<fuse_i.h>的结构表示,该结构中包含fuse请求的输入输出参数,请求对象的inode、file等。

        6.每个fuse的输入、输出参数都支持三个参数,见fuse_in、fuse_out结构的定义<fuse_i.h>,参数以<*value, size>的形式传递,当填充fuse_req结构时,根据请求类型,以及请求参数,设置fuse_in的参数个数(numargs),并将参数填充到args数组中,同时设置fuse_out输出参数的个数,并将存放输出结果的地址(局部变量outarg)填充到args数组中。

        7.当fuse文件系统设置好请求输入输出参数之后,所有接口最后都会调用fuse_request_send将代表本次请求的fuse_req结构的状态标志设置为FUSE_REQ_PENDING,将请求加到fuse_conn的pending链表中,并调用request_wait_answer等待请求完成(等待队列被唤醒后,需要检查请求状态是否为FUSE_REQ_FINISHED)。当本次请求被响应后,结果已经被存放在局部变量outarg中,fuse进行相应的处理即可向上层返回结果。

        8.每个请求fuse_req结构中包含一个wait_queue_head_t的waitq字段,每个请求在被发出之后,它首先会唤醒fuse_conn的waitq等待队列,告诉用户态守护进程有请求达到;然后其会调用wait_event_interruptible在req的waitq上睡眠等待FUSE_REQ_FINISHED条件变为真。

        9.fuse设备驱动是一个简单块设备驱动程序,用于fuse在用户态和内核态之间交换数据,fuse包含一个用户空间的守护程序,其一直循环运行,主要任务是调用read从fuse设备上读取请求,当没有请求时,它会在fuse_conn的waitq上睡眠等待(对应上一段中请求发出后唤醒fuse_conn的waitq等待队列),当有请求是其从fuse_conn的pengding队列中取出最前的一个请求(对应上一段中请求发出后加到fuse_conn的pending链表),并将该请求移动到processing队列中,守护进程将fuse_req的相关信息读到用户态后,根据请求表示调用用户态实现的回调函数,并将结果通过fuse_dev_write写到fuse设备驱动,用户态请求完成后,从processing队列中找到对应的fuse_req,将结果拷贝到fuse_req的out参数中,并将fuse_req的state设置为FUSE_REQ_FINISHED,然后唤醒fuse_req的waitq。此时,fuse_req被处理完毕,fuse文件系统向上层返回。

 八、剖析FUSE程序运行过程的3个关键步骤

        a.fuse加载模块

        b.mount和open过程

        c.对文件write

1.fuse下write的一般流程

(1)客户端在mount目录下面,对一个regular file调用write, 这一步是在用户空间执行

(2)write内部会调用虚拟文件系统提供的一致性接口vfs_write*

(3)根据FUSE模块注册的file_operations信息,vfs_write会调用fuse_file_aio_write,将写请求放入fuse connectionrequest pending queue, 随后进入睡眠等待应用程序reply

(4)用户空间的libfuse有一个守护进程通过函数fuse_session_loop轮询杂项设备/dev/fuse, 一旦request queue有请求即通过fuse_kern_chan_receive接收

(5)fuse_kern_chan_receive通过read读取request queue中的内容,read系统调用实际上是调用的设备驱动接口fuse_dev_read*

(6)在用户空间读取并分析数据,执行用户定义的write操作,将状态通过fuse_reply_write返回给kernel

(7)fuse_reply_write调用VFS提供的一致性接口vfs_write*

(8)vfs_write最终调用fuse_dev_write将执行结果返回给第3步中等待在waitq的进程,此进程得到reply后,write返回

2.数据结构

(1)内核部分

a.struct fuse_conn:每一次mount会实例化一个struct fuse_connfuse connection, 它代表了用户空间和内核的通信连接。fuse connection维护了包括pending list, processing listio list在内的request queuefuse connection通过这些队列管理用户空间和内核空间通信过程。

b.struct fuse_req:每次执行系统调用时会生成一个struct fuse_req, 这些fuse_req依据state被组织在不同的队列中,struct fuse_conn维护了这些队列.

c.struct file: 存放打开文件与进程之间进行交互的有关信息,描述了进程怎样与一个打开的文件进行交互,这类信息仅当进程访问文件期间存在于内核内存中。

d.struct inode:文件系统处理文件所需要得所有信息都放在一个名为inode(索引节点)的数据结构中。文件名可以随时更改,但是索引节点对文件是唯一的,并且随着文件的存在而存在。

e.struct file_operation:定义了可以对文件执行的操作。

(2)用户空间部分

struct fuse_req:这个结构和上文中内核的fuse_req同名,有着类似的作用,但是数据成员不同。

struct fuse_session:定义了客户端管理会话的结构体,包含了一组对session可以执行的操作。

struct fuse_chan:定义了客户端与FUSE内核连接通道的结构体,包含了一组对channel可以执行的操作。

struct fuse_ll_ops:结构的成员为一个函数指针func和命令名字符串name,内核中发过来的每一个request最后都映射到以此结构为元素的数组中

 4.fuse模块加载

        FUSE内核模块需要在用户空间使用insmod或者modprobe加载。它们通过系统调用init_module启动加载过程,注册过程比较简单,包括如下步骤:

a.创建高速缓存结构fuse_inode_cachep

b.遍历file_systems链表,如果未注册,则将fuseblk_fs_type链到file_systems链表尾部

c.遍历file_systems链表,如果未注册,则将fuse_fs_type链到file_systems链表尾部

d.创建fuse_kobjconnections_kobj两个kobject

e.遍历file_systems链表,如果未注册,则将fuse_ctl_fs_type链到file_systems链表尾部

模块成功加载以后,以下接口被注册

static struct file_system_type fuseblk_fs_type = { //块设备

    .owner    = THIS_MODULE,

    .name     = "fuseblk",

    .mount    = fuse_mount_blk,

    .kill_sb  = fuse_kill_sb_blk,

    .fs_flags = FS_REQUIRES_DEV | FS_HAS_SUBTYPE,

};

static struct file_system_type fuse_fs_type = {

    .owner    = THIS_MODULE,

    .name     = "fuse",

    .fs_flags = FS_HAS_SUBTYPE,

    .mount    = fuse_mount,

    .kill_sb  = fuse_kill_sb_anon,

};

const struct file_operations fuse_dev_operations = {

    .owner        = THIS_MODULE,

    .llseek       = no_llseek,

    .read         = do_sync_read,

    .aio_read     = fuse_dev_read,

    .splice_read  = fuse_dev_splice_read,

    .write        = do_sync_write,

    .aio_write    = fuse_dev_write,

    .splice_write = fuse_dev_splice_write,

    .poll         = fuse_dev_poll,

    .release      = fuse_dev_release,

    .fasync       = fuse_dev_fasync,

};

static struct miscdevice fuse_miscdevice = {

    .minor = FUSE_MINOR,

    .name  = "fuse",

    .fops  = &fuse_dev_operations,

};
static struct file_system_type fuseblk_fs_type = { //块设备

    .owner    = THIS_MODULE,

    .name     = "fuseblk",

    .mount    = fuse_mount_blk,

    .kill_sb  = fuse_kill_sb_blk,

    .fs_flags = FS_REQUIRES_DEV | FS_HAS_SUBTYPE,

};

 static struct file_system_type fuse_fs_type = {

    .owner    = THIS_MODULE,

    .name     = "fuse",

    .fs_flags = FS_HAS_SUBTYPE,

    .mount    = fuse_mount,

    .kill_sb  = fuse_kill_sb_anon,

};

 const struct file_operations fuse_dev_operations = {

    .owner        = THIS_MODULE,

    .llseek       = no_llseek,

    .read         = do_sync_read,

    .aio_read     = fuse_dev_read,

    .splice_read  = fuse_dev_splice_read,

    .write        = do_sync_write,

    .aio_write    = fuse_dev_write,

    .splice_write = fuse_dev_splice_write,

    .poll         = fuse_dev_poll,

    .release      = fuse_dev_release,

    .fasync       = fuse_dev_fasync,

};

 static struct miscdevice fuse_miscdevice = {

    .minor = FUSE_MINOR,

    .name  = "fuse",

    .fops  = &fuse_dev_operations,

};

5.mount和open过程

        FUSE模块加载注册了fuseblk_fs_typefuse_fs_type两种文件类型,默认情况下使用的是fuse_fs_typemount* 函数指针被初始化为fuse_mount,fuse_mount实际调用mount_nodev,它主要由如下两步组成:

(1)sget(fs_type)搜索文件系统的超级块对象(super_block)链表(type->fs_supers),*如果找到一个与块设备相关的超级块,则返回它的地址。否则,分配并初始化一个新的超级块对象,把它插入到文件系统链表和超级块全局链表中,并返回其地址。

(2)fill_super(此函数由各文件系统自行定义):* 这个函数式各文件系统自行定义的函数,它实际上是fuse_fill_super。一般fill_super会分配索引节点对象和对应的目录项对象, 并填充超级块字段值,另外对于fuse还需要分配fuse_conn,fuse_req。需要说明的是,它在底层调用了fuse_init_file_inodefuse_file_operationsfuse_file_aops分别初始化inode->i_fopinode->i_data.a_ops

static const struct file_operations fuse_file_operations = {

    .llseek         = fuse_file_llseek,

    .read           = do_sync_read,

    .aio_read       = fuse_file_aio_read,

    .write          = do_sync_write,

    .aio_write      = fuse_file_aio_write,

    .mmap           = fuse_file_mmap,

    .open           = fuse_open,

    .flush          = fuse_flush,

    .release        = fuse_release,

    .fsync          = fuse_fsync,

    .lock           = fuse_file_lock,

    .flock          = fuse_file_flock,

    .splice_read    = generic_file_splice_read,

    .unlocked_ioctl = fuse_file_ioctl,

    .compat_ioctl   = fuse_file_compat_ioctl,

    .poll           = fuse_file_poll,

    .fallocate      = fuse_file_fallocate,

};

 

static const struct address_space_operations fuse_file_aops  = {

    .readpage       = fuse_readpage,

    .writepage      = fuse_writepage,

    .launder_page   = fuse_launder_page,

    .readpages      = fuse_readpages,

    .set_page_dirty = __set_page_dirty_nobuffers,

    .bmap           = fuse_bmap,

    .direct_IO      = fuse_direct_IO,

};

open系统调用底层实现相当复杂,它的主要工作是实例化file对象。file->f_op就是在open中被赋值为inode->i_fop,这一过程读者可以在fs/open.c中的do_entry_open函数中找到。如上所述,inode->i_fop已经被fuse_init_file_inode初始化为fuse_file_operations

至此,普通文件和设备文件的操作接口都已成功初始化。

6.FUSE用户空间流程

        FUSE在用户空间提供了fuse userspace library和mount /unmount。fuse usespace library提供了一组API供用户开发用户空间文件系统。用户要做的就是实现fuse_operations 或fuse_lowlevel_ops定义的操作,这两个结构类似于VFS中的struct file_operations。

        mount工具fusermount用于挂载用fuse实现的文件系统。

        用户在使用fuse的时候有两种开发模式:一种是high-level模式,此模式下fuse的入口函数为fuse_main,它封装了一系列初始化操作,使用简单,但是不灵活。另一种是low-level模式,用户可以利用fuse提供的底层函数灵活开发应用程序。

        需要说明的是high-level模式其实是对low-level的封装,因此这里分析lowlevel模式。

1.调用fuse_mount实例化struct fuse_chan为ch, 将指定目录mount到挂载点

2.实例化struct fuse_session为se,并且将se和ch关联

3.进入循环,从/dev/fuse读取数据,处理以后执行响应的操作

 展示了fuse_mount函数内部流程:

1. 确保打开的文件描述符至少大于2

2. 分析并检查用户传入的参数

3. 打开/dev/fuse 得到fd,用户空间与内核通过/dev/fuse通信

4. mount源目录到挂载点

5. 用fd实例化struct fuse_chan为ch

6. 返回ch

        图6展示了fuse_mount_compat25内部细节,进入循环以后,函数fuse_session_receive_buf实际通过fuse_ll_receive_buf从/dev/fuse中读取数据,其通过fbuf返回。

        fuse_ll_receive_buf是通过read或者splice系统调用从内核request队列中读取数据。函数fuse_session_process_buf实际通过fuse_ll_process_buf处理数据,fuse_ll_process_buf会根据数据类型最后执行用户定义的操作fuse_ll_ops[in->opcode].func(req, in->nodeid, inarg)。

        执行完用户定义的操作以后需要向内核返回执行结果,fuse提供了一组类似fuse_reply_XXX的API, 这些API最后实际通过系统调用writev将结果传入内核。

7.FUSE内核部分流程

FUSE在内核空间执行的部分主要包括FUSE模块加载以及杂项设备驱动。模块加载过程已经在第4节介绍,这一节主要描述从request队列读写请求的流程。

FUSE设备驱动程序本质上是一个生产者——消费者模型。生产者为用户在挂载目录下对普通文件(regular file)执行的系统调用,每一次系统调用会产生一个request然后将去放入pending list。pending list能存放的元素个数只和系统内存有关;消费者为用户对设备文件/dev/fuse或者/dev/fuseblk的read,这一操作会去pending list或interrupt list取request,当list为空时,进程主动schedule让出CPU。

request结构的细节在第3节已经介绍,此处不赘述。enmu fuse_req_state定义了request的6种状态,其含义分别为:

      FUSE_REQ_INIT:请求被初始化

      FUSE_REQ_PENDING:请求挂起待处理

      FUSE_REQ_READING:请求正在读

      FUSE_REQ_SENT:请求被发送

      FUSE_REQ_WRITING:请求正在写

      FUSE_REQ_FINISHED:请求已经完成 

         图7是在mount目录下面执行write以后触发的一个函数调用序列,图中省略了VFS层的函数调用。 fuse_file_aio_write是在mount过程中注册到fuse_file_operations.aio_write的函数指针,它会调用fuse_perform_write,fuse_perform_write调用get_fuse_conn得到struct fuse_conn实例fc,它保存在struct super_block的私有数据成员中s_fs_info中,而struct super_block是struct inode的一个成员。接下来是循环从用户空间拷贝数据到内核,数据实际保存在struct pages中,内核fuse_req保存了pages指针,然后调用fuse_send_write_pages。

         Fuse_send_write_pages调用会等待脏数据写回到磁盘上,然后调用fuse_write_fill将包括操作码FUSE_WRITE在内的信息写入request。

        随后fuse_request_send(fc, req),它先通过fuse_get_unique获取唯一请求号,请求号是一个64位无符号整数,请求号从1开始随请求依次递增。然后调用queue_request(fc, req),它主要完成4件事情:

        1.将request->list插入fc维护的pending链表尾部

        2.置req->state为FUSE_REQ_PENDING

        3.wake_up唤醒等待队列fc->waitq

        4.kill_fasync异步通知用户进程数据到达

        从queue_request返回以后调用request_wait_answer:进程被投入睡眠,等待请求完成(wait_event(req->state == FUSE_REQ_FINISHED))。  

        如果用户程序处理完了请求,它会reply,进程被唤醒,到此可以向上层调用返回处理结果(错误码或者写入字节数)。

        在第6节我们提到了用户空间有个daemon进程会循环read设备文件/fuse/dev以便处理内核请求,图9展示了该read调用触发的函数调用序列。 

        从第4节可知,FUSE模块加载过程注册了对设备文件/dev/fuse的操作接口fuse_dev_operations。由此可知,read底层实际调用的是fuse_dev_read

        fuse_dev_read首先通过fuse_get_conn获得struct fuse_conn的实例fc,通过fuse_copy_init为struct fuse_copy_state分配内存并将其实例化。主要的数据读取在fuse_dev_do_read中分4步完成:

1.request_wait:在挂起的列表上等待一个请求到达:

        (1).DECLARE_WAITQUEUE(wait, current): 创建等待队列项,并将其初始化为current

        (2).add_wait_queue_exclusive(&fc->waitq, &wait): 将wait加入fc->waitq,当有请求发送到        FUSE文件系统时,这个等待队列上的进程会被唤醒

        (3).如果没有request,一直循环检查pending list和interrupt list, 直到有请求;如果有请求则将state设置为TASK_RUNNING

        (4).将wait从等待队列中移除

2.list_entry(fc->pending.next, struct fuse_req, list):从fc->pending.next中取出request,req->state状态设为FUSE_REQ_READING, 

3. 将req->list移到fc->io

4. fuse_copy_one:将数据拷贝到struct fuse_copy_state的buf中(此buf指针指向应用层的void *buf),   返回。

阅读代码时需要注意:fuse_dev_read:struct fuse_copy_state成员write为1;fuse_dev_write:  struct fuse_copy_state成员write为0。

用户读取request,分析并执行以后需要调用fuse_write_reply回复内核,这个函数最终调用write写/dev/fuse。图10是write触发的函数调用序列。

        write前两步和read类似即获取fc(struct fuse_conn)和实例化cs(struct fuse_copy_state)实际的写数据操作在fuse_dev_do_write中执行,可以分为7步完成数据的写入:

1.fuse_copy_one(cs, &oh, sizeof(oh)): 将数据从cs(struct fuse_copy_state)拷贝到oh(struct fuse_out_header)

2.request_find(fc, oh.unique),根据unique id在fc(fuse_conn)中找到相应的request

3.设置req->state为FUSE_REQ_WRITING

4.将req->list移到fc->io队列

5.req被赋予cs->req

6.copy_out_args(cs, &req->out, nbytes):从cs(struct fuse_copy_state)中拷贝参数到req->out

7.request_end:请求处理完成,设置req->state=FUSE_REQ_FINISHED, 唤醒等待在waitq的进程wake_up(&req->waitq)

九、参考掩码

不上

  • 3
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

QQ851301776

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

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

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

打赏作者

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

抵扣说明:

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

余额充值