libevent源码分析(7)--2.1.8--信号事件处理机制分析

一、信号绑定机制
     简单来说,就是将外部信号转换为内部IO事件来处理。
     由于信号捕捉函数是全局绑定的,所以没办法像IO事件一样,将IO事件和文件描述符绑定在一起,而libevent又需要将IO事件、信号事件、定时事件都采用事件触发机制来实现,那么对于信号事件来说,就需要一层中间中转来实现,将信号事件在通知event_base时转换成IO事件,具体来说,就是在信号绑定时没办法改变全局绑定的实现,只能在通知event_base上作出改变了:
(1)libevent在内部创建信号通知管道,用于内部传递捕捉到的信号
(2)管道的两端分别是event_base和信号捕捉函数
(3)将管道的一端文件描述符和信号触发事件的回调函数绑定在一块,注册到event_base,一旦管道中有信号传来,回调函数就会处理管道中的信号,因此event_base只需要后台方法监听此内部管道即可得知是否有信号事件来临。
(4)在添加信号事件时,会将信号绑定到全局信号捕捉函数,信号捕捉函数中的处理逻辑是一旦捕捉到信号,就将信号写入内部通知管道,这样,信号捕捉函数只需要将信号写入管道,就可以通知event_base信号发生。

具体来说就是:


信号     —>     信号捕捉函数     —>     写入内部管道     —>     内部信号回调函数     —>     激活外部信号回调函数



二、evsig_init
evsig_init函数就是初始化内部信号事件,epoll、poll、select等后台方法在初始化时都会执行内部信号事件的初始化,为下一步信号事件注册做准备。
// 主要是将信号事件base->sig.ev_signal[0]绑定到base上,信号关联文件描述符;
// base->sig.ev_signal_pair是文件描述符对,一个是用来读取数据的,一个是用来写数据的;
// 其中base->sig.ev_signal_pair[0],即用来读取信号的;
// base->sig.ev_signal_pair[1]是用来写信号的,此处只是绑定0,因为这是信号接收端
// 只有真有外部信号事件注册时,才会绑定base->sig.ev_signal_pair[0]

int
evsig_init_(struct event_base *base)
{
    /*
     * Our signal handler is going to write to one end of the socket
     * pair to wake up our event loop.  The event loop then scans for
     * signals that got delivered.
     */
     // 信号句柄将会写入socket对的其中一个,用来唤醒event loop;
     // event loop会检查这些传输的信号
     // 创建内部使用的通知管道,将创建好的文件描述符存在ev_signal_pair中
     // 下面会分析
    if (evutil_make_internal_pipe_(base->sig.ev_signal_pair) == -1) {
#ifdef _WIN32
        /* Make this nonfatal on win32, where sometimes people
           have localhost firewalled. */
        event_sock_warn(-1, "%s: socketpair", __func__);
#else
        event_sock_err(1, -1, "%s: socketpair", __func__);
#endif
        return -1;
    }

     // 如果存在原有信号句柄则释放,并置空
     // base->sig类型为:
     // typedef void (*ev_sighandler_t)(int);

     /* Data structure for the default signal-handling implementation in signal.c
      */
     //struct evsig_info {
     //    /* Event watching ev_signal_pair[1] */
     //     监听ev_signal_pair[1]的事件,即内部信号管道监听事件
     //    struct event ev_signal;
     //   /* Socketpair used to send notifications from the signal handler */
     //    内部信号管道的两端,0的一端用来读信号,1的一端用来写信号,
     //    evutil_socket_t ev_signal_pair[2];
     //    /* True iff we've added the ev_signal event yet. */
     //     如果我们已经增添了ev_signal则为真,即注册了外部信号事件的话
     //    int ev_signal_added;
     //    /* Count of the number of signals we're currently watching. */
     //      用来统计当前event_base到底监听了多少信号
     //    int ev_n_signals_added;

      //   /* Array of previous signal handler objects before Libevent started
      //    * messing with them.  Used to restore old signal handlers. */
     //     在libevent开始信号处理之前的原有信号句柄
     //#ifdef EVENT__HAVE_SIGACTION
      //        struct sigaction **sh_old;
     //#else
      //   ev_sighandler_t **sh_old;
     //#endif
     //    /* Size of sh_old. */
     //     原有的信号句柄的最大个数
     //    int sh_old_max;
     // };

    if (base->sig.sh_old) {
        mm_free(base->sig.sh_old);
    }
    base->sig.sh_old = NULL;
    base->sig.sh_old_max = 0;

     // 将信号事件base->sig.ev_signal绑定在base上,关联文件描述符实base->sig.ev_signal_pair[0],
     // ev_signal_pair[0]是用来读的,事件类型是EV_READ|EV_PERSIST,包括可读和持久化,事件发生
     //  时的回调函数是evsig_cb函数,回调函数的参数是base
     // 这里就说明了,base->sig.ev_signal是内部事件,是用来外部信号发生时,通知内部信号处理函数的;
     // 其实就是将外部信号转换为内部IO事件来处理
     // 下篇文章分析
    event_assign(&base->sig.ev_signal, base, base->sig.ev_signal_pair[0],
        EV_READ | EV_PERSIST, evsig_cb, base);

     // 设置信号事件的事件状态为EVLIST_INTERNAL,即内部事件,区分内外部事件
    base->sig.ev_signal.ev_flags |= EVLIST_INTERNAL;
     // 设置信号事件的优先级为0,即最高优先级,信号事件优先处理
    event_priority_set(&base->sig.ev_signal, 0);

     // 设置信号事件的后台处理方法,evsigops
     // 下文分析信号处理后台方法的具体执行
     // static const struct eventop evsigops = {
    //     "signal",
    //     NULL,
    //     evsig_add,
    //     evsig_del,
    //     NULL,
    //     NULL,
    //     0, 0, 0
     //     };
     // 下文分析evsig_add 和     evsig_del函数
     // 每当信号事件注册时,就会调用evsig_add将信号注册到event_base中
    base->evsigsel = &evsigops;

    return 0;
}




