Linux下半部机制之——tasklet

一、tasklet是什么?

        tasklet是利用軟中斷實現的一種下半部機制。tasklet相比於軟中斷,其接口更加簡單方便,鎖保護要求較低。 tasklet由tasklet_struct結構體表示:

struct tasklet_struct  
{  
    struct tasklet_struct *next;    //鏈表中下一個tasklet  
    unsigned long state;            //tasklet狀態  
    atomic_t count;                 //引用計數  
    void (*func)(unsigned long);    //tasklet處理函數  
    unsigned long data;             //給tasklet處理函數的參數  
};  

二、tasklet分类

tasklet還分為了高優先級tasklet與一般tasklet,前面分析軟中斷時softirq_init()註冊的兩個tasklet軟中斷。

void __init softirq_init(void)  
{  
    ......  
    //此處註冊兩個軟中斷  
    open_softirq(TASKLET_SOFTIRQ, tasklet_action);  
    open_softirq(HI_SOFTIRQ, tasklet_hi_action);      
    ......  
}  

其處理函數分別為 tasklet_action()和tasklet_hi_action()。

三、tasklet_action函数

static void tasklet_action(struct softirq_action *a)  
{  
    struct tasklet_struct *list;  
  
    local_irq_disable();  
    list = __get_cpu_var(tasklet_vec).head;  
    __get_cpu_var(tasklet_vec).head = NULL;  
    __get_cpu_var(tasklet_vec).tail = &__get_cpu_var(tasklet_vec).head;  
    local_irq_enable();  
  
    while (list) {  
        struct tasklet_struct *t = list;  
  
        list = list->next;  
  
        if (tasklet_trylock(t)) {           //判断TASKLET_STATE_RUN标记   
            if (!atomic_read(&t->count)) {  //t->count為零才會調用task_struct裡的函數  
                if (!test_and_clear_bit(TASKLET_STATE_SCHED, &t->state))  
                    BUG();  
  
                 t->func(t->data);    //設置了TASKLET_STATE_SCHED標誌才會被遍歷到鏈表上對應的函數  
                tasklet_unlock(t);  
                continue;  
            }  
            tasklet_unlock(t);  
        }  
  
        local_irq_disable();  
        t->next = NULL;  
        *__get_cpu_var(tasklet_vec).tail = t;  
        __get_cpu_var(tasklet_vec).tail = &(t->next);  
        __raise_softirq_irqoff(TASKLET_SOFTIRQ);  
        local_irq_enable();  
    }  
}  

四、tasklet_hi_action函数

static void tasklet_hi_action(struct softirq_action *a)  
{  
    struct tasklet_struct *list;  
  
    local_irq_disable();  
    list = __get_cpu_var(tasklet_hi_vec).head;  
    __get_cpu_var(tasklet_hi_vec).head = NULL;  
    __get_cpu_var(tasklet_hi_vec).tail = &__get_cpu_var(tasklet_hi_vec).head;  
    local_irq_enable();  
  
    while (list) {  
        struct tasklet_struct *t = list;  
  
        list = list->next;  
  
        if (tasklet_trylock(t)) {         //判断TASKLET_STATE_RUN标记
            if (!atomic_read(&t->count)) {  
                if (!test_and_clear_bit(TASKLET_STATE_SCHED, &t->state))  
                    BUG();  
                t->func(t->data);  
                tasklet_unlock(t);  
                continue;  
            }  
            tasklet_unlock(t);  
        }  
  
        local_irq_disable();  
        t->next = NULL;  
        *__get_cpu_var(tasklet_hi_vec).tail = t;  
        __get_cpu_var(tasklet_hi_vec).tail = &(t->next);  
        __raise_softirq_irqoff(HI_SOFTIRQ);  
        local_irq_enable();  
    }  
}  

五、tasklet_action和tasklet_hi_action主要逻辑

這兩個函數主要是做了如下動作:

