select机制的驱动实现及原理

一、驱动实现select机制的步骤

    1、首先初始化一个等待队列头
    2、在驱动中实现poll函数,该函数只需做两件事情
        a、使用poll_wait()函数将等待队列添加到poll_table中。
        b、返回描述设备是否可读或可写的掩码。
    3、在驱动的相应地方调用wake_up()函数,唤醒等待队列。

    两点说明:
        a、等待队列
            select函数阻塞的原理,实际上是通过等待队列实现的,若对等待队列不熟悉,请看我的另一篇文章《等待队列的简单使用》。否则看以下的 “select机制内核代码走读” 会很吃力。
        b、掩码值及含义
            POLLIN
            如果设备可被不阻塞地读, 这个位必须设置.

            POLLRDNORM
            这个位必须设置, 如果"正常"数据可用来读. 一个可读的设备返回( POLLIN|POLLRDNORM ).

            POLLRDBAND
            这个位指示带外数据可用来从设备中读取. 当前只用在 Linux 内核的一个地方( DECnet 代码 )并且通常对设备驱动不可用.

            POLLPRI
            高优先级数据(带外)可不阻塞地读取. 这个位使 select 报告在文件上遇到一个异常情况, 因为 selct 报告带外数据作为一个异常情况.

            POLLHUP
            当读这个设备的进程见到文件尾, 驱动必须设置 POLLUP(hang-up). 一个调用 select 的进程被告知设备是可读的, 如同 selcet 功能所规定的.

            POLLERR
            一个错误情况已在设备上发生. 当调用 poll, 设备被报告位可读可写, 因为读写都返回一个错误码而不阻塞.
            
            POLLOUT
            这个位在返回值中设置, 如果设备可被写入而不阻塞.

            POLLWRNORM
            这个位和 POLLOUT 有相同的含义, 并且有时它确实是相同的数. 一个可写的设备返回( POLLOUT|POLLWRNORM).

            POLLWRBAND
            如同 POLLRDBAND , 这个位意思是带有零优先级的数据可写入设备. 只有 poll 的数据报实现使用这个位, 因为一个数据报看传送带外数据.

            注:应当重复一下 POLLRDBAND 和 POLLWRBAND 仅仅对关联到 socket 的文件描述符有意义: 通常设备驱动不使用这些标志!


二、以按键驱动为例

        

        驱动代码button.c

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/types.h>
#include <linux/fcntl.h>
#include <linux/mm.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/errno.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/init.h>
#include <linux/major.h>
#include <linux/delay.h>
#include <linux/io.h>
#include <asm/uaccess.h>
#include <linux/poll.h>
#include <linux/irq.h>
#include <asm/irq.h>
#include <linux/interrupt.h>
#include <asm/uaccess.h>
#include <linux/platform_device.h>
#include <linux/cdev.h>
#include <linux/miscdevice.h>
#include <linux/sched.h>
#include <linux/gpio.h>
#include <asm/gpio.h>
 
#define BUTTON_NAME "poll_button"
#define BUTTON_GPIO 140
 
static int button_major = 0;                              
static int button_minor = 0;
static struct cdev button_cdev;                               
static struct class *p_button_class = NULL;            
static struct device *p_button_device = NULL;    
 
static struct timer_list button_timer;
static volatile int ev_press = 0;
static volatile char key_value[] = {0};
static int old_value;
static int Button_Irq = 0;
static int flag_interrupt = 1;
 
static DECLARE_WAIT_QUEUE_HEAD(button_waitq);
 
static irqreturn_t buttons_interrupt(int irq, void *dev_id)
{
    if(flag_interrupt) {
        flag_interrupt = 0;
        old_value = gpio_get_value(BUTTON_GPIO);
        mod_timer(&button_timer,jiffies + HZ/100);    //启动消抖定时器,消抖时间10ms
    }
    
    return IRQ_RETVAL(IRQ_HANDLED);
}
 