三、evutil_make_internal_pipe_函数
主要是创建内部管道,用来作为外部信号捕捉函数与内部IO事件回调函数之间通知管道
/* Internal function: Set fd[0] and fd[1] to a pair of fds such that writes on
 * fd[1] get read from fd[0].  Make both fds nonblocking and close-on-exec.
 * Return 0 on success, -1 on failure.
 */
// 内部函数:设置fd[0] 和fd[1]为文件描述符对,这样就可以从fd[0]读,从fd[1]写,
// 这两个fds的模式都为nonblocking以及close-on-exec
// close-on-exec: 子进程在fork父进程时,使用写时复制方式获取父进程的数据空间、堆和栈副本,
// 其中包括文件描述符,刚fork成功时,父子进程中相同的文件描述符指向文件表中的同一项(即共享同一
// 文件偏移量),这其中当然也包括父进程创建的socket;接着,一般会调用exec执行另外一个程序,此时
// 会用全新的程序替换子进程的正文、数据、堆和栈等,此时保存的文件描述符的变量当然也不存在了,我们
// 就无法关闭无用的文件描述符了,通常会使用fork子进程后在子进程中直接执行close关掉无用的文件描述符,
// 然后在执行exec。但是复杂系统中,有时fork子进程时不知道打开了多少个文件描述符,逐一清理的话比较难,
// 期望是fork子进程前打开某个文件描述符时就指定好:此描述符在fork子进程执行exec时就关闭,即指定
// 打开模式为close-on-exec。
int
evutil_make_internal_pipe_(evutil_socket_t fd[2])
{
    /*
      Making the second socket nonblocking is a bit subtle, given that we
      ignore any EAGAIN returns when writing to it, and you don't usally
      do that for a nonblocking socket. But if the kernel gives us EAGAIN,
      then there's no need to add any more data to the buffer, since
      the main thread is already either about to wake up and drain it,
      or woken up and in the process of draining it.
    */
// 指定第二个socket非阻塞有一些微妙,这使得我们当写这个socket时需要忽略任何EAGAIN返回结果,
// 而一般情况下不能对非阻塞socket这样处理。但是如果内核返回EAGAIN,那么不需要向缓存中添加
// 任何数据,因为主线程已经或者打算唤醒然后读完它,或者已经被唤醒并正在读取它。

// 如果有PIPE2接口,则使用PIPE2接口创建
#if defined(EVENT__HAVE_PIPE2)
    if (pipe2(fd, O_NONBLOCK|O_CLOEXEC) == 0)
        return 0;
#endif
// 如果没有PIPE2,有PIPE,则使用PIPE接口创建
#if defined(EVENT__HAVE_PIPE)
    if (pipe(fd) == 0) {
        if (evutil_fast_socket_nonblocking(fd[0]) < 0 ||
            evutil_fast_socket_nonblocking(fd[1]) < 0 ||
            evutil_fast_socket_closeonexec(fd[0]) < 0 ||
            evutil_fast_socket_closeonexec(fd[1]) < 0) {
            close(fd[0]);
            close(fd[1]);
            fd[0] = fd[1] = -1;
            return -1;
        }
        return 0;
    } else {
        event_warn("%s: pipe", __func__);
    }
#endif

// 如果既没有PIPE2,也没有PIPE,则使用套接字
#ifdef _WIN32
#define LOCAL_SOCKETPAIR_AF AF_INET
#else
#define LOCAL_SOCKETPAIR_AF AF_UNIX
#endif
    if (evutil_socketpair(LOCAL_SOCKETPAIR_AF, SOCK_STREAM, 0, fd) == 0) {
        if (evutil_fast_socket_nonblocking(fd[0]) < 0 ||
            evutil_fast_socket_nonblocking(fd[1]) < 0 ||
            evutil_fast_socket_closeonexec(fd[0]) < 0 ||
            evutil_fast_socket_closeonexec(fd[1]) < 0) {
            evutil_closesocket(fd[0]);
            evutil_closesocket(fd[1]);
            fd[0] = fd[1] = -1;
            return -1;
        }
        return 0;
    }
    fd[0] = fd[1] = -1;
    return -1;
}



