【libuv高效编程】libuv学习超详细教程8——libuv signal 信号句柄解读

libuv系列文章

linux信号

信号(signal),又称为软中断信号,用于通知进程发生了异步事件,它是Linux系统响应某些条件而产生的一个事件,它是在软件层次上对中断机制的一种模拟,是一种异步通信方式,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。

信号是进程间通信机制中唯一的异步通信机制,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。正如我们所了解的中断服务函数一样,在中断发生的时候,就会进入中断服务函数中去处理,同样的,当进程接收到一个信号的时候,也会相应地采取一些行动。我们可以使用术语"生成(raise)"表示一个信号的产生,使用术语"捕获(catch)"表示进程接收到一个信号。

在Linux系统中,信号可能是由于系统中某些错误而产生,也可以是某个进程主动生成的一个信号。由于某些错误条件而生成的信号:如内存段冲突、浮点处理器错误或非法指令等,它们由shell和终端处理器生成并且引起中断。由进程主动生成的信号可以作为在进程间传递通知或修改行为的一种方式,它可以明确地由一个进程发送给另一个进程,当进程捕获了这个信号就会按照程序进行相应并且去处理它。无论何种情况,它们的编程接口都是相同的,信号可以被生成、捕获、响应或忽略。进程之间可以互相发送信号,内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。

Linux信号种类与描述

信号值名称描述默认处理
1SIGHUP控制终端被关闭时产生。终止
2SIGINT程序终止(interrupt)信号,在用户键入INTR字符(通常是Ctrl + C)时发出,用于通知前台进程组终止进程。终止
3SIGQUITSIGQUIT 和SIGINT类似,但由QUIT字符(通常是Ctrl + \)来控制,进程在因收到SIGQUIT退出时会产生core文件,在这个意义上类似于一个程序错误信号。终止并产生转储文件(core文件)
4SIGILLCPU检测到某进程执行了非法指令时产生,通常是因为可执行文件本身出现错误, 或者试图执行数据段、堆栈溢出时也有可能产生这个信号。终止并产生转储文件(core文件)
5SIGTRAP由断点指令或其它trap指令产生,由debugger使用。终止并产生转储文件(core文件)
6SIGABRT调用系统函数 abort()时产生。终止并产生转储文件(core文件)
7SIGBUS总线错误时产生。一般是非法地址,包括内存地址对齐(alignment)出错。比如访问一个四个字长的整数,但其地址不是4的倍数。它与SIGSEGV的区别在于后者是由于对合法存储地址的非法访问触发的(如访问不属于自己存储空间或只读存储空间)。终止并产生转储文件(core文件)
8SIGFPE处理器出现致命的算术运算错误时产生,不仅包括浮点运算错误,还包括溢出及除数为0等其它所有的算术的错误。终止并产生转储文件(core文件)
9SIGKILL系统杀戮信号。用来立即结束程序的运行,本信号不能被阻塞、处理和忽略。如果管理员发现某个进程终止不了,可尝试发送这个信号将进程杀死。终止
10SIGUSR1用户自定义信号。终止
11SIGSEGV访问非法内存时产生,进程试图访问未分配给自己的内存,或试图往没有写权限的内存地址写数据。终止
12SIGUSR2用户自定义信号。终止
13SIGPIPE这个信号通常在进程间通信产生,比如采用FIFO(管道)通信的两个进程,读管道没打开或者意外终止就往管道写,写进程会收到SIGPIPE信号。此外用Socket通信的两个进程,写进程在写Socket的时候,读进程已经终止,也会产生这个信号。终止
14SIGALRM定时器到期信号,计算的是实际的时间或时钟时间,alarm函数使用该信号。终止
15SIGTERM程序结束(terminate)信号,与SIGKILL不同的是该信号可以被阻塞和处理。通常用来要求程序自己正常退出,shell命令kill缺省产生这个信号,如果进程终止不了,才会尝试SIGKILL。终止
16SIGSTKFLT已废弃。终止
17SIGCHLD子进程暂停或终止时产生,父进程将收到这个信号,如果父进程没有处理这个信号,也没有等待(wait)子进程,子进程虽然终止,但是还会在内核进程表中占有表项,这时的子进程称为僵尸进程,这种情况我们应该避免。父进程默认是忽略SIGCHILD信号的,我们可以捕捉它,做成异步等待它派生的子进程终止,或者父进程先终止,这时子进程的终止自动由init进程来接管。忽略
18SIGCONT系统恢复运行信号,让一个停止(stopped)的进程继续执行,本信号不能被阻塞,可以用一个handler来让程序在由stopped状态变为继续执行时完成特定的工作恢复运行
19SIGSTOP系统暂停信号,停止进程的执行。注意它和terminate以及interrupt的区别:该进程还未结束,只是暂停执行,本信号不能被阻塞,处理或忽略。暂停
20SIGTSTP由控制终端发起的暂停信号,停止进程的运行,但该信号可以被处理和忽略,比如用户键入SUSP字符时(通常是Ctrl+Z)发出这个信号。暂停
21SIGTTIN后台进程发起输入请求时控制终端产生该信号。暂停
22SIGTTOU后台进程发起输出请求时控制终端产生该信号。暂停
23SIGURG套接字上出现紧急数据时产生。忽略
24SIGXCPU处理器占用时间超出限制值时产生。终止并产生转储文件(core文件)
25SIGXFSZ文件尺寸超出限制值时产生。终止并产生转储文件(core文件)
26SIGVTALRM由虚拟定时器产生的虚拟时钟信号,类似于SIGALRM,但是计算的是该进程占用的CPU时间。终止
27SIGPROF类似于SIGALRM / SIGVTALRM,但包括该进程用的CPU时间以及系统调用的时间。终止
28SIGWINCH窗口大小改变时发出。忽略
29SIGIO文件描述符准备就绪, 可以开始进行输入/输出操作。终止
30SIGPWR启动失败时产生。终止
31SIGUNUSED非法的系统调用。终止并产生转储文件(core文件)

