【北京迅为】《i.MX8MM嵌入式Linux开发指南》-第三篇 嵌入式Linux驱动开发篇-第五十九章 等待队列

i.MX8MM处理器采用了先进的14LPCFinFET工艺,提供更快的速度和更高的电源效率;四核Cortex-A53,单核Cortex-M4,多达五个内核 ,主频高达1.8GHz,2G DDR4内存、8G EMMC存储。千兆工业级以太网、MIPI-DSI、USB HOST、WIFI/BT、4G模块、CAN、RS485等接口一应俱全。H264、VP8视频硬编码,H.264、H.265、VP8、VP9视频硬解码,并提供相关历程,支持8路PDM接口、5路SAI接口、2路Speaker。系统支持Android9.0(支持获取root限)Linux4.14.78+Qt5.10.1、Yocto、Ubuntu20、Debian9系统。适用于智能充电桩,物联网,工业控制,医疗,智能交通等,可用于任何通用工业和物联网应用、

【公众号】迅为电子

【粉丝群】258811263


五十九章 等待队列

本章导读

阻塞和非阻塞 IO 是 Linux 驱动开发里面很常见的两种设备访问模式,在编写驱动的时候一定要考虑到阻塞和非阻塞。本章我们就来学习一下阻塞和非阻塞 IO,以及如何在驱动程序中处理阻塞与非阻塞

59.1章节讲解了阻塞和非阻塞IO的概念

59.2章节编写了驱动程序,在iTOP-IMX8MM开发板上为例,实现了非阻塞的按键驱动

59.3章节编写应用测试程序

59.4章节运行测试,发现CPU占用率很高

59.5章节在59.2章节的基础上编写驱动程序,用等待队列阻塞,当按键按下的时候,再去读取value的信息,这样做是比较专业的,而且可以极大的减少cpu的占用率。

本章内容对应视频讲解链接(在线观看):

等待队列  https://www.bilibili.com/video/BV1Vy4y1B7ta?p=38

程序源码在网盘资料“iTOP-i.MX8MM开发板\02-i.MX8MM开发板网盘资料汇总(不含光盘内容)\嵌入式Linux开发指南(iTOP-i.MX8MM)手册配套资料\2.驱动程序例程\016-等待队列”路径下。

59.1 阻塞和非阻塞IO

59.1.1 阻塞与非阻塞简介

阻塞操作是指在执行设备操作时,若不能获得资源,则挂起进程直到满足可操作的条件后再进行操作。

被挂起的进程进入睡眠状态,被从调度器的运行队列移走,直到等待的条件被满足。而非阻塞操作的进程

在不能进行设备操作时,并不挂起,它要么放弃,要么不停地查询,直至可以进行操作为止。

在阻塞访问时,不能获取资源的进程将进入休眠,它将 CPU 资源“礼让”给其他进程。因为阻塞的进程会进入休眠状态,所以必须确保有一个地方能够唤醒休眠的进程,否则,进程就真的“寿终正寝”了。唤醒进程的地方最大可能发生在中断里面,因为在硬件资源获得的同时往往伴随着一个中断。而非阻塞的进程则不断尝试,直到可以进行 I/O。阻塞访问如图所示:

若用户以非阻塞的方式访问设备文件,则当设备资源不可获取时,设备驱动的 xxx_read() 、 xxx_write

() 等操作应立即返回,read() 、write() 等系统调用也随即被返回,应用程序收到-EAGAIN 返回值。

 

应用程序可以使用如下所示示例代码来实现阻塞访问:

int fd;

int data = 0;

fd = open("/dev/xxx_dev", O_RDWR); /* 阻塞方式打开 */

ret = read(fd, &data, sizeof(data)); /* 读取数据 */

可以看出对于设备驱动文件的默认读取方式就是阻塞式的,所以我们前面所有的例程测试 APP 都是采

用阻塞 IO。

如果应用程序要采用非阻塞的方式来访问驱动设备文件,可以使用如下所示代码:

int fd;

int data = 0;

fd = open("/dev/xxx_dev", O_RDWR | O_NONBLOCK); /* 非阻塞方式打开 */

