Linux系统编程-进程间通信

一.进程间通信概念

每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC,InterProcess Communication)
在这里插入图片描述

二.进程间通信的目的

数据传输:一个进程需要将它的数据发送给另一个进程

资源共享:多个进程之间共享同样的资源

通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)

进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常

三.进程间通信的六种方式

单机
半双工管道——PIPE
全双工管道——命名全双工管道
消息队列
信号量
共享内存
多机
套接字
STREAMS

1.半双工管道

创建管道:pipi函数
#include <unistd.h>
int pipe(int pipefd[2]);
pipe函数定义中的fd参数是⼀个⼤⼩为2的⼀个数组类型的指针。该函数成功时返回0,并将⼀对打开的⽂件描述符值填⼊fd参数指向的数组。失败时返回 -1并设置errno。
通过pipe函数创建的这两个⽂件描述符 fd[0] 和 fd[1] 分别构成管道的两端,fd[0]表示读端,fd[1]表示写端。
下面展示无名管道代码

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

int main()
{
	int fd[2];	//两个文件描述符
	int pid;
	char buf[128];

	if(pipe(fd) == -1){
		printf("creat pipe failed!\n");
	}
wsdx	pid = fork();

	if(pid < 0){
		printf("creat child failed!\n");
	}else if(pid > 0){	//父进程
		printf("this is father\n");
		close(fd[0]);
		write(fd[1],"this is father from",strlen("this is father from"));
		wait();
	}else{		//子进程
		printf("this is child\n");
		close(fd[1]);
		read(fd[0],buf,128);
		printf("read from father:%s\n",buf);
		exit(0);
	}


	return 0;
}

2.有名管道

函数原型如下:
#include <sys/stat.h>
#include <sys/types.h>
int mkfifo( const char * filename, mode_t mode );

mkfifo函数中参数mode指定FIFO的读写权限,新创建FIFO的用户ID和组ID规则域open函数相同。参数filename指定新创建FIFO的文件名称。函数如果成功返回0,出 错返回–1,并更改errno的值。errno有可能出现的值为:EACCESS、EEXIST、ENAMETOO- LONG、ENOENT、ENOSPE、ENOTDIR和EROFS。

mkfifo 功能

mkfifo() 函数创建一个名为 pathname 的 FIFO 特殊文件,mode 参数用于指定权限。创建的 FIFO 特殊文件与管道是类似的,都可以用于进程间通信。这种特殊的FIFO文件可以被文件系统加载,因此可以像普通文件一样读写和删除。

使用mkfifo函数创建了FIFO特殊文件后,任何进程都可以像普通文件一样打开之,并读写。通常,读取FIFO特殊文件会被阻塞,直到有进程写入数据到FIFO文件。
mkfifo函数的简单demo

// ------read------

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>

int main()
{
	int ret = mkfifo("./file",0600);
	char buf[30] = {0};

	if(ret == -1 && errno != EEXIST){
		printf("mkfifo failed\n");
		perror("why");
	}else{
		if(errno == EEXIST){
			printf("file have\n");
		}
	}

	int fd = open("./file",O_RDONLY);

	int n_read = read(fd,buf,30);
	printf("read %d byte from fifo,context: %s \n",n_read,buf);

	close(fd);

	return 0;
}

// ------write------

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>

int main()
{
	int cnt = 0;
	char *str = "message from fifo";

	int fd = open("./file",O_WRONLY);
	while(1){
		int n_write = write(fd,str,strlen(str));
		cnt++;
		sleep(1);
		if(cnt == 5){
			exit(0);
		}

	}
	return 0;
}

3.消息队列

基本概念
  消息队列就是⼀个存储消息的链表,这些消息具有特定的格式,以及特定的优先级。
  对消息队列有写权限的进程可以向其中添加新消息,对消息队列有读权限的进程则可以从其中读取消息。  消息队列是随内核持续的,只有在内核重启,或者删除⼀个消息队列时,该消息队列才会真正地被删除。

用户消息缓冲区
无论发送进程还是接收进程,都需要在进程空间中用消息缓冲区来暂存消息。该消息缓冲区的结构定义如下

struct msgbuf {
long mtype; /* 消息的类型 /
char mtext[1]; /
消息正文 */
};
可通过mtype区分数据类型,同过判断mtype,是否为需要接收的数据
mtext[]为存放消息正文的数组,可以根据消息的大小定义该数组的长度

------创建消息队列------
通过msgget创建消息队列
函数原型如下

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgget(key_t key, int msgflg);
参数:

key: 某个消息队列的名字
msgflg:由九个权限标志构成,用法和创建文件时使用的mode模式标志是一样的,这里举两个来说明
IPC_CREAT
如果消息队列对象不存在,则创建之,否则则进行打开操作
IPC_EXCL
如果消息对象不存在则创建之,否则产生一个错误并返回
返回值:

成功msgget将返回一个非负整数,即该消息队列的标识码;
失败则返回“-1”
那么如何获取key值?

通过宏定义key值
通过ftok函数生成key值,这里就不具体介绍ftok函数用法

------添加信息到消息队列------
向消息队列中添加数据,使用到的是msgsnd()函数
函数原型如下

int msgsnd(int msgid, const void *msg_ptr, size_t msg_sz, int msgflg);
参数:

msgid: 由msgget函数返回的消息队列标识码
msg_ptr:是一个指针,指针指向准备发送的消息,
msg_sz:是msg_ptr指向的消息长度,消息缓冲区结构体中mtext的大小,不包括数据的类型
msgflg:控制着当前消息队列满或到达系统上限时将要发生的事情
如:
msgflg = IPC_NOWAIT 表示队列满不等待,返回EAGAIN错误
返回值:

成功返回0
失败则返回-1

------从消息队列中读取消息------
从消息队列中读取消息,我们使用msgrcv()函数,
函数原型如下

int msgrcv(int msgid, void *msg_ptr, size_t msgsz,
long int msgtype, int msgflg);
参数:

msgid: 由msgget函数返回的消息队列标识码
msg_ptr:是一个指针,指针指向准备接收的消息,
msgsz:是msg_ptr指向的消息长度,消息缓冲区结构体中mtext的大小,不包括数据的类型
msgtype:它可以实现接收优先级的简单形式
msgtype=0返回队列第一条信息
msgtype>0返回队列第一条类型等于msgtype的消息 
msgtype<0返回队列第一条类型小于等于msgtype绝对值的消息
msgflg:控制着队列中没有相应类型的消息可供接收时将要发生的事
msgflg=IPC_NOWAIT,队列没有可读消息不等待,返回ENOMSG错误。
msgflg=MSG_NOERROR,消息大小超过msgsz时被截断
注意
msgtype>0且msgflg=MSC_EXCEPT,接收类型不等于msgtype的第一条消息

返回值:

成功返回实际放到接收缓冲区里去的字符个数
失败,则返回-1

------消息队列的控制函数------
函数原型

int msgctl(int msqid, int command, strcut msqid_ds *buf);
参数:

msqid: 由msgget函数返回的消息队列标识码
command:是将要采取的动作,(有三个可取值)分别如下

注意:若选择删除队列,第三个参数传NULL
返回值:
如果操作成功,返回“0”;如果失败,则返回“-1”

查看消息队列
查看消息队列
ipcs -q 命令查看已经创建的消息队列,包括他的key值信息,id信息,拥有者信息,文件权限信息,已使用的字节数,和消息条数。
ipcrm -Q加消息队列的key值,或来删除一个消息队列。

下面展示一些demo

// —————-msgget—————-
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>

//	int msgget(key_t key, int msgflg);
//	int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
//	ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
//	int msgctl(int msqid, int cmd, struct msqid_ds *buf);

struct msgbuf {
	long mtype;         /* 消息的类型 */
	char mtext[128];      /* 消息正文 */
};

int main()
{
	struct msgbuf readBuf;

	key_t key;
	key = ftok(".",'z');
	printf("key = %x\n",key);

	//创建或打开消息队列,成功返回队列ID,失败返回-1
	int msgID = msgget(key,IPC_CREAT|0777);
	if(msgID == -1){
		printf("get msgID failed\n");
	}

	//读取消息,成功返回消息数据的长度,失败返回-1
	msgrcv(msgID,&readBuf,sizeof(readBuf.mtext),888,0);
	printf("read from que:%s\n",readBuf.mtext);

	struct msgbuf sndBuf = {988,"thank you for reach"};
	msgsnd(msgID,&sndBuf,strlen(sndBuf.mtext),0);

	//控制消息队列,成功返回0,失败返回-1
	msgctl(msgID,IPC_RMID,NULL);	//删除消息队列

	return 0;
}
// ——————msgsnd
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>

//	int msgget(key_t key, int msgflg);
//	int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
//	ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);

struct msgbuf {
	long mtype;         /* 消息的类型 */
	char mtext[128];      /* 消息正文 */
};

