Linux C编程笔记

定义有参数的宏时,应该注意:

1、  宏名与形参表的圆括号间不能有空格,否则会导致错误。如:#define MUL(x,y) (x)*(y)MUL”(“之间不能有空格。

2、  在宏定义中,字符串内的形式参数最好用括号括起来,以避免错误。

带参数的宏与函数的比较

1、  有参数宏的形参不是变量,不分配内存空间,无需说明数据类型。而函数的形参是变量,要分配空间,在函数定义时要指明参数的数据类型。

2、  预处理程序认为有参数宏的实参是字符串,并用它去替换形参。

如:#define MUL(x,y) x*y

c = MUL(a+1,b+1)

         a+1去替换x,而不是先计算a+1的值再去替换x。如果是函数,则先计算a+1的值,再把这个值传递给x

3、  使用宏的次数较多时,宏替换后源程序一般会变长。而函数调用不会使程序变长。宏替换不会占用运行时间,只是占用编译时间。而函数调用则会占用运行时间,一般用宏来代表一些较为简单的表达式较合适。

共用体

共用体中声明的变量共同使用一块内存,即同一块内存可用来存放几种不同类型的数据,但有在某时刻只能在其中存放一个成员变量。共用体变量中起作用的成员是最后一次存入的数据。

不能把共用体变量作为函数参数,也不能使函数返回共用体变量,但可以使用指向共用体变量的指针。共用体类型的变量可以出现在结构体的声明中,也可以作为数组元素的类型。反之,结构体也可以出现共用体类型的声明中,数组也可以作为共用体的成员。

 

位域

C语言允许在一个结构体中以位为单位使用内存,这种以位为单位的成员称为位域或位段。在某些内存受限的开发环境中,使用位域是节约内存的一种重要手段。

struct bit_data

{

         int a:6;

         int b:4;

         int c:4;

         int d;

};

存储空间如下:

a

b

c

空闲

d

0             56              910             1314            3132          63

struct bit_data data;

data.b = 18;

其中第二条语句的用法不正确,因为成员变量b4位,它可存储的数的范围为0000~1111,即0~15。但由于第二条语句也符合语法。则b实际存储的是低4位。

若某一位要从另外一个存储单元开始存放,结构体中的成员可以定义为如下形式:

struct bit_data

{

         int a:6;

         int b:4;

         int : 8;

         int c:4;

         int d;

};

a

b

空闲

c

空闲

d

0           56           910         1718          2122          3132       63

0~5存放成员a6~9存放成员b10~17强制空闲,18~21存放成员c22~31空闲,32~63存放成员d

 

管道的局限性

1、  因为读数据的同时也将数据从管道移去,因此管道不能用来对多个接受者广播数据。

2、  管道中的数据被当做是字节流,因此无法识别信息的边界。如果写进程发送不同长度的数据对象通过管道,那么读进程不能确定接收了多少个对象,或是它不能确定对象的边界。

3、  如果一个管道有多个读进程,那么写进程不能发送数据到指定的读进程;同样,如果有多个写进程,那么没有办法来判别是它们中的哪一个发送了数据。

 

 

System V IPC机制

Linux支持Unix System V 中的三种进程间通信的机制,它们是:消息队列、信号量、共享内在。它们有一个共同点,就是它们使用相同的认证方法,一个进程只有通过系统调用向内核传递一个唯一的引用标识才能访问这些资源。

 

1、  消息队列

消息队列就是消息链表的头部指针。消息队列一个或多个进程写信息,同样这个消息可被一个或多个进程读取。消息队列中也以一个数据结构,称为msg

2、  信号量

信号量是为了控制进程对资源的使用而发明的。信号量可以实现一些同步协议也可以实现临界区的概念。

3、  共享内存

共享内存区域是被多个进程共享的一部分物理内存。进程可以把这些区域映射到它们地址空间中的任一合适的虚拟地址范围。这些地址范围对每一个进程来说可以是不同的。

 

 

管道的特点

1、  管道没有名字,它是为一次使用而创建的。