static void button_timer_handle(unsigned long arg)
{
    int tmp_value;
    
    tmp_value = gpio_get_value(BUTTON_GPIO);
    
    if(tmp_value == old_value) {
        key_value[0] = tmp_value;        
        ev_press= 1;                                 //有按键按下,唤醒等待队列
        wake_up_interruptible(&button_waitq);        
    }
 
    flag_interrupt = 1;        
}
 
static int button_open(struct inode *inode,struct file *file)
{
    Button_Irq = gpio_to_irq(BUTTON_GPIO);
    enable_irq(Button_Irq);
 
    if(request_irq(Button_Irq, buttons_interrupt, IRQF_TRIGGER_FALLING, "BUTTON_IRQ", NULL) != 0) {
        printk("request irq failed !!! \n");
        disable_irq(Button_Irq);
        free_irq(Button_Irq, NULL);
        return -EBUSY;
    }
    
    return 0;
}
 
static int button_close(struct inode *inode, struct file *file)
{
    free_irq(Button_Irq, NULL);
    return 0;
}
 
 
static int button_read(struct file *filp, char __user *buff, size_t count, loff_t *offp)
{
    unsigned long err; 
 
    if (filp->f_flags & O_NONBLOCK) {        
        /*nothing to do*/
        //如果没有使用select机制,并且应用程序设置了非阻塞O_NONBLOCK,那么驱动这里就不使用等待队列进行等待。
    } else { 
        wait_event_interruptible(button_waitq, ev_press); //如果应用层没有使用select,直接读的话,这里会阻塞,直到按键按下。如果使用select机制,进来这里时ev_press为真,不会阻塞。 
    }
     
    err = copy_to_user(buff, (const void *)key_value, min(sizeof(key_value), count)); 
    key_value[0] = 0; 
    ev_press = 0; 
     
    return err ? -EFAULT : min(sizeof(key_value), count);
}
 
static unsigned int button_poll(struct file *file, struct poll_table_struct *wait)
{
    unsigned int mask = 0;
 
    //将等待队列添加到poll_table中
    poll_wait(file, &button_waitq, wait);
 
    if(ev_press) {    
        //返回描述设备是否可读或可写的掩码    
        mask = POLLIN | POLLRDNORM;
    }
 
    return mask;
}
 
static const struct file_operations button_fops = {
    .owner = THIS_MODULE,
    .open = button_open,
    .release = button_close,
    .read = button_read,
    .poll = button_poll,
    //.write = button_write,
    //.ioctl = button_ioctl
};
 
static int button_setup_cdev(struct cdev *cdev, dev_t devno)
{
    int ret = 0;
 
    cdev_init(cdev, &button_fops);
    cdev->owner = THIS_MODULE;
    ret = cdev_add(cdev, devno, 1);
 
    return ret;
}
 
static int __init button_init(void)
{
    int ret;
    dev_t devno;
    
    printk("button driver init...\n");
 
    init_timer(&button_timer);
    button_timer.function = &button_timer_handle;
        
    if(button_major) {
        devno = MKDEV(button_major, button_minor);
        ret = register_chrdev_region(devno, 1, BUTTON_NAME);
    } else {
        ret = alloc_chrdev_region(&devno, button_minor, 1, BUTTON_NAME);
        button_major = MAJOR(devno);        
    }
    
    if(ret < 0) {
        printk("get button major failed\n");
        return ret;
    }
 
    ret = button_setup_cdev(&button_cdev, devno);
    if(ret) {
        printk("button setup cdev failed, ret = %d\n",ret);
        goto cdev_add_fail;
    }
 
    p_button_class = class_create(THIS_MODULE, BUTTON_NAME);
    ret = IS_ERR(p_button_class);
    if(ret) {
        printk(KERN_WARNING "button class create failed\n");
        goto class_create_fail;
    }
    p_button_device = device_create(p_button_class, NULL, devno, NULL, BUTTON_NAME);
    ret = IS_ERR(p_button_device);
    if (ret) {
        printk(KERN_WARNING "button device create failed, error code %ld", PTR_ERR(p_button_device));
        goto device_create_fail;
    }
 
    return 0;
    
device_create_fail:
    class_destroy(p_button_class);
class_create_fail:
    cdev_del(&button_cdev);
cdev_add_fail:
    unregister_chrdev_region(devno, 1);
    return ret;
}
 