1.禁止中斷,併為當前處理器檢索tasklet_vec或tasklet_hi_vec鏈表。
2.將當前處理器上的該鏈表設置為NULL,達到清空的效果。
3.循環遍歷獲得鏈表上的每一個待處理的tasklet。
4.如果是多處理器系統,通過檢查TASKLET_STATE_RUN來判斷這個tasklet是否正在其他處理器上運行。如果它正在運行,那麼現在就不要執行,跳 到下一個待處理的tasklet去。
5.如果當前這個tasklet沒有執行,將其狀態設置為TASKLETLET_STATE_RUN,這樣別的處理器就不會再去執行它了。
6.檢查count值是否為0,確保tasklet沒有被禁止。如果tasklet被禁止,則跳到下一個掛起的tasklet去。
7.現在可以確定這個tasklet沒有在其他地方執行,並且被我們設置為執行狀態,這樣它在其他部分就不會被執行,並且引用計數器為0,現在可以執行tasklet的處理程序了。
8.重複執行下一個tasklet,直至沒有剩餘的等待處理的tasklets。

六、如何注册使用tasklet

一般情況下,都是用tasklet來實現下半部,tasklet可以動態創建、使用方便、執行速度快。下面來看一下如何創建自己的tasklet呢?

1. 第一步,聲明自己的tasklet。

既可以靜態也可以動態創建,這取決於選擇是想有一個對tasklet的直接引用還是間接引用。靜態創建方法(直接引用),可以使用下列兩個宏的一個(在linux/interrupt.h中定義):

#define DECLARE_TASKLET(name, func, data) \  
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }  
  
#define DECLARE_TASKLET_DISABLED(name, func, data) \  
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(1), func, data }  


DECLARE_TASKLET(name,func,data)  
DECLARE_TASKLET_DISABLED(name,func,data)

這兩個宏之間的區別在於引用計數器的初始值不同,前面一個把創建的tasklet的引用計數器設置為0,使其處於激活狀態,另外一個將其設置為1,處於禁止狀態。而動態創建(間接引用)的方式如下: tasklet_init(t,tasklet_handler,dev); 其實現代碼為:

void tasklet_init(struct tasklet_struct *t,  
          void (*func)(unsigned long), unsigned long data)  
{  
    t->next = NULL;  
    t->state = 0;  
    atomic_set(&t->count, 0);  
    t->func = func;  
    t->data = data;  
}

2. 第二步,编写tasklet处理程序

tasklet處理函數類型是void tasklet_handler(unsigned long data)。因為是靠軟中斷實現,所以tasklet不能休眠,也就是說不能在tasklet中使用信號量或者其他什麼阻塞式的函數。由於tasklet 運行時允許響應中斷,所以必須做好預防工作,如果新加入的tasklet和中斷處理程序之間共享了某些數據額的話。兩個相同的tasklet絕不能同時執 行,如果新加入的tasklet和其他的tasklet或者軟中斷共享了數據,就必須要進行適當地鎖保護。

3. 第三步,调度tasklet

調用tasklet_schedule()(或tasklet_hi_schedule())函數,tasklet就會進入掛起狀態以便執行。如果在還沒有得到運行機會之前,如果有一個相同的tasklet又被調度了,那麼它仍然只會運行一次。如果這時已經開始運行,那麼這個新的tasklet會被重新調度並再次運行。一種優化策略是一個tasklet總在調度它的處理器上執行。

調用tasklet_disable()來禁止某個指定的 tasklet,如果該tasklet當前正在執行,這個函數會等到它執行完畢再返回。調用tasklet_disable_nosync()也是來禁止 的,只是不用在返回前等待tasklet執行完畢,這麼做不太安全,因為沒法估計該tasklet是否仍在執行。 tasklet_enable()激活一個tasklet。可以使用tasklet_kill()函數從掛起的對列中去掉一個tasklet。這個函數會 首先等待該tasklet執行完畢,然後再將其移去。當然,沒有什麼可以阻止其他地方的代碼重新調度該tasklet。由於該函數可能會引起休眠,所以禁止在中斷上下文中使用它。

static inline void tasklet_schedule(struct tasklet_struct* t)
{
    //檢查tasklet的狀態是否為TASKLET_STATE_SCHED.如果是,說明tasklet已經被調度過了,函數返回。
    if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state)) {
                      __tasklet_schedule(t);
    }
}

