第四课 字符设备驱动

字符设备驱动的结构:
在这里插入图片描述
不使用设备驱动模型实现字符设备驱动的大致流程如下:
字符设备驱动程序大致流程

每个字符设备都对应一个cdev结构体:

struct cdev
{
	struct kobject kobj;
	struct module *owner;        /* 模块所有者,一般为THIS_MODULE */
	struct file_operations *ops; /* 文件操作结构体,定义了字符设备驱动提供给虚拟文件系统的接口函数 */
	struct list_head list;
	dev_t dev;                   /* 设备号,32位,高12位是主设备号,低20位是次设备号 */
	unsigned int count;
};

dev_t中获得主设备号和次设备号,以及通过主设备号和次设备号生成dev_t的方法:

MAJOR(dev_t dev);
MINOR(dev_t dev);
MKDEV(int major, int minor);

操作cdev结构体的函数:

/* 清零cdev;初始化cdev的链表list;初始化kobj;建立cdev与file_operations之间的关系 */
void cdev_init(struct cdev *, struct file_operations *);
/* 动态申请一个cdev内存;初始化该cdev对象的链表list和kobj */
struct cdev *cdev_alloc(void);
void cdev_put(struct cdev *p);
/* 向系统中添加一个cdev,完成字符设备的注册 */
/* dev-设备的第一个设备号 
*  count-该设备对应的连续次设备号的数量
*/
int cdev_add(struct cdev *p, dev_t dev, unsigned count);/* 通常在模块加载函数中调用 */
/* 从系统中删除一个cdev,完成字符设备的注销 */
void cdev_del(struct cdev *);/* 通常在模块卸载函数中调用 */

分配和释放设备号:

/* 
*从用户指定的设备号开始申请count个设备号,存在设备号重复的冲突
*from:用户指定的起始设备号,必须包含主设备号,次设备号一般从0开始
*count:用户申请的设备号数量,主设备号不变,次设备号会从0到count
*name:设备或驱动名称
*/
int register_chrdev_region(dev_t from, unsigned count, const char *name);

/* 
*申请count个设备号,但主设备号部分由系统自动分配可用的,可避开设备号重复的冲突
*dev:申请到的主设备号以及第一个次设备号组成的设备号会存放到该内存中,内存由调用者申请
*baseminor:第一个次设备号,一般从0开始
*count:次设备号数量
*name:设备或驱动名称
*/
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)

/*
*释放由上述两个函数申请的设备号
*from:从哪个设备号开始释放
*count:需要释放的设备号数量
*/
void unregister_chrdev_region(dev_t from, unsigned count)

/* 
*申请256个设备号,主设备号由用户指定,次设备号为256个,并分配和初始化cdev内存。与unregister_chrdev成对使用
*major:用户指定的主设备号
*name:设备名
*fops:file_operations结构体指针,会绑定到cdev上
*/
int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops);

/* 
*注销申请的设备号,并注销cdev。与register_chrdev成对使用
*major:用户指定的主设备号
*name:设备名
*/
void unregister_chrdev(unsigned int major, const char *name);

file_operations结构体:
该结构体是字符设备驱动程序的主体内容,字符设备驱动主要是实现这个结构体中的各个成员函数。