2、  管道的两个描述符是同时打开的。如果从一个没有任何进程向它写的管道读数据,read将返回文件结束。如果往一个没有任何进程读它的管道写数据,则视为错误。这将导致生成SIGPIPE信号,并且当信号被阻塞时以EPIPE错误失败。

3、  管道不允许文件定位。读和写操作都是顺序的,读从文件的开始处读,写则写至文件尾。

 

 

创建管道的简单方法

#include <stdio.h>

FILE *popen(const char * cmdstring,const char * type);

int pclose(FILE *fp);

这两个函数的作用类似于fopenfclose,具体说明如下:

1、  函数popen用于创建管道。它内部调用forkexec函数执行命令行cmdstring,返回一个FILE结构的指针,即用于访问管道的指针。

2、  popen中的参数const char * cmdstring就是一个命令行。所有的shell命令行参数和选项都可以使用。如下的函数调用都是合法的:

popen(“ls *.*”,”r”);

popen(“sort>/tmp/foo”,”w”);

popen(“sort|uniq|more”,”w”);

3、  popen中的参数const char * type指出管道的类型。如果管道是以类型”r”打开的,则这个管道的输入端连接到了命令行cmdstring的标准输出端。此时,命令行的输出可以从管道中读入。如果以类型”w”打开,则该管道的输出端连接到了命令行的标准输入端。此时,向管道中写入的数据就成为命令行的输入数据。但不可以即可读又可写。在Linux下,规定管道的打开方式取决于type的第一个字符,如type”rw”,则管道就是以”r”方式打开的。

4、  函数pclose是用来关闭管道的。它关闭标准输入输出流,等待命令行执行完毕,然后返回结束时的状态。如果shell不能执行这个命令行,结束时的状态就如同在shell中执行了exit(127)

5、   

管道与命名管道的区别

命令管道又称作先进先出队列。

1、  管道只能用于相同祖先的进程间通信,而命令管道就没有这个限制,没有任何联系的进程间也可以使用这种机制进行通信。

2、  管道是在内核中存储的,程序运行结束后将不复存在,而命令管道是作为特殊设备文件存储在文件系统中的,当程序运行结束后,它继续存在于文件系统中备用。除非删除该文件,否则这个命名管道不会消失。

 

 

命名管道的应用

1、  shell命令行使用命令管道将数据从一个命令传到另一个命令,而不需要创建中间的临时文件。

2、  在客户——服务器结构中,使用命令管道在客户和服务器间交换数据。

 

 

IPC机制的基本概念

1、  关键字和标识符——每个IPC资源有2个唯一的标志与之相连:关键字和标识符。

11标识符

每个System V的进程通信机制中的对象都和唯一的一个引用标识符相联系,如果进程要访问此IPC对象,则需要在系统中传递这个唯一的引用标识符。

标识符的唯一局限在相应的IPC对象的类别内。假设“12345”是某个消息队列的标识符,则肯定不会有第二个消息队列的标识符是“12345”,但某个共享内存或某个信号集的标识符却有可能是“12345”。

12关键字

关键字是用来定位System VIPC机制的对象的引用标识符的。当创建一个IPC机制的对象时,必须指定一个关键字。关键字的类型key_t,是系统中预先规定的,它定义在头文件<sys/types.h>中。

关键字的取值可以为IPC_PRIVATE或者其他值不等于IPC_PRIVATE的整数。IPC_PRIVATE定义在<sys/ipc.h>中,其值通常为0。这个关键字有特殊的含义,表示总是创建一个新的IPC资源。因为这个关键字调用内核总是分配新的IPC资源,所以它实际上表示所创建的IPC资源是创建进程私有的,即其他进程不可能得到它。但创建这个IPC资源的进程可以与其子进程共享该资源,子进程通过fork继承父进程的IPC资源。

其他不等于IPC_PRIVATE的关键字可以是直接指定的整常数,如12345,也可以是由公共种子导出的值。如,C库中提供了一个函数ftok可以将文件名转换为关键字。该函数的原型为:

#include <sys/ipc.h>

key_t ftok(const char *path,int fd);

fork函数根据pathid返回一个类型为key_t的关键字。