void __tasklet_schedule(struct tasklet_struct* t)
{
    unsigned long flags;

    //保存中斷狀態,然後禁止本地中斷。在執行tasklet代碼時,這麼做能夠保證處理器上的數據不會弄亂。
    local_irq_save(flags);

    //把需要調度的tasklet加到每個處理器一個的tasklet_vec鏈表或task_hi_vec鏈表的表頭上去。
    t->next = NULL;
    *__get_cpu_var(tasklet_vec).tail = t;
    __get_cpu_var(tasklet_vec).tail = &(t->next);

    //喚起TASKLET_SOFTIRQ或HI_SOFTIRQ軟中斷,這樣在下一次調用do_softirq()時就會執行該tasklet。
    raise_softirq_irqoff(TASKLET_SOFTIRQ);

    //恢復中斷到原狀態並返回。
    local_irq_restore(flags);
}

七、tasklet实例——静态方法

/***************************************************************************//**
*  \file       driver.c
*
*  \details    Simple linux driver (Tasklet Static method)
*
*  \author     EmbeTronicX
*
* *******************************************************************************/
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include<linux/slab.h>                 //kmalloc()
#include<linux/uaccess.h>              //copy_to/from_user()
#include<linux/sysfs.h> 
#include<linux/kobject.h> 
#include <linux/interrupt.h>
#include <asm/io.h>
 
 
#define IRQ_NO 11
 
void tasklet_fn(unsigned long); 

/* Init the Tasklet by Static Method */
DECLARE_TASKLET(tasklet,tasklet_fn, 1);
 
 
/*Tasklet Function*/
void tasklet_fn(unsigned long arg)
{
        printk(KERN_INFO "Executing Tasklet Function : arg = %ld\n", arg);
}
 
 
//Interrupt handler for IRQ 11. 
static irqreturn_t irq_handler(int irq,void *dev_id) {
        printk(KERN_INFO "Shared IRQ: Interrupt Occurred");
        /*Scheduling Task to Tasklet*/
        tasklet_schedule(&tasklet); 
        
        return IRQ_HANDLED;
}
 
volatile int etx_value = 0;
 
dev_t dev = 0;
static struct class *dev_class;
static struct cdev etx_cdev;
struct kobject *kobj_ref;
 
static int __init etx_driver_init(void);
static void __exit etx_driver_exit(void);
 
/*************** Driver Functions **********************/
static int etx_open(struct inode *inode, struct file *file);
static int etx_release(struct inode *inode, struct file *file);
static ssize_t etx_read(struct file *filp, 
                char __user *buf, size_t len,loff_t * off);
static ssize_t etx_write(struct file *filp, 
                const char *buf, size_t len, loff_t * off);
 
/*************** Sysfs Functions **********************/
static ssize_t sysfs_show(struct kobject *kobj, 
                struct kobj_attribute *attr, char *buf);
static ssize_t sysfs_store(struct kobject *kobj, 
                struct kobj_attribute *attr,const char *buf, size_t count);
 
struct kobj_attribute etx_attr = __ATTR(etx_value, 0660, sysfs_show, sysfs_store);
 
/*
** File operation sturcture
*/
static struct file_operations fops =
{
        .owner          = THIS_MODULE,
        .read           = etx_read,
        .write          = etx_write,
        .open           = etx_open,
        .release        = etx_release,
};
 
/*
** This function will be called when we read the sysfs file
*/  
static ssize_t sysfs_show(struct kobject *kobj, 
                struct kobj_attribute *attr, char *buf)
{
        printk(KERN_INFO "Sysfs - Read!!!\n");
        return sprintf(buf, "%d", etx_value);
}

/*
** This function will be called when we write the sysfs file
*/  
static ssize_t sysfs_store(struct kobject *kobj, 
                struct kobj_attribute *attr,const char *buf, size_t count)
{
        printk(KERN_INFO "Sysfs - Write!!!\n");
        sscanf(buf,"%d",&etx_value);
        return count;
}