对于表格有几点需要注意的地方:

  1. 信号的“值”在 x86、PowerPC 和 ARM平台下是有效的,但是别的平台的信号值也许跟这个表的不一致。
  2. “描述”中注明的一些情况发生时会产生相应的信号,但并不是说该信号的产生就一定发生了这个事件。事实上,任何进程都可以使用kill()函数来产生任何信号。
  3. 信号 SIGKILL 和 SIGSTOP 是两个特殊的信号,他们不能被忽略、阻塞或捕捉,只能按缺省动作来响应。
  4. 一般而言,信号的响应处理过程如下:如果该信号被阻塞,那么将该信号挂起,不对其做任何处理,等到解除对其阻塞为止。如果该信号被捕获,那么进一步判断捕获的类型,如果设置了响应函数,那么执行该响应函数;如果设置为忽略,那么直接丢弃该信号。最后才执行信号的默认处理。

信号的处理

生成信号的事件一般可以归为3大类:程序错误、外部事件以及显式请求。例如零作除数、非法存储访问等,这种情况通常是由硬件而不是由Linux内核检测到的,但由内核向发生此错误的那个进程发送相应的信号;例如当用户在终端按下某些键时产生终端生成的信号,当进程超越了CPU或文件大小的限制时,内核会生成一个信号通知进程;例如使用kill()函数允许进程发送任何信号给其他进程或进程组。

信号的生成既可以是同步的,也可以是异步的。同步信号大多数是程序执行过程中出现了某个错误而产生的,由进程显式请求生成的给自己的信号也是同步的。

异步信号是接收进程可控制之外的事件所生成的信号,这类信号一般是进程无法控制的,只能被动接收,因为进程也不知道这个信号会何时发生,只能在发生的时候去处理它。一般外部事件总是异步地生成信号,异步信号可在进程运行中的任意时刻产生,进程无法预期信号到达的时刻,它所能做的只是告诉Linux内核假如有信号生成时应当采取什么行动(这相当于注册信号对应的处理)。