path参数必须是一个已存在文件的路径名。id只有低8位有效。对于命名同一个文件的所有路径名,当用同样的id调用ftok函数,该函数返回相同的关键字;当用不同的id调用ftok函数时,返回不同的关键字。如果id8位为0ftok的行为不确定。

ftok返回的关键字是根据文件的inode确定的,因此,如果这个文件在删除后又重新创建,则由ftok返回的关键字也会改变,尽管路径名仍然一样。

使用IPC资源通信的进程虽然可直接用诸如1234这样的整数作为关键字,但它们间需要在程序编码上保持一致,并且,这样做还有一个更致命的弱点:其他进程也可能使用这个整数作为另外的IPC资源的关键字。在这种情况下,则有可能导致混乱。因此,最好用ftok函数来生成IPC资源的关键字。

 

2、  ipc_perm结构介绍

每个进程通信机制的对象即有一个ipc_perm结构与之对应,这个结构中记录了对象的一些信息,如所有者、创建者和权限等。它定义在头文件<sys/ipc.h>中,具体定义如下所示:

struct ipc_perm

{

         uid_t uid;  所有者的有效用户ID

         gid_t gid;  所有者的有效组ID

         uid_t cuid;  创建者有效用户ID

         gid_t cgid;  创建者有效组ID

         mode_t mode;  访问权限

         ulong seq;  应用序号

         key_t key;  关键字

};

uid,gid,cuidcgid:它们在创建对象时就确定下来了,也可通过系统函数调用修改它们的值。但有权修改这些值的只能是对象的创建者或超级用户。

mode:记录了IPC机制的对象的访问权限,它与文件的访问权限有些类似,同样,用户、组用户和其他用户这三类不同用户的权限不同。但这些权限中没有可执行权限,且术语也有些改变。消息队列和共享内在的权限使用术语“可读”,“可写”,而信号量使用术语“可读”,“可改变“。

sqe:记录了IPC机制的对象的应用序号,它并不是确定的值。每次对象被使用,这个值都会被增加1,直到整数的最大值,然后又重新从0开始。

key:记录了进程通信对象的关键字的值。

 

3、  IPCS命令

可用ipcs命令得到IPC机制中所有对象的状态。这条命令可带选项,”-q”,”-s”,”-m”分别表示只输出消息队列、信号量、共享内存的对象的状态。在默认情况下,三种机制的对象都输出。

 

 

 

消息队列

消息队列是一条由消息连接而成的链表,它保存在内核中,通过消息队列的引用标识符来访问。

每个消息队列都有一个msqid_ds结构与之对应,在这个结构中,保存了消息队列的当前状态参数。

打开和创建消息队列的函数是msgget(key_t key,int flag),详细见《精通Linux C编程》第242

消息队列允许2种操作:发送消息和接收消息。进程间通过向消息队列发送消息和从消息队列接收消息实现进程间的通信。

每个消息由两部分组成,消息的类型域和所传递的数据域。其中,类型域由一个正的长整数构成,而数据域根据不同的需要可以有不同的形式。例如传递的数据由字符组成,且一次传递最长不会超过512个字符,则消息可定义为下面的类型:

struct mymsg

{

         long mtype;

         char mtext[512];

}

 

消息队列不同于管道,通信的两个进程可以是完全无关的进程,它们之间不需要约定同步的方法。只要消息队列存在并且有存放消息的空间,发送进程就可以向队列中存放消息,并且可以在接收进程开始之前终止其执行。但使用管道通信的进程,不论是匿名管道还是命名管道,通信的两个进程都必须是正在运行的进程。

 

消息队列总是保持在系统中直到系统重启,除非我们明确地删除它。

 

 

信号量

信号量实际上是整数计数器,主要用来控件多个进程对共享资源的访问。共享资源分为:互斥共享和同步共享。

 

与消息队列相同,每个信号量都与一个结构相联系,这个结构的定义在头文件<sys/sem.h>中。详细说明在《精通Linux C编程》的250~251页间。

 

信号量最常见的用法是控制程序中的一个关键区。这个关键区可能需要访问由多个进程共享的数据段,也可能需要访问其他共享资源,比如打印机。

 

信号量控制