/*
** This function will be called when we open the Device file
*/  
static int etx_open(struct inode *inode, struct file *file)
{
        printk(KERN_INFO "Device File Opened...!!!\n");
        return 0;
}

/*
** This function will be called when we close the Device file
*/   
static int etx_release(struct inode *inode, struct file *file)
{
        printk(KERN_INFO "Device File Closed...!!!\n");
        return 0;
}

/*
** This function will be called when we read the Device file
*/
static ssize_t etx_read(struct file *filp, 
                char __user *buf, size_t len, loff_t *off)
{
        printk(KERN_INFO "Read function\n");
        asm("int $0x3B");  // Corresponding to irq 11
        return 0;
}


/*
** This function will be called when we write the Device file
*/
static ssize_t etx_write(struct file *filp, 
                const char __user *buf, size_t len, loff_t *off)
{
        printk(KERN_INFO "Write Function\n");
        return len;
}
 
/*
** Module Init function
*/ 
static int __init etx_driver_init(void)
{
        /*Allocating Major number*/
        if((alloc_chrdev_region(&dev, 0, 1, "etx_Dev")) <0){
                printk(KERN_INFO "Cannot allocate major number\n");
                return -1;
        }
        printk(KERN_INFO "Major = %d Minor = %d \n",MAJOR(dev), MINOR(dev));
 
        /*Creating cdev structure*/
        cdev_init(&etx_cdev,&fops);
 
        /*Adding character device to the system*/
        if((cdev_add(&etx_cdev,dev,1)) < 0){
            printk(KERN_INFO "Cannot add the device to the system\n");
            goto r_class;
        }
 
        /*Creating struct class*/
        if((dev_class = class_create(THIS_MODULE,"etx_class")) == NULL){
            printk(KERN_INFO "Cannot create the struct class\n");
            goto r_class;
        }
 
        /*Creating device*/
        if((device_create(dev_class,NULL,dev,NULL,"etx_device")) == NULL){
            printk(KERN_INFO "Cannot create the Device 1\n");
            goto r_device;
        }
 
        /*Creating a directory in /sys/kernel/ : kernel_kobj是kernel内定义的的一个kobj类型结构*/
        kobj_ref = kobject_create_and_add("etx_sysfs",kernel_kobj);
 
        /*Creating sysfs file for etx_value*/
        if(sysfs_create_file(kobj_ref,&etx_attr.attr)){
                printk(KERN_INFO"Cannot create sysfs file......\n");
                goto r_sysfs;
        }
        if (request_irq(IRQ_NO, irq_handler, IRQF_SHARED, "etx_device", (void *)(irq_handler))) {
            printk(KERN_INFO "my_device: cannot register IRQ ");
                    goto irq;
        }
 
        printk(KERN_INFO "Device Driver Insert...Done!!!\n");
        return 0;
 
irq:
        free_irq(IRQ_NO,(void *)(irq_handler));
 
r_sysfs:
        kobject_put(kobj_ref); 
        sysfs_remove_file(kernel_kobj, &etx_attr.attr);
 
r_device:
        class_destroy(dev_class);
r_class:
        unregister_chrdev_region(dev,1);
        cdev_del(&etx_cdev);   
        return -1;
}

/*
** Module exit function
*/  
static void __exit etx_driver_exit(void)
{
        /*Kill the Tasklet */ 
        tasklet_kill(&tasklet);
        free_irq(IRQ_NO,(void *)(irq_handler));
        kobject_put(kobj_ref); 
        sysfs_remove_file(kernel_kobj, &etx_attr.attr);
        device_destroy(dev_class,dev);
        class_destroy(dev_class);
        cdev_del(&etx_cdev);
        unregister_chrdev_region(dev, 1);
        printk(KERN_INFO "Device Driver Remove...Done!!!\n");
}
 
module_init(etx_driver_init);
module_exit(etx_driver_exit);
 
MODULE_LICENSE("GPL");
MODULE_AUTHOR("EmbeTronicX <embetronicx@gmail.com>");
MODULE_DESCRIPTION("A simple device driver - Tasklet Static");
MODULE_VERSION("1.15");
obj-m += tasklet_tdriver.o 
KDIR = /home/ldeng/Documents/runninglinuxkernel_4.0