四、event_assign函数分析

/**
  Prepare a new, already-allocated event structure to be added.
  The function event_assign() prepares the event structure ev to be used
  in future calls to event_add() and event_del().  Unlike event_new(), it
  doesn't allocate memory itself: it requires that you have already
  allocated a struct event, probably on the heap.  Doing this will
  typically make your code depend on the size of the event structure, and
  thereby create incompatibility with future versions of Libevent.
  The easiest way to avoid this problem is just to use event_new() and
  event_free() instead.
  A slightly harder way to future-proof your code is to use
  event_get_struct_event_size() to determine the required size of an event
  at runtime.
  Note that it is NOT safe to call this function on an event that is
  active or pending.  Doing so WILL corrupt internal data structures in
  Libevent, and lead to strange, hard-to-diagnose bugs.  You _can_ use
  event_assign to change an existing event, but only if it is not active
  or pending!
  The arguments for this function, and the behavior of the events that it
  makes, are as for event_new().
  @param ev an event struct to be modified
  @param base the event base to which ev should be attached.
  @param fd the file descriptor to be monitored
  @param events desired events to monitor; can be EV_READ and/or EV_WRITE
  @param callback callback function to be invoked when the event occurs
  @param callback_arg an argument to be passed to the callback function
  @return 0 if success, or -1 on invalid arguments.
  @see event_new(), event_add(), event_del(), event_base_once(),
    event_get_struct_event_size()
  */
// 准备新的已经分配的事件结构体以添加到event_base中;
// 函数event_assign准备事件结构体ev,以备将来event_add和event_del调用。
// 不像event_new,他没有自己分配内存:他需要你已经分配了一个事件结构体,应该是在堆上分配的;
// 这样做一般会让你的代码依赖于事件结构体的尺寸,并且会造成与未来版本不兼容。
// 避免自己分配出现问题的最简单的办法是使用event_new和event_free来申请和释放。
// 让代码未来兼容的稍微困难的方式是使用event_get_struct_event_size来确定运行时事件的尺寸。
// 注意,如果事件处于激活或者未决状态,调用这个函数是不安全的。这样会破坏libevent内部数据结构,
// 导致一些奇怪的,难以调查的问题。你只能使用event_assign来改变一个存在的事件,但是事件状态不能
// 是激活或者未决的。
// ev:需要修正的事件结构体
// base: 将ev添加到的event_base
// fd: 需要监控的文件描述符
// events: 需要监控的事件类型;可以是EV_READ或者EV_WRITE
// callback:当事件发生时的回调函数
// callback_arg: 传递给回调函数的参数,一般是event_base句柄
// 如果成功则返回0,如果失败则返回-1
// 相关查看 event_new,event_add,event_del,event_base_once,event_get_struct_event_size