调用semget只能创建一个信号量集合,但这个信号量集合中每个信号量并没有初值。如,我们希望用一个信号量管理含有20个缓冲区的一个缓冲池,则这个信号量的初值应当设置为20。为了给信号量集合中每个信号量设置一个初值,需要调用semctl函数。

 

共享内存

共享内存是允许多个进程共享一块内存,由此来达到交换信息的进程通信机制。共享内存机制是最快的一种进程通信机制,因为没有中间介质,如消息队列、管道等等的延迟,数据直接由内存映射到进程空间。通常,共享内存段由一个进程创建,接下来的读写操作就由许多进程参加,这样就能传递信息了。

共享内存机制唯一的不足在于,需要一定的同步机制控制多个进程对同一块内存的读写。当一个进程在写数据时,不允许其他的进程写数据或读数据,这可以通过信号量控制实现。

 

多线程程序作为一种多任务、并发的工作方式,有以下的优点:

1、  提高应用程序响应。这对图形界面的程序尤其有意义中,当一个操作耗时很长时,整个系统都会等待这个操作,此时程序不会响应键盘、鼠标、菜单的操作,而使用多线程技术,将耗时长的操作置于一个新的线程,可以避免这种尴尬的情况。

2、  使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。

3、  改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。

 

 

线程的基本概念:

一个进程是一个复合的实体,可分为两个部分:线程的集合和资源集合。线程是一个动态的对象,它表示进程中的一个控制点,并且执行一系列的指令。资源包括地址空间、打开的文件、用户凭证和配额等,这些资源为进程中所有线程所共享。此外每个线程有它自己的私有对象,比如程序计数器、堆栈和寄存器的值。

 

线程具有一个ID、一个堆栈、一个执行优先权,以及执行的开始地址,POSIX线程通过pthread_t类型的ID来引用。pthread_t其实就是无符号长整数,在文件/usr/include/bit/pthreadtypes.h中定义。

 

如果线程可在进程的执行期间的任意时刻被创建,并且线程的数量事先没有必要指定,这样的线程被称为动态线程。

 

gcc编译多线程程序时,必须与pthread函数库连接。可实现如下:

gcc –lpthread –o ex1 ex1.c

 

 

线程同步

按照POSIX标准,POSIX提供了两种类型的同步机制,它们是互斥锁(mutex)和条件变量(condition variable)

一、互斥锁是一个简单的锁定命令,它可用来锁定对共享资源的访问。对于线程来说,整个地址空间都是共享的资源,所以线程的任何资源都是共享的资源。互斥锁的特点是:原子性、唯一性、非繁忙等待。

处理互斥锁常用的函数如下:

1、  pthread_mutex_init函数

#include <pthread.h>

pthread_mutex_t fastmutex = PTHREAD_MUTEX_INITIALIZER;

pthread_mutex_t recmutex = PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP;

pthread_mutex_t errchkmutex = PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP;

int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutex_attr *attr);

上面三个常量是常用的处理互斥锁的常量。

pthread_mutex_init用来初始化一个由参数mutex指向的互斥锁,这个互斥锁的属性由参数attr指定,或通过指定attrNULL而使用默认的属性。

不会出现多个线程同时初始化同一个互斥锁的情形,一个互斥锁在使用期间一定不会被重新初始化。

如果pthread_mutex_init执行成功,则返回0,并将新创建的互斥锁的ID值放到参数mutex中。如果执行失败,则将返回一个错误编号。

2、  pthread_mutex_destroy函数

函数原型:int pthread_mutex_destroy(pthread_mutex_t *mutex);

pthread_mutex_destroy函数解除由参数mutex指向的互斥锁的任何状态。储存互斥锁的内存并不被释放。

执行成功,则返回0;如果执行失败,则将返回一个错误编号

3、  pthread_mutex_lock函数

函数原型:int pthread_mutex_lock(pthread_mutex_t *mutex);

pthread_mutex_lock函数可锁定由参数mutex指向的互斥锁。若mutex已经被锁定,则当前调用的线程将阻塞直到互斥锁被其他线程释放(阻塞线程按照线程优先级等待)。当pthread_mutex_lock返回时,说明互斥锁已经被当前线程成功加锁。