ret = read(fd, &data, sizeof(data)); /* 读取数据 */

使用 open 函数打开“/dev/xxx_dev”设备文件的时候添加了参数“O_NONBLOCK”,表示以非阻塞方式打开设备,这样从设备中读取数据的时候就是非阻塞方式的了。

 

使用 open 函数打开“/dev/xxx_dev”设备文件的时候添加了参数“O_NONBLOCK”,表示以非阻塞方式打开设备,这样从设备中读取数据的时候就是非阻塞方式的了。

59.1.2 等待队列

当我们进程去访问设备的时候,经常需要等待有特定事件发生以后再继续往下运行,这个时候就需要在驱动里面实现当条件不满足的时候进行休眠,当条件满足的时候在由内核唤醒进程。在 Linux 驱动程序中,可以使用等待队列(Wait Queue)来实现阻塞进程的唤醒。等待队列很早就作为一个基本的功能单位出现在 Linux 内核里了,它以队列为基础数据结构,与进程调度机制紧密结合,可以用来同步对系统资源的访问。队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。即满足先进先出的形式 FIFO。

举个例子,比如说我现在去食堂打饭,阿姨和我说现在没有饭,你需要等一会,等我做好了我再叫你,那么我当前不能获得资源,我被阻塞在这儿了,那么等待队列就是让我们阻塞在这儿,然后等特定的事件发生以后,再继续运行。那么等待队列阻塞在这儿的这件事情就相当于阿姨和我们说现在没有饭,你需要等一会。为什么我们要先讲完中断以后再讲等待队列呢?举个例子来说,比如说阿姨和你说现在没饭,你需要在旁边等一会,等我做好了我再叫你,如果说阿姨做完了不叫你,你又睡着了,那么你今天是不是吃不上饭了,所以说在我们阻塞访问的时候不能获得资源的进程,将进入休眠状态,他将cpu的资源全部让给别的进程,必须保证有一个地方可以唤醒休眠进程,否则的话将会长睡不醒。进程唤醒最大可能的地方发生在中断里面,伴随着一个中断的发生我们可以唤醒该进程,对应的事件是阿姨说饭好了,小王你过来打吧。所以说,我们学习等待队列在中断之后,这样用等待队列可以极大的降低cpu的占用率。

Linux 内核的等待队列是以双循环链表为基础数据结构,与进程调度机制紧密结合,能够用于实现核心

的异步事件通知机制。它有两种数据结构:等待队列头(wait_queue_head_t)和等待队列项(wait_queue_t)。

等待队列头和等待队列项中都包含一个 list_head 类型的域作为”连接件”。它通过一个双链表和把等待 task

的头,和等待的进程列表链接起来。

59.1.3 等待队列头

等待队列头就是一个等待队列的头部, 每个访问设备的进程都是一个队列项, 当设备不可用的时候就要将这些进程对应的等待队列项添加到等待队列里面。

等待队列头使用结构体wait_queue_head_t来表示,这个结构体定义在文件include/linux/wait里面,结构体内容如下:

struct __wait_queue_head {

spinlock_t lock; //自旋锁

    struct list_head task_list; //链表头

};

typedef struct __wait_queue_head wait_queue_head_t;

类型名是wait_queue_head_t,只需要记住这个即可。

定义一个等待队列头:

wait_queue_head_t   test_wq;  //定义一个等待队列的头

定义等待队列头以后需要初始化,可以使用init_waitqueue_head函数初始化等待队列头, 函数原型如下:

函数

void init_waitqueue_head(wait_queue_head_t *q)

q

wait_queue_head_t 指针

功能

动态初始化等待队列头结构

也可以使用宏 DECLARE_WAIT_QUEUE_HEAD 来一次性完成等待队列头的定义和初始化。

DECLARE_WAIT_QUEUE_HEAD (wait_queue_head_t *q);

59.1.4 等待队列项

等待队列头就是一个等待队列的头部,每个访问设备的进程都是一个队列项,当设备不可用的时候就

要将这些进程对应的等待队列项添加到等待队列里面。结构体 wait_queue_t 表示等待队列项,结构体内容如下:

struct __wait_queue {

unsigned int flags;

void *private;

wait_queue_func_t func;

struct list_head task_list;

};

typedef struct __wait_queue wait_queue_t;

使用宏 DECLARE_WAITQUEUE 定义并初始化一个等待队列项,宏的内容如下:

DECLARE_WAITQUEUE(name, tsk)

name 就是等待队列项的名字,tsk 表示这个等待队列项属于哪个任务(进程),一般设置为 current , 在

Linux 内核中 current 相当于一个全局变量,表示当前进程。因此 DECLARE_WAITQUEUE 就是给当前正在运行的进程创建并初始化了一个等待队列项。

59.1.5 添加/删除队列

当设备不可访问的时候就需要将进程对应的等待队列项添加到前面创建的等待队列头中,只有添加到

等待队列头中以后进程才能进入休眠态。当设备可以访问以后再将进程对应的等待队列项从等待队列头中

移除即可,等待队列项添加队列函数如下所示:

函数

void add_wait_queue(wait_queue_head_t *q,wait_queue_t *wait)

q

等待队列项要加入的等待队列头。

wait

要加入的等待队列项

返回值

功能

从等待队列头中添加队列

等待队列项移除队列函数如下:

函数

void remove_wait_queue(wait_queue_head_t *q,wait_queue_t *wait)

q

要删除的等待队列项所处的等待队列头

wait

要删除的等待队列项

返回值

59.1.6 等待唤醒

当设备可以使用的时候就要唤醒进入休眠态的进程,唤醒可以使用如下两个函数

void wake_up(wait_queue_head_t *q)           //功能:唤醒所有休眠进程

void wake_up_interruptible(wait_queue_head_t *q)//功能:唤醒可中断的休眠进程

参数 q 就是要唤醒地等待队列头,这两个函数会将这个等待队列头中的所有进程都唤醒。

wake_up 函数可以唤醒处于 TASK_INTERRUPTIBLE 和 TASK_UNINTERRUPTIBLE状态的进程,而wake_up_interruptible 函数只能唤醒处于 TASK_INTERRUPTIBLE 状态的进程。

 

59.1.7 等待事件

除了主动唤醒以外,也可以设置等待队列等待某个事件,当这个事件满足以后就自动唤醒等待队列中

的进程,相关函数:

#define wait_event(wq, condition)

do {

if (condition)

break;

__wait_event(wq, condition);

} while (0)

wait_event(queue,condition);等待以 queue 为等待队列头等待队列被唤醒,condition 必须满足,否则阻塞

wait_event_interruptible(queue,condition);可被信号打断

wait_event_timeout(queue,condition,timeout);阻塞等待的超时时间,时间到了,不论 condition 是否满足,都要返回

wait_event_interruptible_timeout(queue,condition,timeout)

wait_event()宏

功能:不可中断的阻塞等待,让调用进程进入不可中断的睡眠状态,在等待队列里面睡眠直到condition变成真,被内核唤醒。

wait_event_interruptible() 函数

功能:可中断的阻塞等待,让调用进程进入可中断的睡眠状态,直到condition变成真被内核唤醒或被信号打断唤醒。

wait_event_timeout() 宏:

也与 wait_event()类似.不过如果所给的睡眠时间为负数则立即返回.如果在睡眠期间被唤醒,且 condition为真则返回剩余的睡眠时间,否则继续睡眠直到到达或超过给定的睡眠时间,然后返回 0.

wait_event_interruptible_timeout() 宏:

与 wait_event_timeout()类似,不过如果在睡眠期间被信号打断则返回 ERESTARTSYS 错误码.

wait_event_interruptible_exclusive() 宏:

同样和 wait_event_interruptible()一样,不过该睡眠的进程是一个互斥进程

注意:调用的时要确认condition 值是真还是假,如果调用condition为真,则不会休眠。

59.2 编写驱动程序

程序源码在网盘资料“iTOP-i.MX8MM开发板\02-i.MX8MM开发板网盘资料汇总(不含光盘内容)\嵌入式Linux开发指南(iTOP-i.MX8MM)手册配套资料\2.驱动程序例程\016-等待队列\001”路径下。