int main()
{
	struct msgbuf sndBuf = {888,"this is message from quen"};

	key_t key;
	key = ftok(".",'z');
	printf("key = %x\n",key);

	//创建或打开消息队列,成功返回队列ID,失败返回-1
	int msgID = msgget(key,IPC_CREAT|0777);
	if(msgID == -1){
		printf("get msgID failed\n");
	}

	//添加消息,成功返回0,失败返回-1
	msgsnd(msgID,&sndBuf,strlen(sndBuf.mtext),0);

	struct msgbuf readBuf;
	msgrcv(msgID,&readBuf,sizeof(readBuf.mtext),988,0);
	printf("read from que:%s\n",readBuf.mtext);

	//控制消息队列,成功返回0,失败返回-1
	msgctl(msgID,IPC_RMID,NULL);	//删除消息队列

	return 0;
}

4.共享内存

共享内存IPC原理:
共享内存进程间通信机制主要用于实现进程间大量的数据传输,下图所示为进程间使用共享内存实现大量数据传输的示意图:
在这里插入图片描述
共享内存是在内存中单独开辟的一段内存空间,这段内存空间有自己特有的数据结构,包括访问权限、大小和最近访问的时间等。
两个进程在使用此共享内存空间之前,需要在进程地址空间与共享内存空间之间建立联系,即将共享内存空间挂载到进程中。

1.创建共享内存
#include <sys/ipc.h> #include <sys/shm.h>
/*

  • 第一个参数为 key 值,一般由 ftok() 函数产生
  • 第二个参数为欲创建的共享内存段大小(单位为字节)
  • 第三个参数用来标识共享内存段的创建标识
    */

int shmget(key_t key, size_t size, int shmflg);
2.共享内存控制
#include <sys/ipc.h> #include <sys/shm.h>
/*

  • 第一个参数为要操作的共享内存标识符
  • 第二个参数为要执行的操作
  • 第三个参数为 shmid_ds 结构的临时共享内存变量信息
    */

int shmctl(int shmid, int cmd, struct shmid_ds *buf);
3.映射共享内存对象
系统调用 shmat() 函数实现将一个共享内存段映射到调用进程的数据段中,并返回内存空间首地址,其函数声明如下:

#include <sys/types.h>
#include <sys/shm.h>
/*

  • 第一个参数为要操作的共享内存标识符
  • 第二个参数用来指定共享内存的映射地址,非0则为此参数,为0的话由系统分配
  • 第三个参数用来指定共享内存段的访问权限和映射条件
    */

void *shmat(int shmid, const void *shmaddr, int shmflg);
4.分离共享内存对象
在使用完毕共享内存空间后,需要使用 shmdt() 函数调用将其与当前进程分离。函数声明如下:

#include <sys/types.h>
#include <sys/shm.h>
/*

  • 参数为分配的共享内存首地址
    */

int shmdt(const void *shmaddr);

共享内存的demo

// 共享内存_写
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

//	int shmget(key_t key, size_t size, int shmflg);

int main()
{
	int shmID;
	char *shmaddr;
	key_t key;
	key = ftok(".",26);

	//创建或获取一个共享内存,成功返回共享内存ID,失败返回-1
	shmID = shmget(key,1024*4,IPC_CREAT|0600);
	if(shmID == -1){
		printf("shmget error!\n");
		exit(-1);
	}

	//连接共享内存到当前进程的地址空间,成功返回指向共享内存的指针,失败返回-1
	shmaddr = shmat(shmID,0,0);

	strcpy(shmaddr,"chenglicheng");

	sleep(5);

	//断开与共享内存的连接,成功返回0,失败返回-1
	shmdt(shmaddr);

	//控制共享内存的相关信息,成功返回0,失败返回-1
	shmctl(shmID,IPC_RMID,0);

	printf("quet\n");

	return 0;
}
// 共享内存_读
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

//	int shmget(key_t key, size_t size, int shmflg);

int main()
{
	int shmID;
	char *shmaddr;
	key_t key;
	key = ftok(".",26);

	//创建或获取一个共享内存,成功返回共享内存ID,失败返回-1
	shmID = shmget(key,1024*4,0);
	if(shmID == -1){
		printf("shmget error!\n");
		exit(-1);
	}

	//连接共享内存到当前进程的地址空间,成功返回指向共享内存的指针,失败返回-1
	shmaddr = shmat(shmID,0,0);

	printf("data:%s\n",shmaddr);

	sleep(5);

	//断开与共享内存的连接,成功返回0,失败返回-1
	shmdt(shmaddr);

	printf("quet\n");

	return 0;
}

5.信号