执行成功返回0,失败则返回其他值。

4、  pthread_mutex_trylock

函数原型:int pthread_mutex_trylock(pthread_mutex_t *mutex);

pthread_mutex_trylock来尝试给由参数mutex指定的互斥锁加锁。这个函数是pthread_mutex_lock的非阻塞版本。pthread_mutex_lock在给一个互斥锁加锁时,如果互斥锁已经被锁定,则pthread_mutex_lock将一直阻塞,不会立即返回。而使用pthread_mutex_trylock给一个互斥锁加锁时,若互斥锁已经被锁定,则pthread_mutex_trylock调用将返回错误。否则,互斥锁将被调用者加锁。

成功返回0,失败则返回其他值。

5、  pthread_mutex_unlock

函数原型:int pthread_mutex_unlock(pthread_mutex_t *mutex);

pthread_mutex_unlock给由参数mutex指定的互斥锁解锁。互斥锁必须处于加锁状态而且调用本函数的线程必须是给互斥锁加锁的同一个线程才能给互斥锁解锁。若有其他线程在等待互斥锁,则有核心的调度程序决定哪个线程将获得互斥锁并脱离阻塞状态

成功返回0,失败则返回其他值。

二、条件变量,是对互斥锁的补充,它允许线程阻塞并等待另一个线程发送的信号。当收到信号时,阻塞的线程就被唤醒并试图锁定与之相关的互斥锁。

处理条件变量的一些函数:

1、  pthread_cond_init函数

函数原型:

#include <pthread.h>

int pthread_cond_init(pthread_cond_t *cond,const pthread_cond_attr *attr);

pthread_cond_init初始化由参数cond指定的条件变量。这个条件变量的属性由参数attr指定。如果参数attrNULL,则就使用默认的属性设置。

多线程不能同时初始化同一个条件变量。如果一个条件变量正在使用,它不能被重新初始化。

成功则返回0,并将新创建的条件变量的ID放在参数cond中;失败则返回其他值。

2、  pthread_cond_destroy函数

函数原型:int pthread_cond_destroy(pthread_cond_t *cond);

使用pthread_cond_destroy来消除由参数cond指向的条件变量的任何状态。但是储存条件变量的内存空间不被释放。

成功则返回0,失败则返回其他值。

3、  pthread_cond_wait函数

函数原型:int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);

函数pthread_cond_wait释放由参数mutex指向的互斥锁,并且使调用线程关于参数cond指向的条件变量阻塞。被阻塞的线程可以被pthread_cond_signalpthread_cond_broadcast或由fork和传递信号引起的中断唤醒。

即使返回错误信息,pthread_cond_wait通常在互斥锁被调用线程加锁后才返回。

函数将阻塞直到条件变量被信号唤醒。它在阻塞前自动释放互斥锁,在返回前再自动获得它。

如果有多个线程关于条件变量阻塞,其退出阻塞状态的顺序将不确定。

成功则返回0,失败则返回其他值。

4、  pthread_cond_timewait函数

函数原型:int pthread_cond_timewait(pthread_cond_t *cond, pthread_mutex_t *mutex,const struct timespec *abstime);

pthread_cond_timewaitpthread_cond_wait的用法相似,区别在于pthread_cond_timewait在经过由参数abstime指定的时间时不阻塞。

即使是返回错误,pthread_cond_timewait也只在给互斥锁加锁后返回。

pthread_cond_timewait函数将阻塞,直到条件变量获得信号或者经过由abstime指定的时间。

如果pthread_cond_timewait执行成功则返回零。如果阻塞条件变量的时间超过了由参数abstime所指定的时间,则就返回ETIMEOUT。其他值意味着错误。

5、  pthread_cond_signal函数

函数原型:int pthread_cond_signal(pthread_cond_t *cond);

使用pthread_cond_signal使得关于由参数cond指向的条件变量阻塞的线程退出阻塞状态。在同一个互斥锁的保护下使用pthread_cond_signal,否则,条件变量可以在对关联条件变量的测试和pthread_cond_wait带来的阻塞之间获得信号,这将导致无限期的等待。

