C++ Report:应用设计模式去简化信号控制

 译者:封尘浪

http://blog.csdn.net/qqzwp


 

--------------------------------------------------------------------------------

Applying Design Patterns to Simplify Signal Handling
应用设计模式去简化信号控制
Douglas C. Schmidt
Editor, Patterns++
C++ Report
April 1998
It's remarkable how many seemingly vexing programming problems can be resolved by knowledge of basic design patterns.
这很令人兴奋,多少令人苦恼的编程问题,都能通过简单的设计模式知识来解决。


The purpose of this article is to demonstrate how patterns like Singleton [GOF], Adapter [GOF], and Hook Method [Pree] can simplify
这篇文章的目的是演示,像Singleton Adapter 和 Hook Method这样的模式能简化开发组件,配合信号和信号句柄去避免平常遇到的欺骗和陷阱。
the development of components that avoid common traps and pitfalls associated with signals and signal handling.

 

--------------------------------------------------------------------------------


1. Technical Background on Signals
信号的技术背景
Signals are ``software interrupts'' that allow applications to handle various types of events asynchronously.
信号就是软件允许应用程序去操作各种各样的异步事件类型而造成的“中断”。
They are particularly useful for supporting multiple ``flow of control'' in single-threaded processes.

这儿是一些详细有用的支撑:多个在信号线程处理中控制,
 Many operating systems support signals, including all versions of UNIX and Win32. In addition, rudimentary support for signals is
许多操作系统支持信号,包括所有的类UNIX和Window32系统,最基本的信号支持定义在ANSI C 标准库和ANSI/ISO C++ 标准库中
defined in the ANSI C standard library and the ANSI/ISO C++ standard library.
There are a wide variety of signals, including:
这儿是一些普遍的信号包括:

SIGSEGV -- which is trapped by the hardware, e.g., when a NULL pointer is dereferenced;
SIGSEGV -- 当解除NULL引用时:这是通过硬件捕获的陷阱

SIGALRM -- which indicates the the expiration of a software timer;
SIGALRM -- 这个信号表示软件时间已满

SIGINT -- which occurs when a user types a break character, such as control-C.
SIGINT -- 这通常发生在用户使用了中断控制字符例如:Ctrl+C

Applications can register to handle these and other signals by passing callback functions to the signal or sigaction OS APIs
应用程序能够在注册后去控制其它的信号,通过回掉函数去激发信号或者sigaction 系统的API

[Stevens]. Likewise, applications can selectively ignore most signals by registering the SIG_IGN disposition with these OS APIs.
同样的,应用程序普遍会忽视许多信号,通过注册SIG_IGN来配置系统API

As with other OS APIs, such as thread creation routines, the standard signal handling APIs expect C-style functions, rather than C++
objects or C++ member functions.
作为其他的系统API,    比如线程创建程序,这些标准的信号控制API要求C风格的函数,和其他的一些C++对象或者数值函数

When the OS raises a signal that an application has registered to handle, the currently executing function is ``pinned'' by the signal handler function. To accomplish this, the OS first pushes a new activation record on top of the process' run-time stack. Next, it automatically invokes the application's pre-registered signal handler function. When this function completes, the OS pops off the signal handler context and returns control back to the point where the program was executing before the signal occurred.

当应用程序注册之后去控制时:系统激活一个信号,当前执行的函数被信号控制函数用指针保存了地址,来完成任务,系统第一次添加一个新的被激活的记录,放在进程的顶上,其实就是一个运行时栈,接下来,它自动的调用应用程序每次注册的信号函数,当这个函数完成使命时,系统弹出这个信号控制句柄,并且返回控制指针,指向之前那个执行程序的信号。


--------------------------------------------------------------------------------


2. Signal Handler Example
信号控制例子:
The following C code illustrates how to use the SIGINT and SIGQUIT signals to shutdown a UNIX application process when a user types
control-C or control-\, respectively. Much of the error handling logic has been omitted to save space and reduce unnecessary details.
下面的C代码用来说明,当一个用户使用诸如Ctrl+C 或者Ctrl+\时,怎样使用SIGINT和SIGQUIT信号去关闭各自的UNIX应用程序进程,许多的错误处理逻辑被省略了,用来保存空间,和减少不必要的说明。

 