1.基本概念
信号是事件发生时对进程的通知机制,也就是所谓的软件中断。信号和硬件的中断类似,是软件层对中断机制的模拟,在多数情况下是无法预测信号产生的时间,所以软件层提供了一种处理异步事件的方法。

2.信号的来源分为硬件来源和软件来源。

硬件来源:
硬件发生异常,即硬件检测到错误条件并通知内核,随即再由内核发送相应的信号给相关进程,如除数为0、无效的内存引用等。
用户按终端键,引起终端产生的信号(比如Ctrl + C键产生SIGINT)。
软件来源:
用户通过指令杀死,如kill指令。
发生软件事件, 如程序执行raise, alarm、setitimer、sigqueue等函数。

3.信号处理
信号通常是发送给对应的进程,当信号到达后,该进程需要做出相应的处理措施,通常进程会视具体信号执行相应的操作,有三种操作方式。

忽略信号:
  信号到达后、直接忽略,就好像是没有出该信号,信号对该进程不会产生任何影响。事实上,大多数信号都可以使用这种方式进行处理,但有两种信号却决不能被忽略,分别是SIGKILL 和 SIGSTOP。
捕获信号:
  当信号到达进程后,执行signal()绑定好的信号处理函数。
执行系统默认操作:
  进程不对该信号事件作出处理,而是交由系统进行处理,每一种信号都会有其对应的系统默认的处理方式。

4.常见的信号
在linux系统中通过kill -l命令可以查看到相应的信号。信号编号是从 1 开始,不存在编号为 0 的信号,事实上 kill()函数对信号编号 0 有着特殊的应用。
在这里插入图片描述
注意:括号" ) "前面的数字对应该信号的编号,编号 1~31 所对应的是不可靠信号,编号 34~64 对应的是可靠信号,从图中可知,可靠信号并没有一个具体对应的名字,而是使用了 SIGRTMIN+N 或 SIGRTMAXN 的方式来表示。其中32和33空缺。

信号处理函数的注册
信号处理函数的注册不只一种方法,分为入门版和高级版
1.入门版:函数 signal
2.高级版:函数sigaction
信号处理发送函数
信号发送函数也不止一个,同样分为入门版和高级版
1.入门版:kill
2.高级版:sigqueue

————signal()
"signal.h"信号处理库提供了signal函数,用来捕获突发事件。以下是 signal() 函数的语法ads。
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_thandler);

signum:可使用信号名(宏)或信号的数字编号,建议使用信号名。
handler:参数 handler 既可以设置为用户自定义的函数,也可以设置为 SIG_IGN 或 SIG_DFL,SIG_IGN 表示此进程需要忽略该信号,SIG_DFL 则表示设置为系统默认操作。

—————sigaction()
除了signal()之外,sigaction()系统调用是设置信号处理方式的另一选择,虽然 signal()函数简单好用,而 sigaction()更为复杂,但作为回报,sigaction()也更具灵活性以及移植性。

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

signum:需要设置的信号,除了 SIGKILL 信号和 SIGSTOP 信号之外的任何信号。
act:参数 act 不为 NULL,则表示需要为信号设置新的处理方式;如果参数 act 为 NULL,则表示无需改变信号当前的处理方式
oldact:参数oldact 不为 NULL,则会将信号之前的处理方式等信息通过参数 oldact 返回出来;如果无意获取此类信息,那么可将该参数设置为 NULL。
返回值:成功返回 0;失败将返回-1,并设置 errno。

struct sigaction 结构体

struct sigaction {
               void     (*sa_handler)(int);
               void     (*sa_sigaction)(int, siginfo_t *, void *);
               sigset_t   sa_mask;
               int        sa_flags;
               void     (*sa_restorer)(void);
           };

sa_handler:指定信号处理函数,与 signal()函数的 handler 参数相同。
sa_sigaction:也用于指定信号处理函数,这是一个替代的信号处理函数。
sa_mask:参数 sa_mask 定义了一组信号。
sa_restorer:该成员已过时,不要再使用了。
sa_flags:参数 sa_flags 指定了一组标志,这些标志用于控制信号的处理过程。

—————kill()
kill()系统调用可将信号发送给指定的进程或进程组中的每一个进程。
int kill(pid_t pid, int sig);
pid:参数 pid 为正数的情况下,用于指定接收此信号的进程 pid。
sig:参数 sig 指定需要发送的信号,也可设置为 0,如果参数 sig 设置为 0 则表示不发送信号,但任执行错误检查,这通常可用于检查参数 pid 指定的进程是否存在。
返回值:成功返回 0;失败将返回-1,并设置 errno。