struct file_operations {
	struct module *owner;/* 所属模块,一般为THIS_MODULE */
	loff_t (*llseek) (struct file *, loff_t, int);/* 调整文件当前读写位置,返回新位置 */
	ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);/* 从设备中读取数据,成功时返回读取的字节数 */
	ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);/* 向设备中写数据,成功时返回写入的字节数 */
	ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);/* 异步读 */
	ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);/* 异步写 */
	int (*readdir) (struct file *, void *, filldir_t);
	unsigned int (*poll) (struct file *, struct poll_table_struct *);/* 用于询问设备是否可被非阻塞地立即读写,用于用户空间poll和select */
	long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);/* 提供设备相关控制命令的实现,对应用户空间fcntl和ioctl */
	long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
	int (*mmap) (struct file *, struct vm_area_struct *);/* 将设备内存映射到进程的虚拟地址空间,对帧缓冲设备如LCD有很大作用,应用程序可直接访问帧缓冲而无须再内核和应用间进行内存复制 */
	int (*open) (struct inode *, struct file *);/* 打开设备 */
	int (*flush) (struct file *, fl_owner_t id);
	int (*release) (struct inode *, struct file *);/* 释放设备 */
	int (*fsync) (struct file *, int datasync);
	int (*aio_fsync) (struct kiocb *, int datasync);
	int (*fasync) (int, struct file *, int);
	int (*lock) (struct file *, int, struct file_lock *);
	ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
	unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
	int (*check_flags)(int);
	int (*flock) (struct file *, int, struct file_lock *);
	ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
	ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
	int (*setlease)(struct file *, long, struct file_lock **);
	long (*fallocate)(struct file *file, int mode, loff_t offset, loff_t len);
};

在实现file_operationsreadwrite函数时,需要注意:用户空间不能直接访问内核空间的内存,需通过一些函数完成用户空间缓冲区到内核空间的复制,以及内核空间到用户空间缓冲区的复制。

/*
*内核空间数据复制到用户空间
*to:用户空间内存地址
*from:内核空间地址
*n:复制的字节数
*/
unsigned long copy_to_user(void *to, const void __user *from, unsigned long n);

/*
*用户空间数据复制到内核空间
*to:内核空间内存地址
*from:用户空间地址
*n:复制的字节数
*/
unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);

若复制的内存是简单类型,如charintlong等,则可用如下函数:

get_user(val, ptr);/* 从用户空间内存地址ptr复制数据到内核的val变量中 */
put_user(val, ptr);/* 将内核的val变量的值写入用户空间内存地址ptr中 */

int val; /* 内核空间整型变量 */
get_user(val, (int *)arg); /* arg是用户空间地址 */
put_user(val, (int *)arg); /* arg是用户空间地址 */

内核空间虽然可以访问用户空间的缓冲区,但在访问之前,一般需要先检查该缓冲区的合法性,即传入的缓冲区是否确实属于用户空间。在内核空间与用户空间的界面处,内核检查用户空间缓冲区的合法性十分重要,可避免安全漏洞。上面4个函数内部均已做了合法性检查。

/*
*type:VERIFY_READ 校验可读权限;VERIFY_WRITE 校验可写权限
*ptr:用户空间缓冲区地址
*size:用户空间缓冲区大小
*/
access_ok(type, ptr, size);

实现globalmem虚拟设备实例:
globalmem是一个有全局内存的虚拟设备,该设备驱动中会分配一片大小为4KB的内存空间,并在驱动中提供针对该片内存的读写、控制、定位函数,用户空间进程可通过系统调用操作这片内存的内容。
每次调用file_operations结构体中的成员函数时,都会传入struct file指针,因此在open函数中可将设备结构体struct globalmem_dev实例赋给struct file.private_data指针,可解决有多个globalmem设备访问的问题。

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <linux/slab.h>

#define GLOBALMEM_SIZE  (0x1000)          /* 全局缓存的内存大小,单位字节 */
#define GLOBALMEM_MAJOR (230)             /* 主设备号 */
#define GLOBALMEM_DEVNAME "globalmem_dev" /* 设备名称 */

/* 设备结构体 */
struct globalmem_dev
{
    struct cdev cdev; /* 字符设备结构体 */
    dev_t  dev_num;   /* 设备号 */
    unsigned char mem[GLOBALMEM_SIZE]; /* 全局缓存内存 */
};
struct globalmem_dev* globalmem_devp = NULL;