int
event_assign(struct event *ev, struct event_base *base, evutil_socket_t fd, short events, void (*callback)(evutil_socket_t, short, void *), void *arg)
{
    if (!base)
        base = current_base;
    if (arg == &event_self_cbarg_ptr_)
        arg = ev;

    event_debug_assert_not_added_(ev);

     // 事件结构体初始设置
    ev->ev_base = base;

    ev->ev_callback = callback;
    ev->ev_arg = arg;
    ev->ev_fd = fd;
    ev->ev_events = events;
    ev->ev_res = 0;
    ev->ev_flags = EVLIST_INIT;
    ev->ev_ncalls = 0;
    ev->ev_pncalls = NULL;

     // 如果是信号事件
    if (events & EV_SIGNAL) {
          // 信号事件与IO事件不能同时存在
        if ((events & (EV_READ|EV_WRITE|EV_CLOSED)) != 0) {
            event_warnx("%s: EV_SIGNAL is not compatible with "
                "EV_READ, EV_WRITE or EV_CLOSED", __func__);
            return -1;
        }
          // 设置事件关闭时执行回调函数的类型:evcb_callback
        ev->ev_closure = EV_CLOSURE_EVENT_SIGNAL;
    } else {
     // 如果是其它类型的事件:IO事件、定时事件

          // 如果事件是永久事件,即每次调用之后不会移除出事件列表
          // 清空IO超时控制,并设置事件关闭时回调函数类型:evcb_callback
        if (events & EV_PERSIST) {
            evutil_timerclear(&ev->ev_io_timeout);
            ev->ev_closure = EV_CLOSURE_EVENT_PERSIST;
        } else {
               // 设置事件回调函数类型:evcb_callback
            ev->ev_closure = EV_CLOSURE_EVENT;
        }
    }

     // 事件超时控制最小堆初始化
    min_heap_elem_init_(ev);

     // 默认情况下,事件优先级设置为中间优先级
    if (base != NULL) {
        /* by default, we put new events into the middle priority */
        ev->ev_pri = base->nactivequeues / 2;
    }

    event_debug_note_setup_(ev);

    return 0;
}


五、evsig_cb
内部信号通知管道检测到有信号发生时的回调函数
/* Callback for when the signal handler write a byte to our signaling socket */
// 当信号句柄往信号处理socket中写入字节时的回调函数
// arg一般是event_base句柄
// 每当注册信号发生时,就会将该信号绑定的回调函数插入到激活队列中
static void
evsig_cb(evutil_socket_t fd, short what, void *arg)
{
     // 用来存储信号通知管道的数据,每个字节代表一个信号
    static char signals[1024];
    ev_ssize_t n;
    int i;
     // NSIG是linux系统定义的常量
     // 用来存储捕捉的信号,每个数组变量代表一个信号
    int ncaught[NSIG];
    struct event_base *base;

    base = arg;

     // 清空存储捕捉信号的缓存
    memset(&ncaught, 0, sizeof(ncaught));

     // 循环读取信号管道中的信号,直到读完或者无法读取为止。
     // 并将读取的信号,并记录信号发生的次数
    while (1) {
#ifdef _WIN32
        n = recv(fd, signals, sizeof(signals), 0);
#else
        n = read(fd, signals, sizeof(signals));
#endif
        if (n == -1) {
            int err = evutil_socket_geterror(fd);
            if (! EVUTIL_ERR_RW_RETRIABLE(err))
                event_sock_err(1, fd, "%s: recv", __func__);
            break;
        } else if (n == 0) {
            /* XXX warn? */
            break;
        }
          // 遍历缓存中的每个字节,转换成信号值
          // 如果信号值合法,则表示该信号发生一次,增加该信号发生次数
        for (i = 0; i < n; ++i) {
            ev_uint8_t sig = signals[i];
            if (sig < NSIG)
                ncaught[sig]++;
        }
    }

    EVBASE_ACQUIRE_LOCK(base, th_base_lock);
     // 遍历当前信号发生次数,激活与信号相关的事件,实际上是将
     // 信号相关的回调函数插入到激活事件队列中。
    for (i = 0; i < NSIG; ++i) {
        if (ncaught[i])
            evmap_signal_active_(base, i, ncaught[i]);
    }
    EVBASE_RELEASE_LOCK(base, th_base_lock);
}