我们以IMX8MM开发板为例,在Ubuntu的/home/topeet/imx8mm/16/001目录下新建driver.c,编写驱动代码如下所示;

/*
 * @Author: topeet
 * @Description: 等待队列实验
 */
#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/miscdevice.h>
#include <linux/uaccess.h>
#include <linux/fs.h>
#include <linux/gpio.h>
#include <linux/of_gpio.h>
#include <linux/interrupt.h>
#include <linux/of_irq.h>
#include <linux/wait.h>
#include <linux/sched.h>
#include <linux/io.h>

//定义结构体表示我们的节点
struct device_node *test_device_node;
struct property *test_node_property;
//要申请的中断号
int irq;
//GPIO 编号
int gpio_nu;
//用来模拟管脚的状态
int value = 0;

/**
 * @description: 中断处理函数test_key
 * @param {int} irq :要申请的中断号
 * @param {void} *args :
 * @return {*}IRQ_HANDLED
 */
irqreturn_t test_key(int irq, void *args)
{
    value = !value;
    return IRQ_RETVAL(IRQ_HANDLED);
}
int misc_open(struct inode *node, struct file *file)
{
    printk("hello misc_open \n");
    return 0;
}

int misc_release(struct inode *node, struct file *file)
{
    printk("hello misc_release bye bye\n");
    return 0;
}

static ssize_t misc_read(struct file *file, char __user *ubuf, size_t size, loff_t *loff_t)
{
   
    if (copy_to_user(ubuf, &value, sizeof(value)) != 0)
    {
        printk("copy_to_user error\n");
        return -1;
    }
   
    return 0;
}
//文件操作集
struct file_operations misc_fops = {
    .owner = THIS_MODULE,
    .open = misc_open,
    .release = misc_release,
    .read = misc_read};

struct miscdevice misc_dev = {
    .minor = MISC_DYNAMIC_MINOR,
    .name = "test_wq",
    .fops = &misc_fops,
};
/**
 * @brief led_probe : 与设备信息层(设备树)匹配成功后自动执行此函数,
 * @param inode : 文件索引
 * @param file  : 文件
 * @return 成功返回 0           
*/
int led_probe(struct platform_device *pdev)
{
    int ret = 0;
    printk("led_probe\n");
    //of_find_node_by_path函数通过路径查找节点,/test_key是设备树下的节点路径
    test_device_node = of_find_node_by_path("/test");
    if (test_device_node == NULL)
    {
        printk("of_find_node_by_path is error\n");
        return -1;
    }
    //of_get_named_gpio函数获取 GPIO 编号
    gpio_nu = of_get_named_gpio(test_device_node, "gpios", 0);
    if (gpio_nu < 0)
    {
        printk("of_get_named_gpio is error\n");
        return -1;
    }
    //设置GPIO为输入模式
    gpio_direction_input(gpio_nu);
    //获取GPIO对应的中断号
    irq = irq_of_parse_and_map(test_device_node, 0);
    printk("irq is %d \n", irq);
    /*申请中断,irq:中断号名字  
     test_key:中断处理函数
     IRQF_TRIGGER_RISING:中断标志,意为上升沿触发
     "test_key":中断的名字
     */
    ret = request_irq(irq, test_key, IRQF_TRIGGER_RISING, "test_key", NULL);
    if (ret < 0)
    {
        printk("request_irq \n");
        return -1;
    }
    //注册杂项设备
    ret = misc_register(&misc_dev);
    if (ret < 0)
    {
        printk("misc_register is error\n");
        return -1;
    }
    printk("misc_register is successd \n");
    return 0;
}

int led_remove(struct platform_device *pdev)
{
    printk("led_remove \n");
    return 0;
}
const struct platform_device_id led_idtable = {
    .name = "led_test",
};
const struct of_device_id of_match_table_test[] = {
    {.compatible = "keys"},
    {},
};
struct platform_driver led_driver = {
    //3. 在led_driver结构体中完成了led_probe和led_remove
    .probe = led_probe,
    .remove = led_remove,
    .driver = {
        .owner = THIS_MODULE,
        .name = "led_test",
        .of_match_table = of_match_table_test},
    //4 .id_table的优先级要比driver.name的优先级要高,优先与.id_table进行匹配
    .id_table = &led_idtable};