/*
文件位置调整
*/
static loff_t globalmem_llseek (struct file *fp, loff_t offset, int origin_pos)
{
    loff_t ret = 0;
    printk(KERN_INFO "call %s\r\n", __func__);

    switch (origin_pos)
    {
        case 0:/* 从起始位置开始 */
            /* 偏移不能小于0 */
            if (0 > offset)
            {
                ret = -EINVAL;
                break;
            }

            /* 偏移不能超过缓存区大小 */
            if (offset > GLOBALMEM_SIZE)
            {
                ret = -EINVAL;
                break;
            }

            /* 调整f_pos指针 */
            fp->f_pos = offset;
            ret = fp->f_pos;

            break;

        case 1:/* 从当前位置开始 */
            /* 当前位置+偏移不能超过缓存区大小 */
            if ((fp->f_pos + offset) > GLOBALMEM_SIZE)
            {
                ret = -EINVAL;
                break;
            }

            /* 当前位置+偏移不能小于0 */
            if (0 > (fp->f_pos + offset))
            {
                ret = -EINVAL;
                break;
            }

            /* 调整f_pos指针 */
            fp->f_pos += offset;
            ret = fp->f_pos;
            break;
        default:
            ret = -EINVAL;
            break;
    }

    return ret;
}

/* 
从内核空间读取数据到用户空间

 */
static ssize_t globalmem_read (struct file *fp, char __user *buf, size_t size, loff_t *ppos)/* ppos为读取位置,读取完后会返回最新位置 */
{
    struct globalmem_dev* devp = fp->private_data;
    loff_t pos = *ppos;
    size_t count = size;  /* 实际可以读取的字节数 */
    int status = 0;

    printk(KERN_INFO "call %s\r\n", __func__);

    /* 越界判断 */
    /* 读取起始位置已超过缓存区大小 */
    if (pos > GLOBALMEM_SIZE)
    {
        return -EFAULT;
    }

    /* 读取起始位置+读取字节数已超过缓存区大小,就只读取起始位置至缓存区末尾的数据 */
    if ((pos + size) > GLOBALMEM_SIZE)
    {
        count = GLOBALMEM_SIZE - pos;
    }

    status = copy_to_user(buf, devp->mem + pos, count);
    if (0 != status)
    {
        printk(KERN_INFO "read failed\r\n");
        return -EFAULT;
    }

    *ppos += count;

    return count;
}

/*
从用户空间写输入到内核空间
*/
static ssize_t globalmem_write (struct file *fp, const char __user *buf, size_t size, loff_t *ppos)
{
    struct globalmem_dev* devp = fp->private_data;
    loff_t pos = *ppos;
    size_t count = size;  /* 实际可以写的字节数 */
    int status = 0;

    printk(KERN_INFO "call %s\r\n", __func__);

    /* 越界判断 */
    if (pos > GLOBALMEM_SIZE)
    {
        return -EFAULT;
    }

    /* 写的起始位置+写入字节数已超过缓存区大小,就只写起始位置至缓存区末尾的数据 */
    if ((pos + size) > GLOBALMEM_SIZE)
    {
        count = GLOBALMEM_SIZE - pos;
    }

    status = copy_from_user(devp->mem + pos, buf, count);
    if (0 != status)
    {
        printk(KERN_INFO "write failed\r\n");
        return -EFAULT;
    }

    *ppos += count;

    return count;
}

/* 
打开
 */
static int globalmem_open (struct inode *inodep, struct file *fp)
{
    fp->private_data = globalmem_devp; /* 将globalmem_devp对象保存到file结构体的private_data,其他操作函数可以使用该对象了 */
    printk(KERN_INFO "call %s\r\n", __func__);
    return 0;
}

/*
释放
*/
static int globalmem_release (struct inode *inodep, struct file *fp)
{
    printk(KERN_INFO "call %s\r\n", __func__);
    return 0;
}

/* 文件操作结构体 */
static const struct file_operations globalmem_fop = {
    .owner = THIS_MODULE,
    .open    = globalmem_open,
    .release = globalmem_release,
    .read    = globalmem_read,
    .write   = globalmem_write,
    .llseek  = globalmem_llseek,
};