static void __exit button_exit(void)
{
    dev_t devno;
 
    printk("button driver exit...\n");
    
    del_timer_sync(&button_timer);    
    devno = MKDEV(button_major, button_minor);    
    device_destroy(p_button_class, devno);
    class_destroy(p_button_class);
    cdev_del(&button_cdev);
    unregister_chrdev_region(devno, 1);
}
 
module_init(button_init);
module_exit(button_exit);
 
MODULE_AUTHOR("Jimmy");
MODULE_DESCRIPTION("button Driver");
MODULE_LICENSE("GPL");

        驱动Makefile文件
ifneq ($(KERNELRELEASE),)
obj-m := button.o
else
KERNELDIR ?= /ljm/git_imx6/linux-fsl/src/linux-3-14-28-r0
TARGET_CROSS = arm-none-linux-gnueabi-

PWD := $(shell pwd)

default:
    $(MAKE) ARCH=arm CROSS_COMPILE=$(TARGET_CROSS) -C $(KERNELDIR) M=$(PWD) modules

endif

install:
    $(MAKE) ARCH=arm CROSS_COMPILE=$(TARGET_CROSS) -C $(KERNELDIR) M=$(PWD) modules_install

clean:
    rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions *.symvers *.order

        应用程序main.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <linux/ioctl.h>
 
#define DEV_BUTTON "/dev/poll_button"
 
int main(void)
{
    int dev_fd;
    int ret;
    char read_buf[20] = {-1};
    struct timeval rto;
    fd_set read_fds;
 
    rto.tv_sec = 10;
    rto.tv_usec = 0;
    
    dev_fd = open(DEV_BUTTON, O_RDWR /*| O_NONBLOCK*/);
    if ( dev_fd == -1 ) {
        printf("open %s failed, ret = %d\n", DEV_BUTTON, dev_fd);
        return -1;
    }
 
    while(1)
    {
        rto.tv_sec =10;
        rto.tv_usec = 0;
        
        FD_ZERO(&read_fds);
        FD_SET(dev_fd, &read_fds);
        
        ret = select(dev_fd+1, &read_fds, NULL, NULL, &rto);
        if(ret == -1) {
            printf("error\n");
            continue;
        } else if(ret == 0) {
            printf("timeout\n");
            continue;
        } else {
            if(FD_ISSET(dev_fd, &read_fds)) {
                read(dev_fd, read_buf, 1);
                printf("button pressed, val = %d\n", read_buf[0]);
            }
        }
    }
    
    printf("clsoe %s\n", DEV_BUTTON);
    close(dev_fd);
    
    return 0;
}

        应用程序Makefile
WORKDIR  = 
INCLUDES = -I.
LIBS     = 
LINKS    = -lpthread

CC = arm-none-linux-gnueabi-gcc
TARGET = main