无论是同步还是异步信号,当信号发生时,我们可以告诉Linux内核采取如下3种动作中的任意一种:

  • 忽略信号。大部分信号都可以被忽略,但有两个除外:SIGSTOP和SIGKILL绝不会被忽略。不能忽略这两个信号的原因是为了给超级用户提供杀掉或停止任何进程的一种手段。此外,尽管其他信号都可以被忽略,但其中有一些却不宜忽略。例如,若忽略硬件例外(非法指令)信号,则会导致进程的行为不确定。
  • 捕获信号。这种处理是要告诉Linux内核,当信号出现时调用专门提供的一个函数。这个函数称为信号处理函数,它专门对产生信号的事件作出处理。
  • 让信号默认动作起作用。系统为每种信号规定了一个默认动作,这个动作由Linux内核来完成,有以下几种可能的默认动作:
  1. 终止进程并且生成内存转储文件,即写出进程的地址空间内容和寄存器上下文至进程当前目录下名为cone的文件中;
  2. 终止终止进程但不生成core文件。
  3. 忽略信号。
  4. 暂停进程。
  5. 若进程是暂停暂停,恢复进程,否则将忽略信号。

libuv的信号

因为libuv是一个跨平台的框架,它的底层处理可以在Windows、也可以在linux,所以libuv信号的实现也是视平台而定的,在这里我们只讲解linux平台下的处理,当然对应的Windows也是差不多的。

信号是有生命周期的,可以把信号当做一个handle,那么libuv的信号就是signal handle,如果创建了signal handle实例并且start了,那么当signal handle指定的信号发生时,将进入对应的回调函数去处理该信号,这与linux的信号处理是差不多的,只不过libuv在系统的处理之上进行抽象,形成与平台无关的处理,仅此而已。

关于libuv的signal handle有几个点要知悉:

  • 以编程方式调用raise()或abort()触发的信号不会被libuv检测到;所以这些信号不会对应的回调函数。

  • SIGKILL和SIGSTOP是不可能被捕捉到的。

  • 通过libuv处理SIGBUS、SIGFPE、SIGILL或SIGSEGV会导致未定义的行为。

  • libuv的信号与平台的信号基本上是一样的,也就是说信号可以从系统中其他进程发出。

  • libuv的信号依赖管道进行通信。

数据类型

uv_signal_t 是 thread handle 的数据类型,通过它可以定义一个 thread handle 的实例。

typedef struct uv_signal_s uv_signal_t;

libuv/include/uv.h文件中存在以下的定义,它继承了UV_HANDLE_FIELDS相关的字段,因此它属于handle,同时还定义了signal的回调函数signal_cb,以及记录触发的信号值signum,当然还有一个UV_SIGNAL_PRIVATE_FIELDS,其实就是定义了红黑树的数据结构与记录触发信号的次数与处理信号的次数。

struct uv_signal_s {
  UV_HANDLE_FIELDS
  uv_signal_cb signal_cb;
  int signum;
  UV_SIGNAL_PRIVATE_FIELDS
};

#define UV_SIGNAL_PRIVATE_FIELDS                                              \
  /* 红黑树的节点 */                                                          \
  struct {                                                                    \
    struct uv_signal_s* rbe_left;                                             \
    struct uv_signal_s* rbe_right;                                            \
    struct uv_signal_s* rbe_parent;                                           \
    int rbe_color;                                                            \
  } tree_entry;                                                               \
  /* 分别记录了触发信号的次数与处理信号的次数 */                               \
  unsigned int caught_signals;                                                \
  unsigned int dispatched_signals;

回调函数:

typedef void (*uv_signal_cb)(uv_signal_t* handle, int signum);
  • uv_signal_t:传入了触发信号的句柄。
  • signum:传入触发信号的值,这个值可能跟系统的值不一样,不过无所谓。

API

uv_signal_init()

int uv_signal_init(uv_loop_t* loop, uv_signal_t* handle);

初始化信号句柄,将signal handle绑定到指定的loop事件循环中。

具体的初始化操作过程是:libuv申请一个管道,用于其他进程(libuv进程或fork出来的进程)和libuv进程通信。然后往libuv的io观察者队列注册一个观察者,这其实就是观察这个管道是否可读,libuv在轮询I/O的阶段会把观察者加到epoll中。io观察者里保存了管道读端的文件描述符loop->signal_pipefd[0]和回调函数uv__signal_event。