六、evsig_add

当执行evsigsel->add的时候,其实是调用的这个函数。
当注册信号事件时,会调用evsig_add函数,主要是将信号事件注册到event_base上,
同时将信号和捕捉信号的函数通过signal系统函数绑定到一起,一旦捕捉信号的函数捕捉到信号,
则会将信号写入内部信号通知管道,然后会触发内部信号通知事件,内部信号通知事件会从内部信号通知管道读取信号,
然后根据信号值,触发相应外部信号事件回调函数
// base:绑定到哪个event_base上
// evsignal:需要绑定的信号
// old:该信号上的老的事件类型
// events:该信号上当前的事件类型
// p:目前尚未用到
static int
evsig_add(struct event_base *base, evutil_socket_t evsignal, short old, short events, void *p)
{
     // 这里实际上是内部信号通知事件,用于将外部信号转换为内部事件的具体实现
    struct evsig_info *sig = &base->sig;
    (void)p;

    EVUTIL_ASSERT(evsignal >= 0 && evsignal < NSIG);

    /* catch signals if they happen quickly */
    EVSIGBASE_LOCK();
    if (evsig_base != base && evsig_base_n_signals_added) {
        event_warnx("Added a signal to event base %p with signals "
            "already added to event_base %p.  Only one can have "
            "signals at a time with the %s backend.  The base with "
            "the most recently added signal or the most recent "
            "event_base_loop() call gets preference; do "
            "not rely on this behavior in future Libevent versions.",
            base, evsig_base, base->evsel->name);
    }

     // 以下是三个全局变量,都是信号事件的
     // 一个是event_base句柄
     // 一个当前event_base中增加的信号事件的总数
     // 一个是信号事件的文件描述符,其实就是上文中提到的base->sig.ev_signal_pair[1],即用来写入信号的内部管道
    evsig_base = base;
    evsig_base_n_signals_added = ++sig->ev_n_signals_added;
    evsig_base_fd = base->sig.ev_signal_pair[1];
    EVSIGBASE_UNLOCK();

    event_debug(("%s: %d: changing signal handler", __func__, (int)evsignal));
     // 将信号注册到信号捕捉函数evsig_handler上
     // evsig_handler会捕捉信号,然后将信号写入base->sig.ev_signal_pair[1],用来通知内部管道,进而通知event_base处理
    if (evsig_set_handler_(base, (int)evsignal, evsig_handler) == -1) {
        goto err;
    }

     // 时间参数为NULL,表明这不是一个超时时间,只有当注册事件描述符有读写行为时,更准确的说,
     //  只有当内部信号通知管道发生写事件时,才会触发回调函数去读信号,进而去激活外部信号注册的回调函数。
     // 这是具体的事件添加,需要查看事件注册相关文档
     // 这里实际上是在添加外部信号事件时,会添加内部信号通知事件,目的是为了将外部信号事件借助内部管道
     //  将捕捉到的外部信号转换为内部IO事件
     // 如果event_base上还没有注册内部通知事件,则需要注册;如果已经注册过了,就不需要重复注册了。
     // 这就说明,只有当注册外部信号事件,才会触发注册内部信号通知事件,否则内部通知事件只申请而不会使用;
    if (!sig->ev_signal_added) {
        if (event_add_nolock_(&sig->ev_signal, NULL, 0))
            goto err;
        sig->ev_signal_added = 1;
    }

    return (0);

err:
    EVSIGBASE_LOCK();
    --evsig_base_n_signals_added;
    --sig->ev_n_signals_added;
    EVSIGBASE_UNLOCK();
    return (-1);
}