/* 模块加载函数 */
static int __init globalmem_init(void)
{
    dev_t dev = MKDEV(GLOBALMEM_MAJOR, 0);
    int   status = 0;

    /* 1、申请设备号 */
    /* 不指定主设备号,由内核自动生成可用主设备号 */
    if (0 == GLOBALMEM_MAJOR)
    {
        status = alloc_chrdev_region(&dev, 0, 1, GLOBALMEM_DEVNAME);
        if (0 != status)
        {
            printk(KERN_INFO "alloc_chrdev_region failed\r\n");
            return -1;
        }
    }
    /* 指定主设备号 */
    else
    {
        status = register_chrdev_region(dev, 1, GLOBALMEM_DEVNAME);
        if (0 != status)
        {
            printk(KERN_INFO "register_chrdev_region failed\r\n");
            return -1;
        }
    }
    printk(KERN_INFO "dev:%s, major:%d, minor:%d\r\n", GLOBALMEM_DEVNAME, MAJOR(dev), MINOR(dev));

    /* 2、申请globalmem_dev内存 */
    globalmem_devp = kzalloc(sizeof(struct globalmem_dev), GFP_KERNEL);
    if (NULL == globalmem_devp)
    {
        printk(KERN_INFO "kzalloc globalmem_dev failed\r\n");
        goto err0;
    }

    globalmem_devp->dev_num = dev; /* 保存设备号 */

    /* 3、初始化cdev结构体 */
    cdev_init(&globalmem_devp->cdev, &globalmem_fop);

    /* 3、添加cdev结构体 */
    status = cdev_add(&globalmem_devp->cdev, globalmem_devp->dev_num, 1);
    if (0 != status)
    {
        printk(KERN_INFO "cdev_add failed\r\n");
        goto err0;
    }

    printk(KERN_INFO "globalmem init success\r\n");

    return 0;

err0:
    unregister_chrdev_region(dev, 1);

    if (NULL != globalmem_devp)
    {
        kfree(globalmem_devp);
        globalmem_devp = NULL;
    }
    printk(KERN_INFO "globalmem init failed\r\n");

    return -1;

}

/* 模块卸载函数 */
static void __exit globalmem_exit(void)
{
    if (NULL != globalmem_devp)
    {
        /* 1、删除cdev对象 */
        cdev_del(&globalmem_devp->cdev);

        /* 2、注销设备号 */
        unregister_chrdev_region(globalmem_devp->dev_num, 1);

        /* 3、释放内存 */
        kfree(globalmem_devp);
        globalmem_devp = NULL;
    }

    printk(KERN_INFO "globalmem_exit\r\n");

}

module_init(globalmem_init); /* 注册模块加载函数 */
module_exit(globalmem_exit); /* 注册模块卸载函数 */

MODULE_AUTHOR("hello <12345678@qq.com>");
MODULE_LICENSE("GPL v2");

有多个globalmem实例的情况:

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <linux/slab.h>

#define GLOBALMEM_SIZE  (0x1000)          /* 全局缓存的内存大小,单位字节 */
#define GLOBALMEM_MAJOR (230)             /* 主设备号 */
#define GLOBALMEM_DEVNAME "globalmem_dev" /* 设备名称 */
#define GLOBALMEM_DEV_NUM (10)            /* 设备数量 */

/* 设备结构体 */
struct globalmem_dev
{
    struct cdev cdev; /* 字符设备结构体 */
    dev_t  dev_num;   /* 设备号 */
    unsigned char mem[GLOBALMEM_SIZE]; /* 全局缓存内存 */
};
struct globalmem_dev* globalmem_devp = NULL;