src=$(wildcard *.c ./callback/*.c)
C_OBJS=$(patsubst %.c, %.o,$(src))
#C_OBJS=$(dir:%.c=%.o)

compile:$(TARGET)
    
$(C_OBJS):%.o:%.c
    $(CC) $(CFLAGS) $(INCLUDES) -o $*.o -c $*.c
    
$(TARGET):$(C_OBJS)
    $(CC) -o $(TARGET) $^ $(LIBS) $(LINKS) 

    @echo 
    @echo Project has been successfully compiled.
    @echo
    
install: $(TARGET)
    cp $(TARGET) $(INSTALL_PATH)

uninstall:
    rm -f $(INSTALL_PATH)/$(TARGET)

rebuild: clean compile

clean:
    rm -rf *.o  $(TARGET) *.log *~


三、select的整体流程


    应用层的select函数会调用到内核函数do_select,do_select调用驱动的poll函数,若poll函数返回的掩码不可读写,那么do_select进入睡眠阻塞。要从睡眠中醒来并且跳出,有两种情况:a、超时跳出;b、驱动中唤醒等待队列,这时do_select再次调用poll函数,如果poll函数返回的掩码可读写,那么就跳出阻塞,否则继续睡眠。注意:上述是在select函数设成阻塞的情况,select函数可以设置成非阻塞的(将select函数的timeout参数设置成0)。

四、select机制内核代码走读
    调用顺序如下select() -> core_sys_select() -> do_select() -> fop->poll()
    
    1、select函数解析


<pre name="code" class="objc">SYSCALL_DEFINE5(select, int, n, fd_set __user *, inp, fd_set __user *, outp,
    fd_set __user *, exp, struct timeval __user *, tvp)
{
 
       struct timespec end_time, *to = NULL;
       struct timeval tv;
       int ret;
 
       if (tvp) {// 如果超时值非NULL
 
              if (copy_from_user(&tv, tvp, sizeof(tv)))   // 从用户空间取数据到内核空间
                     return -EFAULT;
 
              to = &end_time;
 
              // 得到timespec格式的未来超时时间
 
              if (poll_select_set_timeout(to,
                            tv.tv_sec + (tv.tv_usec / USEC_PER_SEC),
                            (tv.tv_usec % USEC_PER_SEC) * NSEC_PER_USEC))
 
                     return -EINVAL;
 
       }
 
       ret = core_sys_select(n, inp, outp, exp, to);             // 关键函数
 
       ret = poll_select_copy_remaining(&end_time, tvp, 1, ret);
 
       /*如果有超时值, 并拷贝离超时时刻还剩的时间到用户空间的timeval中*/
 
       return ret;             // 返回就绪的文件描述符的个数
}   

   2、core_sys_select函数解析
int core_sys_select(int n, fd_set __user *inp, fd_set __user *outp,
                        fd_set __user *exp, struct timespec *end_time)
 
{
    fd_set_bits fds;
 
    /**
    typedef struct {
        unsigned long *in, *out, *ex;
        unsigned long *res_in, *res_out, *res_ex;
    } fd_set_bits;
    这个结构体中定义的全是指针,这些指针都是用来指向描述符集合的。
    **/
 
    void *bits;
    int ret, max_fds;
    unsigned int size;
    struct fdtable *fdt;
 
    /* Allocate small arguments on the stack to save memory and be faster */
 
    long stack_fds[SELECT_STACK_ALLOC/sizeof(long)];
    // 256/32 = 8, stack中分配的空间
 
    /**
        @ include/linux/poll.h
        #define FRONTEND_STACK_ALLOC     256
        #define SELECT_STACK_ALLOC    FRONTEND_STACK_ALLOC
    **/
 
    ret = -EINVAL;
 
    if (n < 0)
        goto out_nofds;
 
    /* max_fds can increase, so grab it once to avoid race */
 
    rcu_read_lock();
    fdt = files_fdtable(current->files); // RCU ref, 获取当前进程的文件描述符表
    max_fds = fdt->max_fds;
    rcu_read_unlock();
 
    if (n > max_fds)                     // 如果传入的n大于当前进程最大的文件描述符,给予修正
        n = max_fds;
 
 
 
    /*
    * We need 6 bitmaps (in/out/ex for both incoming and outgoing),
    * since we used fdset we need to allocate memory in units of
    * long-words.
    */
 
    size = FDS_BYTES(n);
 
    // 以一个文件描述符占一bit来计算,传递进来的这些fd_set需要用掉多少个字
 
    bits = stack_fds;
 
    if (size > sizeof(stack_fds) / 6) {
        // 除6,为什么?因为每个文件描述符需要6个bitmaps
        /* Not enough space in on-stack array; must use kmalloc */
        ret = -ENOMEM;
        bits = kmalloc(6 * size, GFP_KERNEL); // stack中分配的太小,直接kmalloc
        if (!bits)
            goto out_nofds;
    }
 
    // 这里就可以明显看出struct fd_set_bits结构体的用处了。
    fds.in      = bits;
    fds.out     = bits +   size;
    fds.ex      = bits + 2*size;
    fds.res_in  = bits + 3*size;
    fds.res_out = bits + 4*size;
    fds.res_ex  = bits + 5*size;
 
    // get_fd_set仅仅调用copy_from_user从用户空间拷贝了fd_set
 
    if ((ret = get_fd_set(n, inp, fds.in)) ||
        (ret = get_fd_set(n, outp, fds.out)) ||
        (ret = get_fd_set(n, exp, fds.ex)))
        goto out;
 
    zero_fd_set(n, fds.res_in);  // 对这些存放返回状态的字段清0
    zero_fd_set(n, fds.res_out);
    zero_fd_set(n, fds.res_ex);
 
    ret = do_select(n, &fds, end_time);    // 关键函数,完成主要的工作
    if (ret < 0)                           // 有错误
        goto out;
 
    if (!ret) {                            // 超时返回,无设备就绪
        ret = -ERESTARTNOHAND;
        if (signal_pending(current))
            goto out;
        ret = 0;
    }
 
    // 把结果集,拷贝回用户空间
 
    if (set_fd_set(n, inp, fds.res_in) ||
        set_fd_set(n, outp, fds.res_out) ||
        set_fd_set(n, exp, fds.res_ex))
        ret = -EFAULT;
 
out:
    if (bits != stack_fds)
        kfree(bits);               // 如果有申请空间,那么释放fds对应的空间
 
out_nofds:
    return ret;                    // 返回就绪的文件描述符的个数
}


    3、do_select函数解析