七、evsig_set_handler_
设置外部信号的捕捉处理句柄
/* Helper: set the signal handler for evsignal to handler in base, so that
 * we can restore the original handler when we clear the current one. */
// 在event_base中为信号设置信号处理句柄,这样当清除当前信号时,就可以重新存储一般的信号处理句柄
int
evsig_set_handler_(struct event_base *base,
    int evsignal, void (__cdecl *handler)(int))
{
#ifdef EVENT__HAVE_SIGACTION
    struct sigaction sa;
#else
    ev_sighandler_t sh;
#endif

    struct evsig_info *sig = &base->sig;
    void *p;

    /*
     * resize saved signal handler array up to the highest signal number.
     * a dynamic array is used to keep footprint on the low side.
     */
     // 重新调整信号句柄数组大小,以适应最大的信号值。
     // 动态数组用来适应当前最大的信号值
     // 一旦当前信号值大于原有最大信号值,则需要重新申请空间
    if (evsignal >= sig->sh_old_max) {
        int new_max = evsignal + 1;
        event_debug(("%s: evsignal (%d) >= sh_old_max (%d), resizing",
                __func__, evsignal, sig->sh_old_max));
        p = mm_realloc(sig->sh_old, new_max * sizeof(*sig->sh_old));
        if (p == NULL) {
            event_warn("realloc");
            return (-1);
        }

        memset((char *)p + sig->sh_old_max * sizeof(*sig->sh_old),
            0, (new_max - sig->sh_old_max) * sizeof(*sig->sh_old));

        sig->sh_old_max = new_max;
        sig->sh_old = p;
    }

    /* allocate space for previous handler out of dynamic array */
     // 为新增的信号分配空间
    sig->sh_old[evsignal] = mm_malloc(sizeof *sig->sh_old[evsignal]);
    if (sig->sh_old[evsignal] == NULL) {
        event_warn("malloc");
        return (-1);
    }

    /* save previous handler and setup new handler */
     // 保存以前的信号句柄,并配置新的信号句柄
     // 将新增信号和信号处理函数handler绑定到一起
#ifdef EVENT__HAVE_SIGACTION
    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = handler;
    sa.sa_flags |= SA_RESTART;
    sigfillset(&sa.sa_mask);

    if (sigaction(evsignal, &sa, sig->sh_old[evsignal]) == -1) {
        event_warn("sigaction");
        mm_free(sig->sh_old[evsignal]);
        sig->sh_old[evsignal] = NULL;
        return (-1);
    }
#else
    if ((sh = signal(evsignal, handler)) == SIG_ERR) {
        event_warn("signal");
        mm_free(sig->sh_old[evsignal]);
        sig->sh_old[evsignal] = NULL;
        return (-1);
    }
    *sig->sh_old[evsignal] = sh;
#endif

    return (0);
}



八、evsig_handler
这是外部信号发生时的处理函数,主要用于将信号值写入base->sig.ev_signal_pair[1],以达到通知外部信号发生的目的
static void __cdecl
evsig_handler(int sig)
{
    int save_errno = errno;
#ifdef _WIN32
    int socket_errno = EVUTIL_SOCKET_ERROR();
#endif

    ev_uint8_t msg;

    if (evsig_base == NULL) {
        event_warnx(
            "%s: received signal %d, but have no base configured",
            __func__, sig);
        return;
    }

#ifndef EVENT__HAVE_SIGACTION
    signal(sig, evsig_handler);
#endif

    /* Wake up our notification mechanism */
    // 唤醒通知机制
     // 使用写入内部管道的消息保存信号值
    msg = sig;
#ifdef _WIN32
    send(evsig_base_fd, (char*)&msg, 1, 0);
#else
    {
          // 将消息写入内部管道文件描述符,注意是一个字节,因为信号使用一个字节就可以表示了
          // evsig_base_fd在前面就已经设置了
        int r = write(evsig_base_fd, (char*)&msg, 1);
        (void)r; /* Suppress 'unused return value' and 'unused var' */
    }
#endif
    errno = save_errno;
#ifdef _WIN32
    EVUTIL_SET_SOCKET_ERROR(socket_errno);
#endif
}







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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值