all:
	make -C $(KDIR)  M=$(shell pwd) modules 
clean:
	make -C $(KDIR)  M=$(shell pwd) clean

这个例子比较综合,涉及以下知识点:

1.字符设备驱动框架

2.共享中断以及中断注册

3.tasklet注册和触发

4.sysfs文件注册和读写

八、tasklet实例——动态方法

/****************************************************************************//**
*  \file       driver.c
*
*  \details    Simple linux driver (Tasklet Dynamic method)
*
*  \author     EmbeTronicX
*
* *******************************************************************************/
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include<linux/slab.h>                 //kmalloc()
#include<linux/uaccess.h>              //copy_to/from_user()
#include<linux/sysfs.h> 
#include<linux/kobject.h> 
#include <linux/interrupt.h>
#include <asm/io.h>
 
 
#define IRQ_NO 11
 
void tasklet_fn(unsigned long); 

/* Tasklet by Dynamic Method */
struct tasklet_struct *tasklet = NULL;
 
 
/*Tasklet Function*/
void tasklet_fn(unsigned long arg)
{
        printk(KERN_INFO "Executing Tasklet Function : arg = %ld\n", arg);
}
 
 
//Interrupt handler for IRQ 11. 
static irqreturn_t irq_handler(int irq,void *dev_id) {
        printk(KERN_INFO "Shared IRQ: Interrupt Occurred");
        /*Scheduling Task to Tasklet*/
        tasklet_schedule(tasklet); 
        
        return IRQ_HANDLED;
}
 
 
volatile int etx_value = 0;
 
 
dev_t dev = 0;
static struct class *dev_class;
static struct cdev etx_cdev;
struct kobject *kobj_ref;
 
static int __init etx_driver_init(void);
static void __exit etx_driver_exit(void);
 
/*************** Driver Functions **********************/
static int etx_open(struct inode *inode, struct file *file);
static int etx_release(struct inode *inode, struct file *file);
static ssize_t etx_read(struct file *filp, 
                char __user *buf, size_t len,loff_t * off);
static ssize_t etx_write(struct file *filp, 
                const char *buf, size_t len, loff_t * off);
 
/*************** Sysfs Functions **********************/
static ssize_t sysfs_show(struct kobject *kobj, 
                struct kobj_attribute *attr, char *buf);
static ssize_t sysfs_store(struct kobject *kobj, 
                struct kobj_attribute *attr,const char *buf, size_t count);
 
struct kobj_attribute etx_attr = __ATTR(etx_value, 0660, sysfs_show, sysfs_store);

/*
** File operation sturcture
*/
static struct file_operations fops =
{
        .owner          = THIS_MODULE,
        .read           = etx_read,
        .write          = etx_write,
        .open           = etx_open,
        .release        = etx_release,
};

/*
** This function will be called when we read the sysfs file
*/
static ssize_t sysfs_show(struct kobject *kobj, 
                struct kobj_attribute *attr, char *buf)
{
        printk(KERN_INFO "Sysfs - Read!!!\n");
        return sprintf(buf, "%d", etx_value);
}

/*
** This function will be called when we write the sysfsfs file
*/   
static ssize_t sysfs_store(struct kobject *kobj, 
                struct kobj_attribute *attr,const char *buf, size_t count)
{
        printk(KERN_INFO "Sysfs - Write!!!\n");
        sscanf(buf,"%d",&etx_value);
        return count;
}

/*
** This function will be called when we open the Device file
*/   
static int etx_open(struct inode *inode, struct file *file)
{
        printk(KERN_INFO "Device File Opened...!!!\n");
        return 0;
}
 
/*
** This function will be called when we close the Device file
*/   
static int etx_release(struct inode *inode, struct file *file)
{
        printk(KERN_INFO "Device File Closed...!!!\n");
        return 0;
}