int uv_signal_init(uv_loop_t* loop, uv_signal_t* handle) {
  int err;

  /* 初始化loop,它只会被初始化一次 */
  err = uv__signal_loop_once_init(loop);
  if (err)
    return err;

  /* 初始化handle的类型,并且插入loop的handle队列,因为所有的handle都会被放到该队列管理 */
  uv__handle_init(loop, (uv_handle_t*) handle, UV_SIGNAL);
  handle->signum = 0;
  handle->caught_signals = 0;
  handle->dispatched_signals = 0;

  return 0;
}

/* 申请和libuv的通信管道并且注册io观察者 */
static int uv__signal_loop_once_init(uv_loop_t* loop) {
  int err;

  /* 如果已经初始化则返回 */
  if (loop->signal_pipefd[0] != -1)
    return 0;

  /* 申请两个管道,用于其他进程和libuv主进程通信,并设置非阻塞标记 */
  err = uv__make_pipe(loop->signal_pipefd, UV__F_NONBLOCK);
  if (err)
    return err;

  /* 置信号io观察者的处理函数和文件描述符,libuv在循环I/O的时候,
     如果发现管道读端loop->signal_pipefd[0]可读,则执行对应的回调函数uv__signal_event */
  uv__io_init(&loop->signal_io_watcher,
              uv__signal_event,
              loop->signal_pipefd[0]);
  
  /* 插入libuv的signal io观察者队列,当管道可读的时候,执行uv__signal_event */
  uv__io_start(loop, &loop->signal_io_watcher, POLLIN);

  return 0;
}

uv_signal_start()

启动signal handle,并函数注册信号和对应的处理函数,并且设置信号句柄处于活跃状态。

int uv_signal_start(uv_signal_t* handle,
                    uv_signal_cb signal_cb,
                    int signum);
  • handle:信号句柄。
  • signal_cb:信号的回调函数。
  • signum:信号的值。
int uv_signal_start(uv_signal_t* handle, uv_signal_cb signal_cb, int signum) {
  return uv__signal_start(handle, signal_cb, signum, 0);
}

static int uv__signal_start(uv_signal_t* handle,
                            uv_signal_cb signal_cb,
                            int signum,
                            int oneshot) {
  sigset_t saved_sigmask;
  int err;
  uv_signal_t* first_handle;

  assert(!uv__is_closing(handle));

  /* 如果用户提供的signum == 0,则返回错误。 */ 
  if (signum == 0)
    return UV_EINVAL;

  /* 这个信号已经注册过了,重新设置回调处理函数就行。 */
  if (signum == handle->signum) {
    handle->signal_cb = signal_cb;
    return 0;
  }

  /* 如果信号处理程序已经处于活动状态,请先停止它。 */
  if (handle->signum != 0) {
    uv__signal_stop(handle);
  }

  /* 暂时屏蔽所有信号 */
  uv__signal_block_and_lock(&saved_sigmask);

  /* 如果此时没有用于该信号的活动信号监视程序(在任何循环中),则给进程注册一个信号和信号处理函数。主要是调用操作系统的sigaction()函数来处理的。 */
  first_handle = uv__signal_first_handle(signum);
  if (first_handle == NULL ||
      (!oneshot && (first_handle->flags & UV_SIGNAL_ONE_SHOT))) {
    err = uv__signal_register_handler(signum, oneshot);
    if (err) {
      /* 注册信号失败 */
      uv__signal_unlock_and_unblock(&saved_sigmask);
      return err;
    }
  }

  handle->signum = signum;

  /* 设置UV_SIGNAL_ONE_SHOT标记,表示libuv只响应一次信号 */
  if (oneshot)
    handle->flags |= UV_SIGNAL_ONE_SHOT;

  /* 插入红黑树 */
  RB_INSERT(uv__signal_tree_s, &uv__signal_tree, handle);

  /* 接触屏蔽信号 */
  uv__signal_unlock_and_unblock(&saved_sigmask);

  handle->signal_cb = signal_cb;

  /* 设置handle的标志UV_HANDLE_ACTIVE,表示处于活跃状态 */
  uv__handle_start(handle);

  return 0;
}

uv_signal_start_oneshot()