/* Global variables that control  process shutdown.
   全局变量用来关闭进程
 */
sig_atomic_t graceful_quit = 0;
sig_atomic_t abortive_quit = 0;

/* Signal handler for SIGINT. */
void SIGINT_handler (int signum)
{
  assert (signum == SIGINT);
  graceful_quit = 1;
}

/* Signal handler for SIGQUIT. */
void SIGQUIT_handler (int signum)
{
  assert (signum == SIGQUIT);
  abortive_quit = 1;
}

/* ... */

int main (void)
{
  struct sigaction sa;
  sigemptyset (&sa.sa_mask);
  sa.sa_flags = 0;

  /* Register the handler for SIGINT. */
  sa.sa_handler = SIGINT_handler;
  sigaction (SIGINT, &sa, 0);

  /* Register the handler for SIGQUIT. */
  sa.sa_handler =  SIGQUIT_handler;
  sigaction (SIGQUIT, &sa, 0);

  /* Run the main event loop. */
  while (graceful_quit == 0 
         && abortive_quit == 0) 
    do_work ();

  if (abortive_quit == 1) {
    _exit (1);
  }
  else if graceful_quit {
    clean_up ();
    exit (0);
  }

  /* NOTREACHED */
}

 

This code registers the SIGINT_handler and SIGQUIT_handler functions using the standard POSIX sigaction library function. Once the signal handlers are registered, the UNIX OS run-time system collaborates to callback SIGINT_handler or SIGQUIT_handler asynchronously when SIGINT or SIGQUIT occur, respectively. In this particular example, the delivery of the SIGINT signal triggers a graceful shut down of the application's event loop. Likewise, the delivery of the SIGQUIT signal triggers an abortive shut down by calling _exit. Unlike exit, _exit doesn't cleanup global resources, such as C standard I/O buffers, etc.

这些代码注册了SIGINT_handler 和 SUGQUIT_handler 函数,使用了标准的POSIX信号处理函数库,这个信号处理控制被注册后,当SIGINT或者SUGQUIT各自发生时,UNIX运行时系统联合异步回调函数SIGINT_handler 或者 SIGQUIT_handler 。在这个特别的例子中,这个传送关闭应用程序事件循环
开启SIGQUIT的信号是很优雅的!同样的,传送SIGQUIT用来关闭程序的信号是失败的,不同于exit(), _exit()没有清除目标资源,比如C语言中标准的缓冲区。

2.1. Common Traps and Pitfalls
普通的欺骗和陷阱

Although the signal handling approach shown above works, its use of global variables is antithetical to good object-oriented design. For instance, note how each signal handler function must access the global variables graceful_quit and abortive_quit. In a large software system, the use of global variables can compromises encasulation and information hiding significantly. The use of global variables in C++ is also highly non-portable due to variation in the order of initialization of global statics.
In general, the use of global variables in signal handler functions in C++ programs causes an ``impedance mismatch.'' In particular, it presents a conflict between (1) the global-variables required to interact with signal handlers and (2) the well-structured OO design used for other application processing.

尽管这些信号控制接近真实的工作,但它使用的目标变量是对立的为了更好的面对这种设计,通过这些实例,注意到怎样对每个信号控制函数必须获取目标变量优雅的退出和失败的退出,在一个大的软件系统中,这些有用的目标变量能够适当的隐藏信息以便于封装,这些有用的目标变量在C++中也如此
频繁的使用,由于变量与初始化静态变量相似。
通常,这些有用的目标变量信号在C++控制函数中“impedance mismatch"(阻抗失配 不知道对不?),特别的,它现在产生了冲突,在(1)和目标变量中要求(2)与信号控制相互影响,这是一个对其他应用程序进程来讲比较好的结构设计。

2.2. Solution
解决

As usual, the way out of this programming morass is to apply several common design patterns, such as Singleton [GOF], Adapter [GOF], and Hook Method [Pree]. This section outlines the steps required to accomplish this task.

通常情况,这种方式由于申请各自的通用设计模式而使程序乱作一团,例如 Singleton, Adapter, 和 Hook Method 。这部分画线的步骤必须去完成
--------------------------------------------------------------------------------