static int led_driver_init(void)
{
    //1.我们看驱动文件要从init函数开始看
    int ret = 0;
    //2. 在init函数里面注册了platform_driver
    ret = platform_driver_register(&led_driver);
    if (ret < 0)
    {
        printk("platform_driver_register error \n");
        return ret;
    }
    printk("platform_driver_register ok \n");
    return 0;
}

static void led_driver_exit(void)
{
    printk("gooodbye! \n");
    free_irq(irq, NULL);
    misc_deregister(&misc_dev);
    platform_driver_unregister(&led_driver);
}
module_init(led_driver_init);
module_exit(led_driver_exit);
MODULE_LICENSE("GPL");

59.3 编写应用程序

在Ubuntu的/home/topeet/imx8mm/16/001目录下我们编写应用程序app.c,如下图所示:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc,char *argv[])
{
    int fd;
    int value;
     //打开设备节点
    fd = open("/dev/test_wq",O_RDWR);
    if(fd < 0)
    {
        //打开设备节点失败
        perror("open error \n"); 
        return fd;
    }
    while(1)
    {
    read(fd,&value,sizeof(value));      
    printf("value is %d \n",value); 
    }
    close(fd);
    return 0;
}

编译应用程序,如下图所示:

59.4 运行测试

我们将刚刚编写的驱动代码编译为驱动模块,如下图所示:

我们进入共享目录并且加载驱动模块,如下图所示: 

运行应用程序,串口调试信息会不停的打印value值是0,如下图所示:

 

我们按底板上的音量+按键时,value值取反,变为1,如下图所示: 

我们重新编译将应用程序app后台运行,然后输入top查看内存占用率,如下图所示:

 

 

59.5 优化方案

在59.4章节中,如上图所示,app的cpu占用率高达99%,这样肯定是不行的,别的程序是不能运行的,所以说我们要用等待队列阻塞,当按键按下的时候,再去读取value的信息,这样做是比较专业的,而且可以极大的减少cpu的占用率。我们在part1代码的基础上进行修改,代码如下所示:

程序源码在网盘资料“iTOP-i.MX8MM开发板\02-i.MX8MM开发板网盘资料汇总(不含光盘内容)\嵌入式Linux开发指南(iTOP-i.MX8MM)手册配套资料\2.驱动程序例程\016-等待队列\002”路径下。

/*
 * @Author: topeet
 * @Description: 等待队列实验
 */
#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/miscdevice.h>
#include <linux/uaccess.h>
#include <linux/fs.h>
#include <linux/gpio.h>
#include <linux/of_gpio.h>
#include <linux/interrupt.h>
#include <linux/of_irq.h>
#include <linux/wait.h>
#include <linux/sched.h>
#include <linux/io.h>

//定义结构体表示我们的节点
struct device_node *test_device_node;
struct property *test_node_property;
//要申请的中断号
int irq;
//GPIO 编号
int gpio_nu;
//用来模拟管脚的状态
int value = 0;
//等待队列头的定义和初始化。
DECLARE_WAIT_QUEUE_HEAD(key_wq);
//定义等待队列标志位
int wq_flags = 0;

/**
 * @description: 中断处理函数test_key
 * @param {int} irq :要申请的中断号
 * @param {void} *args :
 * @return {*}IRQ_HANDLED
 */
irqreturn_t test_key(int irq, void *args)
{
    //将等待队列置1,然后唤醒等待队列
    wq_flags = 1;
    wake_up(&key_wq);
    //将value取反
    value =!value;
    return IRQ_HANDLED;
}