libuv只响应一次信号,在响应一次后恢复系统默认的信号处理。

int uv_signal_start_oneshot(uv_signal_t* handle,
                            uv_signal_cb signal_cb,
                            int signum) {
  return uv__signal_start(handle, signal_cb, signum, 1);
}

uv_signal_stop()

停止signal handle,将信号句柄设置为非活跃状态,事件循环中不在对它进行轮询。

int uv_signal_stop(uv_signal_t* handle);
int uv_signal_stop(uv_signal_t* handle) {
  assert(!uv__is_closing(handle));
  uv__signal_stop(handle);
  return 0;
}

static void uv__signal_stop(uv_signal_t* handle) {
  uv_signal_t* removed_handle;
  sigset_t saved_sigmask;
  uv_signal_t* first_handle;
  int rem_oneshot;
  int first_oneshot;
  int ret;

  /* 如果没有启动观察程序,则该操作无效。 */
  if (handle->signum == 0)
    return;

  /* 暂时屏蔽所有信号 */
  uv__signal_block_and_lock(&saved_sigmask);

  /* 从红黑树取出信号节点 */
  removed_handle = RB_REMOVE(uv__signal_tree_s, &uv__signal_tree, handle);
  assert(removed_handle == handle);
  (void) removed_handle;

  /* 检查是否还有其他活动的信号监视程序正在观察此信号。如果没有了则注销信号处理程序。*/
  first_handle = uv__signal_first_handle(handle->signum);
  if (first_handle == NULL) {

    /* 注销信号,还是依赖系统的函数sigaction() */
    uv__signal_unregister_handler(handle->signum);
  } else {
    rem_oneshot = handle->flags & UV_SIGNAL_ONE_SHOT;
    first_oneshot = first_handle->flags & UV_SIGNAL_ONE_SHOT;
    if (first_oneshot && !rem_oneshot) {
      ret = uv__signal_register_handler(handle->signum, 1);
      assert(ret == 0);
      (void)ret;
    }
  }

  /* 解除屏蔽所有信号 */
  uv__signal_unlock_and_unblock(&saved_sigmask);

  handle->signum = 0;
  uv__handle_stop(handle);
}

信号的处理过程

libuv的信号分为两个部分,一个部分是用于通知,另一部分才是真正的处理,当系统有信号到达后,libuv会通过管道通知到libuv的事件循环中,然后在事件循环中处理信号,这里的事件循环其实是一个笼统的概念,具体的处理是在poll io阶段,即I/O轮询阶段,因为在等待信号的过程中,它可能会进入阻塞状态。

在libuv的处理中,无论有什么信号到来,它都通过uv__signal_handler()函数去处理信号。为什么呢,因为我们在注册的时候是通过uv__signal_register_handler()函数进行注册的,而这个函数中就将对应的回调处理设置为uv__signal_handler()函数。

信号通知

static int uv__signal_register_handler(int signum, int oneshot) {
  /* When this function is called, the signal lock must be held. */
  struct sigaction sa;

  /* XXX use a separate signal stack? */
  memset(&sa, 0, sizeof(sa));
  if (sigfillset(&sa.sa_mask))
    abort();
  
  /* 注册回调函数uv__signal_handler */
  sa.sa_handler = uv__signal_handler;
  sa.sa_flags = SA_RESTART;

  if (oneshot)
    sa.sa_flags |= SA_RESETHAND;

  /* XXX save old action so we can restore it later on? */
  if (sigaction(signum, &sa, NULL))
    return UV__ERR(errno);

  return 0;
}

接下来看看uv__signal_handler()函数的处理过程,该函数遍历红黑树,找到注册了该信号的handle,然后封装一个msg写入管道(即libuv的通信管道)。信号的通知处理就完成了。我们看看这个函数的代码。:

static void uv__signal_handler(int signum) {
  uv__signal_msg_t msg;
  uv_signal_t* handle;
  int saved_errno;

  saved_errno = errno;
  memset(&msg, 0, sizeof msg);

  if (uv__signal_lock()) {
    errno = saved_errno;
    return;
  }

  /* 获取signal handle */
  for (handle = uv__signal_first_handle(signum);
       handle != NULL && handle->signum == signum;
       handle = RB_NEXT(uv__signal_tree_s, &uv__signal_tree, handle)) {
    int r;

    msg.signum = signum;
    msg.handle = handle;

    /* 往signal_pipefd管道写入数据,就是通知libuv,拿些signal handle需要处理信号,这是在事件循环中处理的 */
    do {
      r = write(handle->loop->signal_pipefd[1], &msg, sizeof msg);
    } while (r == -1 && errno == EINTR);

    assert(r == sizeof msg ||
           (r == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)));

    /* 记录该signal handle收到信号的次数 */
    if (r != -1)
      handle->caught_signals++;
  }

  uv__signal_unlock();
  errno = saved_errno;
}

信号处理

在信号通知完成后,事件循环中管道读取数据段有消息到达,此时事件循环将接收到消息,接下来在libuv的poll io阶段才做真正的处理。从uv__io_init()函数的处理过程得知,它把管道的读取端loop->signal_pipefd[0]看作是一个io观察者,在poll io阶段,epoll会检测到管道loop->signal_pipefd[0]是否可读,如果可读,然后会执行uv__signal_event()函数。在这个uv__signal_event()函数中,libuv将从管道读取刚才写入的一个个msg,从msg中取出对应的handle,然后执行里面保存的回调函数:

static void uv__signal_event(uv_loop_t* loop,
                             uv__io_t* w,
                             unsigned int events) {
  uv__signal_msg_t* msg;
  uv_signal_t* handle;
  char buf[sizeof(uv__signal_msg_t) * 32];
  size_t bytes, end, i;
  int r;

  bytes = 0;
  end = 0;

  /* 读取管道里的消息,处理所有的信号消息 */
  do {
    r = read(loop->signal_pipefd[0], buf + bytes, sizeof(buf) - bytes);

    if (r == -1 && errno == EINTR)
      continue;

    if (r == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) {

      if (bytes > 0)
        continue;

      return;
    }

    /* Other errors really should never happen. */
    if (r == -1)
      abort();

    bytes += r;

    end = (bytes / sizeof(uv__signal_msg_t)) * sizeof(uv__signal_msg_t);

    for (i = 0; i < end; i += sizeof(uv__signal_msg_t)) {
      msg = (uv__signal_msg_t*) (buf + i);
      handle = msg->handle;

      /* 如果收到的信号与预期的信号是一致的,则执行回调函数 */
      if (msg->signum == handle->signum) {
        assert(!(handle->flags & UV_HANDLE_CLOSING));

        /* signal 回调函数 */
        handle->signal_cb(handle, handle->signum);
      }
      
      /* 记录处理的信号个数 */
      handle->dispatched_signals++;

      /* 只响应一次,需要回复系统默认的处理函数 */
      if (handle->flags & UV_SIGNAL_ONE_SHOT)
        uv__signal_stop(handle);
    }

    bytes -= end;

    if (bytes) {
      memmove(buf, buf + end, bytes);
      continue;
    }
  } while (end == sizeof buf);
}

example

我们从example来讲解相关的函数使用吧,本次实验主要是是创建两个线程,其中一个线程等待SIGUSR1信号,另一个线程发送SIGUSR1信号,在处理完信号后退出。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <uv.h>

void signal_handler(uv_signal_t *handle, int signum)
{
    printf("signal received: %d\n", signum);
    uv_signal_stop(handle);
}

void thread1_entry(void *userp)
{
    sleep(2);

    kill(0, SIGUSR1);
}


void thread2_entry(void *userp)
{
    uv_signal_t signal;
    
    uv_signal_init(uv_default_loop(), &signal);
    uv_signal_start(&signal, signal_handler, SIGUSR1);
    
    uv_run(uv_default_loop(), UV_RUN_DEFAULT);
}

int main()
{
    uv_thread_t thread1, thread2;

    uv_thread_create(&thread1, thread1_entry, NULL);
    uv_thread_create(&thread2, thread2_entry, NULL);

    uv_thread_join(&thread1);
    uv_thread_join(&thread2);
    return 0;
}

参考

libuv源码解析之信号处理

例程代码获取

libuv-learning-code

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值