A. Define a Hook Method Interface
定义一个钩子函数接口
-- We'll start out by defining an Event_Handler interface that contains, among other things, a virtual method called handle_signal:

我们开始定义一个事件接口,包含其他的事件,用一个虚函数调用handle_signal

class Event_Handler
{
public:
  // Hook method for the signal hook method.
  virtual int handle_signal (int signum) = 0;

  // ... other hook methods for other types of
  // events such as timers, I/O, and 
  // synchronization objects.
};


 

Programmers subclass from Event_Handler and override the handle_signal hook method to perform application-specific behavior in response to designated signals. Instances of these subclasses, i.e., concrete event handlers, are the targets of the hook method callback. These callbacks are invoked through a Signal_Handler singleton, which is described next.
 
程序员用子类,从Event_Handler 推翻这个handle_signal钩子函数,去执行特殊的程序行为响应指定的信号这只是一个实例,具体的事件句柄是钩子函数回调,回调是通过一个单独的 Signal_Handler 它使被下一个被处理的。


--------------------------------------------------------------------------------


B. Define a Signal Handler Component as a Singleton
定义一个单独的信号控制组件

-- The Signal_Handler component is a singleton that encapsulates a collection of concrete event handlers, which applications register to process various types of signals. The Signal_Handler implementation shown below is based on the Singleton [GOF], Adapter [GOF], and Hook Method [Pree] patterns. The Singleton pattern provides a factory for a singular (sole) instance. This pattern is used for the Signal_Handler component since standard OS processes that support signals have just one table that maps the occurrence of a signal to its C-style signal handler function.

这个Signal_Handler 组件是单独的,用来概括收集一个具体的事件控制,应用程序注册给进程许多不同类型的信号,Signal_Handler 定义显示在下面,基于这个Singleton, Adapter, 和钩子方法模式,这个单独的模式提供一个工厂为一个特殊的实例,这个模式对Signal_Handler组件是有用的,直到标准的系统进程支持信号,有table和map发生一个信号在C语言的信号处理函数中。

The Adapter pattern transforms one interface into another interface that is more appropriate for clients. This pattern is used in the Signal_Handler component to adapt the C-style signal handling functions required by sigaction into the C++ methods defined by concrete event handlers.

这个适配器模式改变一个接口进入到另一个接口,这对于用户来说是更合适的, 这种模式被应用在Signal_Handler 组件上,朝着适用于C风格的控制函数,通过要求信号处理进入C++方法定义的详细事件句柄。

The Hook Method pattern enables general-purpose algorithm code to perform some steps supplied by an application-specific callback method. This pattern decouples the generic signal registration and dispatching logic in the Signal_Handler component from the application-specific behavior performed when the handle_signal hook method is invoked on a concrete event handler.
这种钩子方法模式,通过使用一些特殊的应用回调函数, 能够写出一般目的的算法代码,执行一些步骤。使用这个模式,当handle_signal 钩子方法在具体的事件句柄中执行时,削弱了一般信号注册和迅速在Signal_Handler组件中发出逻辑。

The following C++ code illustrates the application of these patterns to develop the Signal_Handler singleton component.
下面的C++代码说明了,应用程序使用这个模式开发Signal_Handler组件

class Signal_Handler
{
public:
 // Entry point.
 static Signal_Handler *instance (void);

 // Register an event handler <eh> for <signum>
 // and return a pointer to any existing <Event_Handler>
 // that was previously registered to handle <signum>.
 Event_Handler *register_handler (int signum,
		                  Event_Handler *eh);

 // Remove the <Event_Handler> for <signum>
 // by setting the slot in the <signal_handlers_>
 // table to NULL.
 int remove_handler (int signum);

private:
 // Ensure we're a Singleton.
 Signal_Handler (void); 

 // Singleton pointer.
 static Signal_Handler *instance_;

 // Entry point adapter installed into <sigaction> 
 // (must be a static method or a stand-alone 
 // extern "C" function).
 static void dispatcher (int signum);

 // Table of pointers to concrete <Event_Handler>s
 // registered by applications.  NSIG is the number of 
 // signals defined in </usr/include/sys/signal.h>.
 static Event_Handler *signal_handlers_[NSIG];
};

 