int do_select(int n, fd_set_bits *fds, struct timespec *end_time)
 
{
 
       ktime_t expire, *to = NULL;
 
       struct poll_wqueues table;
 
       poll_table *wait;
 
       int retval, i, timed_out = 0;
 
       unsigned long slack = 0;
 
 
 
       rcu_read_lock();
 
       // 根据已经设置好的fd位图检查用户打开的fd, 要求对应fd必须打开, 并且返回
       // 最大的fd。
 
       retval = max_select_fd(n, fds);
 
       rcu_read_unlock();
 
 
 
       if (retval < 0)
 
              return retval;
 
       n = retval;
 
 
 
       // 一些重要的初始化:
 
       // poll_wqueues.poll_table.qproc函数指针初始化,该函数是驱动程序中poll函数实
 
       // 现中必须要调用的poll_wait()中使用的函数。
 
       poll_initwait(&table);
 
       wait = &table.pt;
 
       if (end_time && !end_time->tv_sec && !end_time->tv_nsec) {
 
              wait = NULL;
 
              timed_out = 1;     // 如果系统调用带进来的超时时间为0,那么设置
 
                                          // timed_out = 1,表示不阻塞,直接返回。
 
       }
 
 
 
       if (end_time && !timed_out)
              slack = estimate_accuracy(end_time); // 超时时间转换
 
 
 
       retval = 0;
 
       for (;;) {
 
              unsigned long *rinp, *routp, *rexp, *inp, *outp, *exp;
 
              inp = fds->in; outp = fds->out; exp = fds->ex;
 
              rinp = fds->res_in; routp = fds->res_out; rexp = fds->res_ex;
 
              // 所有n个fd的循环
 
              for (i = 0; i < n; ++rinp, ++routp, ++rexp) {
 
                     unsigned long in, out, ex, all_bits, bit = 1, mask, j;
 
                     unsigned long res_in = 0, res_out = 0, res_ex = 0;
 
                     const struct file_operations *f_op = NULL;
 
                     struct file *file = NULL;
 
 
 
                     // 先取出当前循环周期中的32个文件描述符对应的bitmaps
 
                     in = *inp++; out = *outp++; ex = *exp++;
 
                     all_bits = in | out | ex;  // 组合一下,有的fd可能只监测读,或者写,或者e rr,或者同时都监测
 
                     if (all_bits == 0) {  // 这32个描述符没有任何状态被监测,就跳入下一个32个fd的循环中
 
                            i += __NFDBITS; //每32个文件描述符一个循环,正好一个long型数
 
                            continue;
                     }
 
 
 
                     // 本次32个fd的循环中有需要监测的状态存在
                     for (j = 0; j < __NFDBITS; ++j, ++i, bit <<= 1) {// 初始bit = 1
 
                            int fput_needed;
 
                            if (i >= n)      // i用来检测是否超出了最大待监测的fd
 
                                   break;
 
                            if (!(bit & all_bits))
 
                                   continue; // bit每次循环后左移一位的作用在这里,用来跳过没有状态监测的fd
 
                            file = fget_light(i, &fput_needed); // 得到file结构指针,并增加引用计数字段f_count
 
                            if (file) {        // 如果file存在
 
                                   f_op = file->f_op;
 
                                   mask = DEFAULT_POLLMASK;
 
                                   if (f_op && f_op->poll) {
 
                                          wait_key_set(wait, in, out, bit);// 设置当前fd待监测的事件掩码
 
                                          mask = (*f_op->poll)(file, wait);
 
                                          /*
                                          调用驱动程序中的poll函数,以evdev驱动中的
                                          evdev_poll()为例该函数会调用函数poll_wait(file, &evdev->wait, wait),
                                          继续调用__pollwait()回调来分配一个poll_table_entry结构体,该结构体有一个内嵌的等待队列项,
                                          设置好wake时调用的回调函数后将其添加到驱动程序中的等待队列头中。
                                          */
 
                                   }
 
                                   fput_light(file, fput_needed);
 
                                   // 释放file结构指针,实际就是减小他的一个引用计数字段f_count。
                                   // mask是每一个fop->poll()程序返回的设备状态掩码。
 
                                   if ((mask & POLLIN_SET) && (in & bit)) {
 
                                          res_in |= bit;         // fd对应的设备可读
 
                                          retval++;
 
                                          wait = NULL;       // 后续有用,避免重复执行__pollwait()
 
                                   }
 
                                   if ((mask & POLLOUT_SET) && (out & bit)) {
 
                                          res_out |= bit;              // fd对应的设备可写
 
                                          retval++;
 
                                          wait = NULL;
 
                                   }
 
                                   if ((mask & POLLEX_SET) && (ex & bit)) {
 
                                          res_ex |= bit;
 
                                          retval++;
 
                                          wait = NULL;
 
                                   }
 
                            }
 
                     }
 
                     // 根据poll的结果写回到输出位图里,返回给上级函数
 
                     if (res_in)
 
                            *rinp = res_in;
 
                     if (res_out)
 
                            *routp = res_out;
 
                     if (res_ex)
 
                            *rexp = res_ex;
 
                     /*
                            这里的目的纯粹是为了增加一个抢占点。
                            在支持抢占式调度的内核中(定义了CONFIG_PREEMPT),
                            cond_resched是空操作。
                     */
 
                     cond_resched();
 
              }
 
              wait = NULL;  // 后续有用,避免重复执行__pollwait()
 
              if (retval || timed_out || signal_pending(current))
 
                     break;
 
              if (table.error) {
 
                     retval = table.error;
 
                     break;
 
              }
 
              /*跳出这个大循环的条件有: 有设备就绪或有异常(retval!=0), 超时(timed_out
              = 1), 或者有中止信号出现*/
 
 
 
              /*
               * If this is the first loop and we have a timeout
               * given, then we convert to ktime_t and set the to
               * pointer to the expiry value.
               */
 
              if (end_time && !to) {
 
                     expire = timespec_to_ktime(*end_time);
 
                     to = &expire;
 
              }
 
 
 
              // 第一次循环中,当前用户进程从这里进入休眠,
 
              // 上面传下来的超时时间只是为了用在睡眠超时这里而已
 
              // 超时,poll_schedule_timeout()返回0;被唤醒时返回-EINTR
 
              if (!poll_schedule_timeout(&table, TASK_INTERRUPTIBLE,
 
                                      to, slack))
 
                     timed_out = 1; /* 超时后,将其设置成1,方便后面退出循环返回到上层 */
 
       }
 
       // 清理各个驱动程序的等待队列头,同时释放掉所有空出来的page页(poll_table_entry)
 
       poll_freewait(&table);
 
       return retval; // 返回就绪的文件描述符的个数
}    

--------------------- 
作者:o倚楼听风雨o 
来源:CSDN 
原文:https://blog.csdn.net/silent123go/article/details/52577648 
版权声明:本文为博主原创文章,转载请附上博文链接!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值