如果没有一个线程关于条件变量阻塞,则pthread_cond_signal无效。

成功则返回0,失败则返回其他值。

6、  pthread_cond_broadcast函数

函数原型:int pthread_cond_broadcast(pthread_cond_t *cond);

使用pthread_cond_broadcast使得所有关于由参数cond指向的条件变量阻塞的线程退出阻塞状态。如果没有阻塞的线程,pthread_cond_broadcast无效。

这个函数将唤醒所有由pthread_cond_wait阻塞的线程。因为所有关于条件变量阻塞的线程都同时参与竞争,所有使用这个函数需要小心。

成功则返回0,失败则返回其他值。

BSD套接字接口

BSD套接字接口是BSD的进程间通信方式,它不仅支持各种形式的网络应用,而且它还是一种进程间通信的机制。一个套接字描述一个通信连接的一端,两个相互通信的进程,每个都需要一个套接字描述它们之间的通信连接的端点。套接字可以看成是一种特殊的管道,与管道不同的是套接字所能容纳的数据不受限制。

Linux BSD支持如下类型的套接字,它们是:

1、  Steam(数据流)。这个套接字提供了两个方向的序列数据流,这些数据流保证在传输过程中数据不丢失、破坏或重复。数据流套接字由Internet(INET)地址簇的TCP协议所支持。

2、  Datagram(数据报)。这个套接字也提供两个方向上的数据传送,但不像数据流套接字,它们不提供消息到达的保证。即使到达也不保证这些数据报按照一定的顺序到达或丢失、重复。这种类型的套搠字由Internet地址簇的UDP协议所支持。

3、  Raw(原始套接字)。这种类型的套接字允许进程直接访问底层协议。如,可为以太网设备打开一个Raw Socket,以使用原始IP数据进行传输。

4、  Reliable Delivered Message(可靠传递消息)。它非常像数据报套接字,但保证数据的可靠传输。

5、  Sequenced Packets(顺序数据报)。它像数据流套接字,但数据包的大小固定。

6、  Packet(包)。它不是标准的BSD套接字类型,它是一个Linux特定的扩展,允许进程在设备层直接访问Packet

利用套接字进程通信的进程采用客户机/服务器模式。使用套接字的服务器首先建立一个套接字,然后用一个名称对这个套接字进行绑定。这个名称的格式独立于套接字的地址簇,它是有效的服务器的本地地址。套接字的名称或地址由sockaddr结构来指定,一个INET套接字由一个IP端口地址与之绑定。常用服务的注册端口可以在/etc/services中看到,如端口80Web服务器的特定端口。当给一个套接字绑定一个地址后,服务器侦听输入请求指定的绑定地址的连接。客户建立一个套接字和一个基于它的连接请求,这个连接请求指定的目的服务器的地址。对一个INET套接字来讲,服务器的地址是它的IP地址和端口号。这些传入的请求必须通过各种不同的协议层向上找到自己的通路,然后等待服务器侦听套接字。一旦服务器收到请求,它要么接收要么拒绝。如果传入请求被接收,服务器必须建立一个新的套接字用来接收。如果一个套接字已经用来侦听传入的连接请求,那么它不能用来支持一个连接。

 

通常情况下,服务器包括两个部分:主程序和从程序。主程序负责接收来自客户的请求,从程序一般有几个,它们负责处理各个客户请求。

如果客户请求所指的端口不是已知的端口,则应为它请求分配一个临时端口,然后启动从程序,等待新的客户请求。从程序通常是个子程序,处理完一个客户请求后就中止并返回结果。

服务器通常是作为应用程序,而不是主机。服务器作为应用程序的优点是:它们可以在任何一个支持该通信协议的计算机系统上运行,这样不同的服务器可以在同一个分时系统上运行,或在一台个人计算机上运行,网络编程人员也可以在同一台机器上同时运行客户机和服务器,为测试和调试软件带来方便。如果一台计算机的主要任务是支持某个服务器程序,则服务器这一名称不但是指服务器程序,也指计算机。同理,客户机也是一样。

 

套接字编程用到的相应函数、数据结构等详见《精通Linux C编程》312~315页。

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值