Applications use the Signal_Handler's register_handler method to install the appropriate Event_Handler * for a particular signal. If an existing Event_Handler * is registered for this signal it is returned from the register_handler method. This design enables ``signal chaining,'' which makes it possible to temporarily replace the existing Event_Handler for a signal with another handler within the scope of a function or block. This feature is useful since signal handlers don't otherwise ``nest.''

应用程序使用Signal_Handler 的registr_handler 方法,为了专有的信号去初始化适当的Event_handler* ,如果一个现存的Event_Handler* 已经为该信号注册,那么它将会从这个register_handler中返回,这种设计能够链接信号,使他可能在信号或者其它函数和块中,暂时替换现存的Event_handler。 这个特点对那些没有很好的解决办法的人来说很有用。

The following example illustrates the use of signal chaining to ensure that the EPIPE is handled appropriately during a write system call to a network socket connection:

下面的例子用以说明,有用的信号链接确保了EPIPE在系统调用网络套接字链接时是可操作的。

class EPIPE_Handler : public Event_Handler
{
public:
  virtual int handle_signal (int signum);
  // Handle the SIGPIPE signal appropriately.

  // ...
};

EPIPE_Handler epipe_handler;

ssize_t
sigsafe_write (int socket, 
               const char *buf,
               size_t len)
{
  Event_Handler *oeh;

  // Temporarily replace any existing 
  // signal handler with a special handler
  // for the EPIPE signal for the duration 
  // of this function.
  oeh = Signal_Handler::instance ()->
          register_handler (SIGPIPE, 
                            &epipe_handler);

  ssize_t n = write (socket, buf, len);

  // Restore the original signal handler.
  Signal_Handler::instance ()->
    register_handler (SIGPIPE, oeh);

  return n;
}

 

 

As shown below, the register_handler method is responsible for registering the static method Signal_Handler::dispatcher for that signal with sigaction:


下面展示了,register_handler 方法对寄存器静态方法:Signal_Handler::dispatcher 的信号和信号处理负责。

 

Event_Handler *
Signal_Handler::register_handler (int signum,
      Event_Handler *eh)
{
  // Copy the <old_eh> from the <signum> slot in 
  // the <signal_handlers_> table.
  Event_Handler *old_eh =
    Signal_Handler::signal_handlers_[signum];

  // Store <eh> into the <signum> slot in the
  // <signal_handlers_> table.
  Signal_Handler::signal_handlers_[signum] = eh;
 
  // Register the <dispatcher> to handle this
  // <signum>.
  struct sigaction sa;
  sa.sa_handler = Signal_Handler::dispatcher;
  sigemptyset (&sa.sa_mask);
  sa.sa_flags = 0;
  sigaction (signum, &sa, 0);

  return old_eh;
}


The Signal_Handler's dispatcher method is called back by the signal dispatching mechanism in the OS when the designated signum is raised. This static method plays the role of an adapter, which transforms the C-style signal handling function required by sigaction into the C++ handle_signal method that is overridden by concrete event handlers. The dispatcher simply indexes into the <signum> slot of the <signal_handlers_> and invokes the concrete event handler's handle_signal method, as follows:

当指定的signum启用后, Signal_Handler 的 “dispatcher”方法被系统中通过信号派遣机制回调,这个静态方法扮演了适配器的角色,改变C风格的信号处理方法,信号处理机制要求C++ handle_signal控制具体的事件操作。这个操作简化了进入<signal_handlers_> <signum>位置行使具体的事件操作的handle_signal 方法

void Signal_Handler::dispatcher (int signum)
{
  // Perform a sanity check...
  if (Signal_handler::signal_handlers_[signum] != 0)
    // Dispatch the handler's hook method.
    Signal_handler::signal_handlers_
      [signum]->handle_signal (signum);
}
 

 

Note how the signum received by the dispatcher is passed through to the handle_signal hook method. This enables handle_signal to determine which signal it is handling. Therefore, the same concrete event handler can be programmed to handle different signals in different ways.

注意signum如何通过调度handle_signal钩子函数收到,这能使handle_signal通过信号处理作出决定,因此,相同的事件操作能够通过不同的方法编程实现。

 

--------------------------------------------------------------------------------