/*
文件位置调整
*/
static loff_t globalmem_llseek (struct file *fp, loff_t offset, int origin_pos)
{
    loff_t ret = 0;
    struct globalmem_dev* devp = fp->private_data;
    printk(KERN_INFO "call %s dev_num(%d, %d)\r\n", __func__, MAJOR(devp->dev_num),  MINOR(devp->dev_num));

    switch (origin_pos)
    {
        case 0:/* 从起始位置开始 */
            /* 偏移不能小于0 */
            if (0 > offset)
            {
                ret = -EINVAL;
                break;
            }

            /* 偏移不能超过缓存区大小 */
            if (offset > GLOBALMEM_SIZE)
            {
                ret = -EINVAL;
                break;
            }

            /* 调整f_pos指针 */
            fp->f_pos = offset;
            ret = fp->f_pos;

            break;

        case 1:/* 从当前位置开始 */
            /* 当前位置+偏移不能超过缓存区大小 */
            if ((fp->f_pos + offset) > GLOBALMEM_SIZE)
            {
                ret = -EINVAL;
                break;
            }

            /* 当前位置+偏移不能小于0 */
            if (0 > (fp->f_pos + offset))
            {
                ret = -EINVAL;
                break;
            }

            /* 调整f_pos指针 */
            fp->f_pos += offset;
            ret = fp->f_pos;
            break;
        default:
            ret = -EINVAL;
            break;
    }

    return ret;
}

/* 
从内核空间读取数据到用户空间

 */
static ssize_t globalmem_read (struct file *fp, char __user *buf, size_t size, loff_t *ppos)/* ppos为读取位置,读取完后会返回最新位置 */
{
    struct globalmem_dev* devp = fp->private_data;
    loff_t pos = *ppos;
    size_t count = size;  /* 实际可以读取的字节数 */
    int status = 0;

    printk(KERN_INFO "call %s dev_num(%d, %d)\r\n", __func__, MAJOR(devp->dev_num),  MINOR(devp->dev_num));

    /* 越界判断 */
    /* 读取起始位置已超过缓存区大小 */
    if (pos > GLOBALMEM_SIZE)
    {
        return -EFAULT;
    }

    /* 读取起始位置+读取字节数已超过缓存区大小,就只读取起始位置至缓存区末尾的数据 */
    if ((pos + size) > GLOBALMEM_SIZE)
    {
        count = GLOBALMEM_SIZE - pos;
    }

    status = copy_to_user(buf, devp->mem + pos, count);
    if (0 != status)
    {
        printk(KERN_INFO "read failed\r\n");
        return -EFAULT;
    }

    *ppos += count;

    return count;
}

/*
从用户空间写输入到内核空间
*/
static ssize_t globalmem_write (struct file *fp, const char __user *buf, size_t size, loff_t *ppos)
{
    struct globalmem_dev* devp = fp->private_data;
    loff_t pos = *ppos;
    size_t count = size;  /* 实际可以写的字节数 */
    int status = 0;

    printk(KERN_INFO "call %s dev_num(%d, %d)\r\n", __func__, MAJOR(devp->dev_num),  MINOR(devp->dev_num));

    /* 越界判断 */
    if (pos > GLOBALMEM_SIZE)
    {
        return -EFAULT;
    }

    /* 写的起始位置+写入字节数已超过缓存区大小,就只写起始位置至缓存区末尾的数据 */
    if ((pos + size) > GLOBALMEM_SIZE)
    {
        count = GLOBALMEM_SIZE - pos;
    }

    status = copy_from_user(devp->mem + pos, buf, count);
    if (0 != status)
    {
        printk(KERN_INFO "write failed\r\n");
        return -EFAULT;
    }

    *ppos += count;

    return count;
}

/* 
打开
 */
static int globalmem_open (struct inode *inodep, struct file *fp)
{
	/* 获取用户打开的设备节点文件对应的globalmem实例 */
    struct globalmem_dev *devp = container_of(inodep->i_cdev, struct globalmem_dev, cdev);
    fp->private_data = devp; /* 将该实例赋给private_data后,read、write等函数就可只操作该设备 */
    printk(KERN_INFO "call %s dev_num(%d, %d)\r\n", __func__, MAJOR(devp->dev_num),  MINOR(devp->dev_num));
    return 0;
}