int misc_open(struct inode *node, struct file *file)
{
    printk("hello misc_open \n");
    return 0;
}
int misc_release(struct inode *node, struct file *file)
{
    printk("hello misc_release bye bye\n");
    return 0;
}
ssize_t misc_read(struct file *file, char __user *ubuf, size_t size, loff_t *loff_t)
{
    //阻塞,可被wake_up唤醒
    wait_event_interruptible(key_wq, wq_flags);
    wq_flags = 0;
    if (copy_to_user(ubuf, &value, sizeof(value)) != 0)
    {
        printk("copy_to_user error\n");
        return -1;
    }
    return 0;
}
//文件操作集
struct file_operations misc_fops = {
    .owner = THIS_MODULE,
    .open = misc_open,
    .release = misc_release,
    .read = misc_read};
struct miscdevice misc_dev = {
    .minor = MISC_DYNAMIC_MINOR,
    .name = "test_wq",
    .fops = &misc_fops,
};

/****************************************************************************************
 * @brief led_probe : 与设备信息层(设备树)匹配成功后自动执行此函数,
 * @param inode : 文件索引
 * @param file  : 文件
 * @return 成功返回 0           
 ****************************************************************************************/
int led_probe(struct platform_device *pdev)
{
    int ret = 0;
    // 打印匹配成功进入probe函数
    printk("led_probe\n");
    test_device_node = of_find_node_by_path("/test");
    if (test_device_node == NULL)
    {
        //查找节点失败则打印信息
        printk("of_find_node_by_path is error \n");
        return -1;
    }
    gpio_nu = of_get_named_gpio(test_device_node, "gpios", 0);
    if (gpio_nu < 0)
    {
        printk("of_get_namd_gpio is error \n");
        return -1;
    }
    //设置GPIO为输入模式
    gpio_direction_input(gpio_nu);
    //获取GPIO对应的中断号
    irq = gpio_to_irq(gpio_nu);
    // irq =irq_of_parse_and_map(test_device_node,0);
    printk("irq is %d \n", irq);
    /*申请中断,irq:中断号名字  
     test_key:中断处理函数
     IRQF_TRIGGER_RISING:中断标志,意为上升沿触发
     "test_key":中断的名字
     */
    ret = request_irq(irq, test_key, IRQF_TRIGGER_RISING, "test_key", NULL);
    if (ret < 0)
    {
        printk("request_irq is error \n");
        return -1;
    }
    //注册杂项设备
    ret=misc_register(&misc_dev);
    if(ret<0)
    {
        printk("misc_register is error \n");
        return -1;
    }
    printk("misc_register is success \n");
    return 0;
}

int led_remove(struct platform_device *pdev)
{
    printk("led_remove\n");
    return 0;
}
const struct platform_device_id led_idtable = {
    .name = "keys",
};
const struct of_device_id of_match_table_test[] = {
    {.compatible = "keys"},
    {},
};
struct platform_driver led_driver = {
    //3. 在led_driver结构体中完成了led_probe和led_remove
    .probe = led_probe,
    .remove = led_remove,
    .driver = {
        .owner = THIS_MODULE,
        .name = "led_test",
        .of_match_table = of_match_table_test},
    //4 .id_table的优先级要比driver.name的优先级要高,优先与.id_table进行匹配
    .id_table = &led_idtable};

/**
 * @description: 模块初始化函数
 * @param {*}
 * @return {*}
 */
static int led_driver_init(void)
{
    //1.我们看驱动文件要从init函数开始看
    int ret = 0;
    //2.在init函数里面注册了platform_driver
    ret = platform_driver_register(&led_driver);
    if (ret < 0)
    {
        printk("platform_driver_register error \n");
    }
    printk("platform_driver_register ok \n");
    return 0;
}

/**
 * @description: 模块卸载函数
 * @param {*}
 * @return {*}
 */
static void led_driver_exit(void)
{
    free_irq(irq, NULL);
    platform_driver_unregister(&led_driver);
    printk("gooodbye! \n");
}
module_init(led_driver_init);
module_exit(led_driver_exit);

MODULE_LICENSE("GPL");

我们还是像part1实验一样,将驱动编译为驱动模块,应用程序还是使用part1编译好的app,我们加载驱动模块,如下图所示:

如上图所示,运行了应用程序以后,我们触摸以下屏幕,按一次按键打印一次value的值。 

  • 13
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值