C. Define the Concrete Event Handlers
定义具体的事件操作

-- The next step is to define concrete event handlers that derive from Event_Handler. For example, the following is a concrete event handler that handles the SIGINT signal.

下一步是通过Event_handler定义具体的事件操作,举个例子,下面就是一个具体的通过控制SIGINT信号控制的事件。

class SIGINT_Handler : public Event_Handler
{
public:
  SIGINT_Handler (void)
    : graceful_quit_ (0) {}

  // Hook method.
  virtual int handle_signal (int signum)
  {
    assert (signum == SIGINT);
    this->graceful_quit_ = 1;
  }

  // Accessor.
  sig_atomic_t graceful_quit (void)
  { return this->graceful_quit_; }

private:
  sig_atomic_t graceful_quit_;
};

 

 

 

This concrete event handler maintains state for its graceful_quit_ data member. Its handle_signal hook method sets the value of graceful_quit_ to 1. As shown below, this value instructs the application to terminate gracefully.
The following is a concrete event handler that handles SIGQUIT.

这就是个具体的事件操作,它确保 graceful_quit_date 的数值,它的handle_signal 钩子函数把 graceful_quit_ 设置为1,如下所示,这个数值通知应用程序优雅的结束。
下面是个具体的SIGQUIT句柄事件操作。

 

class SIGQUIT_Handler : public Event_Handler
{
public:
  SIGQUIT_Handler (void)
    : abortive_quit_ (0) {}

  // Hook method.
  virtual int handle_signal (int signum)
  {
    assert (signum == SIGQUIT);
    this->abortive_quit_ = 1;
  }

  // Accessor.
  sig_atomic_t abortive_quit (void)
  { return this->abortive_quit_; }

private:
  sig_atomic_t abortive_quit_;
};


This concrete event handler works just like the SIGINT_Handler.
这个具体的事件就像SIGINT_handler一样工作


--------------------------------------------------------------------------------


D. Register Event Handler Subclasses with Signal Handler Singleton
注册具体事件子类和一个信号处理


-- An application can use the Signal_Handler component and the Event_Handler base class to define concrete event handlers and register them using Signal_Handler::instance, as follows:


一个应用程序能够使用 Signal_Handler 组件和 Event_Handler 基类,去定义具体的事件操作和使用Signal_Handler::instance 注册它们,请看下面的例子:

int main (void)
{
  SIGINT_Handler sigint_handler;
  SIGQUIT_Handler sigquit_handler;

  // Register the handler for SIGINT. 
  Signal_Handler::instance ()->register_handler 
    (SIGINT, &sigint_handler);

  // Register the handler for SIGQUIT.
  Signal_Handler::instance ()->register_handler 
    (SIGQUIT, &sigquit_handler);

  // Run the main event loop.
  while (sigint_handler.graceful_quit () == 0
         && sigquit_handler.abortive_quit () == 0) 
    do_work ();

  if (sigquit_handler.abortive_quit () == 1)
    _exit (1);
  else /* if sigint_handler.graceful_quit () */ {
    clean_up ();
    return 1;
  }
}

By using the Signal_Handler component shown above, we can leverage the Singleton, Adapter, and Hook Method patterns to invoke the handle_signal hook method of the concrete event handlers in response to SIGINT and SIGQUIT signals. This design is an improvement over the original C-style approach since it does not require any global or static variables. In particular, the state of each concrete event handler can be maintained locally within an object.
There are many variations on this basic design. For instance, we could support other sigaction semantics using various flags and sigmasks. Likewise, we could allowing multple Event_Handlers to be registered per-signal. In addition, we could support the POSIX extended signal API. However, the fundamental essence of all these solution involves recognizing the power of several basic patterns: Singleton, Adapter, and Hook Method.

通过使用上面 Signal_Handler 组件,我们能够让 Singleton,Adapter, 和Hook Method 模式去支持handle_signal 钩子方法作为回应SIGINT 和 SIGQUIT信号的具体事件的操作。自从它不要求任何全局静态变量开始,这项设计改善了原始的C风格,特别的,声明每个具体的事件操作能够保持对象的局部性。
在基础的设计中有许多变量,举个例子,我们为了支持其他不活动的性质,使用各自的标志和符号,同样的,我们允许多种 Event_Handlers 去注册每个信号,除此之外,我们能够支持POSIX扩展的信号API,然而,对于所有的问题的解决表明,基础的精华是 Singleton, Adapter 和 Hook Method这几个基础模式。