keil()的demo


#include <stdio.h>
#include <sys/types.h>
#include <signal.h>

//      int kill(pid_t pid, int sig);

//      ./a.out 9 5269


int main(int argc,char **argv)
{
      int signum;
      int pid;

      char cnt[128];

      signum = atoi(argv[1]); //转换成整型数
      pid = atoi(argv[2]);

      printf("signum = %d,pid = %d\n",signum,pid);

//      kill(pid,signum);

      sprintf(cnt,"kill -%d %d",signum,pid);
      system(cnt);

      printf("send signal ok\n");

      return 0;

————-sigqueue()
#include<signal.h>
int sigqueue(pid_t pid,int sig,const union sigval value);

pid是目标进程的进程号
sig是信号代号
value参数是一个联合体,表示信号附带的数据,附带数据可以是一个整数也可以是一个指针,有如下形式:
union sigval {
int sival_int;
void *sival_ptr;//指向要传递的信号参数
};value

// ------接受消息------
#include <stdio.h>
#include <signal.h>

//	sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);

void handle(int signum,siginfo_t *info,void *context)
{
	printf("get signum = %d\n",signum);

	if(context != NULL){	//判断是否有内容
		printf("get data = %d\n",info->si_int);	//读取内容
		printf("get data = %d\n",info->si_value.sival_int);
		printf("from:5d\n",info->si_pid);	//得到发送方的pid
	}
}

int main()
{
	struct sigaction act;

	printf("pid = %d\n",getpid());

	act.sa_sigaction = handle;
	act.sa_flags = SA_SIGINFO;	//be able to get message 

	sigaction(SIGUSR1,&act,NULL);	//注册信号

	while(1);

	return 0;
}

// ------发送消息------
#include <stdio.h>
#include <signal.h>

//	int sigqueue(pid_t pid, int sig, const union sigval value);

int main(int argc,char **argv)
{
	int signum;
	int pid;

	signum = atoi(argv[1]);
	pid = atoi(argv[2]);

	union sigval value;
	value.sival_int = 100;
	
	sigqueue(pid,signum,value);

	printf("%d,down\n",getpid());

	return 0;
}

6.信号量

信号量(semaphore)与已经介绍过的IPC结构不同,他是一个计数器。信号量用于实现进程间的互斥与同步,而不是用于存储进程间数据通信。

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
// 创建或获取一个信号量组:若成功返回信号量集ID,失败返回-1
int semget(key_t key, int num_sems, int sem_flags);
// 对信号量组进行操作,改变信号量的值:成功返回0,失败返回-1
int semop(int semid, struct sembuf semoparray[], size_t numops);
// 控制信号量的相关信息
int semctl(int semid, int sem_num, int cmd, …);

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdlib.h>

//	int semget(key_t key, int num_sems, int sem_flags);
//	int semctl(int semid, int sem_num, int cmd, ...);
//	int semop(int semid, struct sembuf semoparray[], size_t numops); 
union semun {
    int              val;    /* Value for SETVAL */
    struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
    unsigned short  *array;  /* Array for GETALL, SETALL */
    struct seminfo  *__buf;  /* Buffer for IPC_INFO
                             (Linux-specific) */
};

void pGetKey(int id)
{
	struct sembuf sops;
	sops.sem_num = 0;
	sops.sem_op = -1;	//钥匙减一
	sops.sem_flg = SEM_UNDO;

	semop(id,&sops,1);
	printf("get key\n");
}


void vPutBackKey(int id)
{
	struct sembuf sops;
	sops.sem_num = 0;
	sops.sem_op = 1;	//钥匙减一
	sops.sem_flg = SEM_UNDO;

	semop(id,&sops,1);
	printf("put back key\n");
}


int main()
{
	int semID;

	key_t key;
	key = ftok(".",2);

	// 创建或获取一个信号量组:若成功返回信号量集ID,失败返回-1
	semID = semget(key,1,IPC_CREAT|0666);
					 //信号量集合中中只有一个信号量

	union semun initsem;
	initsem.val = 0;
				//操作第0个信号量
	semctl(semID,0,SETVAL,initsem);	//初始化信号量组
					//SETVAL设置信号量的值,设置为initsem

	int pid = fork();
	if(pid > 0){
		pGetKey(semID);
		printf("this is father!\n");
		vPutBackKey(semID);
	}else if(pid == 0){
		printf("this is child!\n");
		vPutBackKey(semID);
	}else{
		printf("fork error");
		exit(-1);
	}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值