/*
** This function will be called when we read the Device file
*/  
static ssize_t etx_read(struct file *filp, 
                char __user *buf, size_t len, loff_t *off)
{
        printk(KERN_INFO "Read function\n");
        asm("int $0x3B");  // Corresponding to irq 11
        return 0;
}

/*
** This function will be called when we write the Device file
*/
static ssize_t etx_write(struct file *filp, 
                const char __user *buf, size_t len, loff_t *off)
{
        printk(KERN_INFO "Write Function\n");
        return len;
}
 
/*
** Module Init function
*/ 
static int __init etx_driver_init(void)
{
        /*Allocating Major number*/
        if((alloc_chrdev_region(&dev, 0, 1, "etx_Dev")) <0){
                printk(KERN_INFO "Cannot allocate major number\n");
                return -1;
        }
        printk(KERN_INFO "Major = %d Minor = %d \n",MAJOR(dev), MINOR(dev));
 
        /*Creating cdev structure*/
        cdev_init(&etx_cdev,&fops);
 
        /*Adding character device to the system*/
        if((cdev_add(&etx_cdev,dev,1)) < 0){
            printk(KERN_INFO "Cannot add the device to the system\n");
            goto r_class;
        }
 
        /*Creating struct class*/
        if((dev_class = class_create(THIS_MODULE,"etx_class")) == NULL){
            printk(KERN_INFO "Cannot create the struct class\n");
            goto r_class;
        }
 
        /*Creating device*/
        if((device_create(dev_class,NULL,dev,NULL,"etx_device")) == NULL){
            printk(KERN_INFO "Cannot create the Device 1\n");
            goto r_device;
        }
 
        /*Creating a directory in /sys/kernel/ */
        kobj_ref = kobject_create_and_add("etx_sysfs",kernel_kobj);
 
        /*Creating sysfs file for etx_value*/
        if(sysfs_create_file(kobj_ref,&etx_attr.attr)){
                printk(KERN_INFO"Cannot create sysfs file......\n");
                goto r_sysfs;
        }
        if (request_irq(IRQ_NO, irq_handler, IRQF_SHARED, "etx_device", (void *)(irq_handler))) {
            printk(KERN_INFO "etx_device: cannot register IRQ ");
            goto irq;
        }

        /* Init the tasklet bt Dynamic Method */
        tasklet  = kmalloc(sizeof(struct tasklet_struct),GFP_KERNEL);
        if(tasklet == NULL) {
            printk(KERN_INFO "etx_device: cannot allocate Memory");
            goto irq;
        }
        tasklet_init(tasklet,tasklet_fn,0);
 
        printk(KERN_INFO "Device Driver Insert...Done!!!\n");
        return 0;

 
irq:
        free_irq(IRQ_NO,(void *)(irq_handler));
 
r_sysfs:
        kobject_put(kobj_ref); 
        sysfs_remove_file(kernel_kobj, &etx_attr.attr);
 
r_device:
        class_destroy(dev_class);
r_class:
        unregister_chrdev_region(dev,1);
        cdev_del(&etx_cdev);        
        return -1;
}

/*
** Module exit function
*/ 
static void __exit etx_driver_exit(void)
{
        /* Kill the Tasklet */ 
        tasklet_kill(tasklet);

        if(tasklet != NULL)
        {
          kfree(tasklet);
        }
        free_irq(IRQ_NO,(void *)(irq_handler));
        kobject_put(kobj_ref); 
        sysfs_remove_file(kernel_kobj, &etx_attr.attr);
        device_destroy(dev_class,dev);
        class_destroy(dev_class);
        cdev_del(&etx_cdev);
        unregister_chrdev_region(dev, 1);
        printk(KERN_INFO "Device Driver Remove...Done!!!\n");
}
 
module_init(etx_driver_init);
module_exit(etx_driver_exit);
 
MODULE_LICENSE("GPL");
MODULE_AUTHOR("EmbeTronicX <embetronicx@gmail.com>");
MODULE_DESCRIPTION("A simple device driver - Tasklet Dynamic");
MODULE_VERSION("1.16");

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

denglin12315

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

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

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

打赏作者

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

抵扣说明:

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

余额充值