--------------------------------------------------------------------------------


3. Caveats
说明

Although signals are powerful and the Signal_Handler design presented in this article is flexible, the use of signals can still be quite problematic. In particular, signals are hard to program and use correctly. For instance, the operations that can occur in signal handler context are very limited. Therefore, highly portable programs should only set variables of type sig_atomic_t and then return. Likewise, POSIX defines a very limited set of system calls that can safely be invoked in a signal handler. Unfortunately, these constraints can be very limiting since signal handlers may want to perform arbitrary handling logic when a signal occurs.
Programming with signals also can be overly complex due to timing problems and race conditions when the current thread of control is preempted by a signal handler. Likewise, debugging programs that use asynchronous signals is hard since events occur at different points of time during program execution.

尽管信号很强大,Signal_Handler 在这篇文章中也很有说服力,但有用的信号依旧有几分瑕渍,特别的是,信号依赖于程序的正确使用,比如,操作符发生在信号控制的上下文中就非常有限,因此,频繁使用的程序因该仅设置变量的类型为sig_atomic_t 和然后再返回,同样的,POSIX定义的系统回调能够提供安全信号控制的设置也非常有限,不幸的是,约束非常有限,当信号发生时,信号也想执行随意的逻辑控制。当最进的信号控制线程控制抢占后,因为定时问题和环境的速度编程和信号也会极度复杂,同样的,自从事件程序执行发生在不同位置不同时间时,调试程序使用异步信号也是困难的。

However, if you decide that your applications require signals and signal handling, knowledge of the patterns and OO design components described in this article can be invaluable to simplify your programming tasks.

然而,如果你决定你的应用程序要求信号和信号处理时,这篇文章中模式知识和OO设计组件,用来简化你的程序工作是非常有益的。

--------------------------------------------------------------------------------


4. Concluding Remarks
结束语

The Signal_Handler and Event_Handler classes described in this article are loosely based on components in the ACE framework [Schmidt]. ACE is an OO framework that implements many core design patterns for concurrent communication software. It provides a rich set of reusable C++ wrappers and framework components that perform common communication software tasks across a range of OS platforms.

这篇文章中 Signal_Handler 和 Event_Handler 类在基于ACE框架的组件被轻而易举的描述。ACE 是一个OO框架,为通信软件实现了许多核心设计模式,他提供了一个丰富的可重用的C++封装和框架组件,利用系统平台可以用来执行普通的通信软件工作。

The communication software tasks provided by ACE include event demultiplexing and event handler dispatching, signal handling, service initialization, interprocess communication, shared memory management, message routing, dynamic (re)configuration of distributed services, concurrent execution and synchronization. ACE is freely available via the WWW at URLwww.cs.wustl.edu/~schmidt/ACE.html.
这个通信软件,通过ACE包括事件多路化和控制执行工作,提供,信号处理,服务初始化,进程间的通信,分享内存管理,消息发送,分布式动态服务,
并行执行,和同步,ACE是免费的可用的链接是:
www.cs.wustl.edu/~schmidt/ACE.html.

Thanks to Achint Sandhu <sandhu@nortel.ca> for comments on this paper.

 

--------------------------------------------------------------------------------


References
[GoF] E. Gamma, R. Helm, R. Johnson, and J. Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software, Reading, MA: Addison-Wesley, 1995.

[Pree] Wolfgang Pree, Design Patterns for Object-Oriented Software Development, Addison-Wesley, Reading, MA, 1994.

[Schmidt] Douglas C. Schmidt, ACE: an Object-Oriented Framework for Developing Distributed Applications, Proceedings of the 6th USENIX C++ Technical Conference, Cambridge, Massachusetts, April, 1994.

[Stevens] W. Richard Stevens, ``UNIX Network Programming,'' Second Edition, Prentice Hall, Englewood Cliffs, NJ, 1997.

 


--------------------------------------------------------------------------------

Back to C++ Report Editorials home page.

Last modified 11:34:51 CDT 28 September 2006

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值