/*
释放
*/
static int globalmem_release (struct inode *inodep, struct file *fp)
{
    struct globalmem_dev* devp = fp->private_data;
    printk(KERN_INFO "call %s dev_num(%d, %d)\r\n", __func__, MAJOR(devp->dev_num),  MINOR(devp->dev_num));
    return 0;
}

/* 文件操作结构体 */
static const struct file_operations globalmem_fop = {
    .owner = THIS_MODULE,
    .open    = globalmem_open,
    .release = globalmem_release,
    .read    = globalmem_read,
    .write   = globalmem_write,
    .llseek  = globalmem_llseek,
};


/* 模块加载函数 */
static int __init globalmem_init(void)
{
    dev_t dev = MKDEV(GLOBALMEM_MAJOR, 0);
    int   status = 0;
    int   i = 0;

    /* 1、申请设备号 */
    /* 不指定主设备号,由内核自动生成可用主设备号 */
    if (0 == GLOBALMEM_MAJOR)
    {
        status = alloc_chrdev_region(&dev, 0, GLOBALMEM_DEV_NUM, GLOBALMEM_DEVNAME);
        if (0 != status)
        {
            printk(KERN_INFO "alloc_chrdev_region failed\r\n");
            return -1;
        }
    }
    /* 指定主设备号 */
    else
    {
        status = register_chrdev_region(dev, GLOBALMEM_DEV_NUM, GLOBALMEM_DEVNAME);
        if (0 != status)
        {
            printk(KERN_INFO "register_chrdev_region failed\r\n");
            return -1;
        }
    }
    printk(KERN_INFO "dev:%s, major:%d, minor:%d\r\n", GLOBALMEM_DEVNAME, MAJOR(dev), MINOR(dev));

    /* 2、申请globalmem_dev内存 */
    globalmem_devp = kzalloc(sizeof(struct globalmem_dev) * GLOBALMEM_DEV_NUM, GFP_KERNEL);
    if (NULL == globalmem_devp)
    {
        printk(KERN_INFO "kzalloc globalmem_dev failed\r\n");
        goto err0;
    }

    /* 初始化多个设备 */
    for (i = 0; i < GLOBALMEM_DEV_NUM; i++)
    {
        (globalmem_devp + i)->dev_num = MKDEV(MAJOR(dev), i); /* 保存设备号,每个设备的主设备号相同,次设备号不同 */

        /* 3、初始化cdev结构体 */
        cdev_init(&(globalmem_devp + i)->cdev, &globalmem_fop);

        /* 3、添加cdev结构体 */
        status = cdev_add(&(globalmem_devp + i)->cdev, (globalmem_devp + i)->dev_num, 1);
        if (0 != status)
        {
            printk(KERN_INFO "cdev_add dev[%d] failed\r\n", i);
            goto err0;
        }
    }

    printk(KERN_INFO "globalmem init success\r\n");

    return 0;

err0:
    unregister_chrdev_region(dev, GLOBALMEM_DEV_NUM);

    if (NULL != globalmem_devp)
    {
        kfree(globalmem_devp);
        globalmem_devp = NULL;
    }
    printk(KERN_INFO "globalmem init failed\r\n");

    return -1;

}

/* 模块卸载函数 */
static void __exit globalmem_exit(void)
{
    int i = 0;
    if (NULL != globalmem_devp)
    {
        for (i = 0; i < GLOBALMEM_DEV_NUM; i++)
        {
            /* 1、删除cdev对象 */
            cdev_del(&(globalmem_devp+i)->cdev);
        }

        /* 2、注销设备号 */
        unregister_chrdev_region(globalmem_devp->dev_num, GLOBALMEM_DEV_NUM);

        /* 3、释放内存 */
        kfree(globalmem_devp);
        globalmem_devp = NULL;
    }

    printk(KERN_INFO "globalmem_exit\r\n");

}

module_init(globalmem_init); /* 注册模块加载函数 */
module_exit(globalmem_exit); /* 注册模块卸载函数 */

MODULE_AUTHOR("hello <1111111111@qq.com>");
MODULE_LICENSE("GPL v2");
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值