Linux进程/线程通信-信号量(POSIX Semaphore)+共享内存

这里主要讲POSIX信号量。

信号量是Linux系统为应用层提供的进程/线程间同步的一种机制。
信号量分两种:无名信号量(unnamed semaphore)和有名信号量(named semaphore)。

两者区别:

区别无名信号量有名信号量
创建方式int sem_init(sem_t *sem, int pshared, unsigned int value)sem_t *sem_open(const char *name, int oflag,mode_t mode, unsigned int value);
销毁方式int sem_destroy(sem_t *sem);int sem_unlink(const char *name);
存储方式存放在内存中存放在虚拟文件系统
相同点无名信号量有名信号量
获取信号量(P操作)int sem_wait(sem_t *sem);int sem_wait(sem_t *sem);
释放信号量(V操作)int sem_post(sem_t *sem);int sem_post(sem_t *sem);

获取信号量有三种接口,不管有名信号量还是无名信号量均可通用:

接口说明
int sem_wait(sem_t *sem);如果信号量值为0,阻塞式等待另一端释放信号量
int sem_trywait(sem_t *sem);尝试获取信号量,如果当前信号量为0,则立即返回,errno置为EAGAIN
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);设置等待超时abs_timeout,时间一到如果信号量依旧为0,则返回,errno置为ETIMEDOUT

1. 无名信号量

无名信号量保存位置:
1.在线程间的“共享内存”区域(比如信号量句柄是全局变量);
2.进程间的共享内存中(比如信号量句柄为共享内存)。
如果是1的话则可用于线程间通信,2可用于进程间通信。

小提示:Linux 2.6版本之前的内核仅支持unnamed semaphore,线程间通信的信号量。

下面代码演示线程间通信:unnamed_thread_sema.c

#include <stdio.h>
#include <semaphore.h>
#include <pthread.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>

#define INDEFINITE		(0xFFFFFFFFu)
#define THREAD_SIZE		(16*1024)

typedef sem_t* SEMA_HANDLE;

SEMA_HANDLE ThSemap=NULL;
int bIsSemaInit=0;

pthread_t ProduceThread;
pthread_t ConsumeThread;


int commodity=0;

int SemaphoreInit(void)
{
	if(bIsSemaInit)
	{
		return 0;
	}

	ThSemap=(SEMA_HANDLE)malloc(sizeof(sem_t));
	if(!ThSemap)
	{
		printf("Semaphore malloc error!\n");
		return -1;
	}

	int rc=sem_init(ThSemap, 0, 1);
	if(rc!=0)
	{
		perror("sem_init");
		if(ThSemap)
		{
			free(ThSemap);
		}
		return -1;
	}

	bIsSemaInit=1;
	return 0;
}

void SemaphoreDeInit(void)
{
	if(ThSemap)
	{
		sem_destroy(ThSemap);
		free(ThSemap);
		ThSemap=NULL;
	}
	
	bIsSemaInit=0;
}

int SemaphoreWait(SEMA_HANDLE pSema, unsigned int timeout)
{
	if(!pSema)
	{
		printf("pSema is null.%s %d\n", __FUNCTION__, __LINE__);
		return -1;
	}

	struct timespec stTimeout;
	memset(&stTimeout, 0x00, sizeof(struct timespec));
	
	stTimeout.tv_sec=(long)timeout;
	int rc = sem_timedwait(pSema, &stTimeout);
	if(rc != 0)
	{
		if(errno==ETIMEDOUT)
		{
			printf("sem_timedwait timeout.\n");
		}

		return -1;
	}

	return 0;
}

int SemaphoreRelease(SEMA_HANDLE pSema)
{
	if(!pSema)
	{
		printf("pSema is null.%s %d\n", __FUNCTION__, __LINE__);
		return -1;
	}

	int rc=sem_post(pSema);
	if(rc!=0)
	{
		printf("sem_post error.%s %d\n", __FUNCTION__, __LINE__);
		return -1;
	}

	return 0;
}

void *produce_thread(void *parg)
{
	while(1)
	{
		sleep(1);
		SemaphoreWait(ThSemap, INDEFINITE);
		commodity++;
		printf("Produce commodity:%d\n", commodity);
		SemaphoreRelease(ThSemap);
	}
}

void *consume_thread(void *parg)
{
	while(1)
	{
		sleep(1);
		SemaphoreWait(ThSemap, INDEFINITE);
		commodity--;
		printf("Consume commodity:%d\n", commodity);
		SemaphoreRelease(ThSemap);
	}
}

//封装创建线程函数的原因:正常在项目中肯定涉及设置线程分离、线程栈大小等等,所以干脆重新封装一个
int CreateThread(pthread_t *tid, void *(*routine)(void *), unsigned int stacksize)
{
	pthread_attr_t stThreadAttr;
	memset(&stThreadAttr, 0x00, sizeof(pthread_attr_t));

	int rc = pthread_attr_init(&stThreadAttr);//获取线程属性,下面有用
	if(rc != 0)
	{
		printf("pthread_attr_init error.");
		return -1;
	}
	
	rc = pthread_create(tid, &stThreadAttr, routine, NULL);//创建线程
	if(rc != 0)
	{
		printf("pthread_create error.\n");
		return -1;
	}

	//设置线程分离:目的防止进程意外退出时,线程的相关资源由内核回收
	pthread_attr_setdetachstate(&stThreadAttr, PTHREAD_CREATE_DETACHED);
	
	if(stacksize > 0)
	{
		//创建线程时,默认线程栈大小是16KB,如果需要设置栈大小,设置值必须要大于16KB才行,否则会设置失败
		rc = pthread_attr_setstacksize(&stThreadAttr, stacksize);
		if(rc != 0)
		{
			printf("pthread_attr_setstacksize error.\n");
			pthread_attr_destroy(&stThreadAttr);
			return -1;
		}
	}
	//init完线程属性后,必须调用destroy销毁
	pthread_attr_destroy(&stThreadAttr);

	return 0;
}

int main(void)
{
	int rc=SemaphoreInit();
	if(rc!=0)
	{
		return -1;
	}

	rc = CreateThread(&ConsumeThread, consume_thread, THREAD_SIZE);
	if(rc != 0)
	{
		SemaphoreDeInit();
		return -1;
	}

	rc = CreateThread(&ProduceThread, produce_thread, THREAD_SIZE);
	if(rc != 0)
	{
		SemaphoreDeInit();
		return -1;
	}

	while(1);
	return 0;
}

运行结果:
在这里插入图片描述
用于进程间通信的无名信号量,信号量句柄的存储位置为共享内存,共享内存有下面两种方式:

System VPOSIX
创建int shmget(key_t key, size_t size, int shmflg);int shm_open(const char *name, int oflag, mode_t mode);
销毁int shmctl(int shmid, int cmd, struct shmid_ds *buf);int shm_unlink(const char *name);

本文主要主要介绍无名信号量,共享内存相关就不展开了。需要用到下面几个接口:

接口说明
int shm_open(const char *name, int oflag, mode_t mode);根据name创建一块共享内存,创建成功后返回一个文件描述符,name位于/dev/shm/name
int shm_unlink(const char *name);删除name指定的路径,下次使用需要重新创建
void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);将fd对应的文件,按指定大小映射到内存中,以进行读写操作
int munmap(void *addr, size_t length);解除内存映射,addr为mmap的返回值
int ftruncate(int fd, off_t length);重置文件大小:mmap内存映射成功后,name对应的文件大小为0,需要重置为申请的大小

OK,下面上代码
生产者 sema_produce.c

#include <stdio.h>
#include <semaphore.h>
#include <pthread.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>

#define INDEFINITE			(0xFFFFFFFFu)
#define SHARED_MEM_PATH		"/proc_sema_mem"

typedef struct stShareMemSema{
	sem_t stSemaHandle;
	int commodity;
	int bIsFinished;
}ShareMemSema_t;

ShareMemSema_t *pSemaMem=NULL;

int bIsSemaInit=0;
int gShmfd=-1;

int shared_memory_init(void)
{
	int shmfd = shm_open(SHARED_MEM_PATH, O_RDWR|O_CREAT, 0666);
	if(shmfd<0)
	{
		if(errno!=EEXIST)
		{
			printf("shm_open failed\n");
			return -1;
		}
	}

	pSemaMem=(ShareMemSema_t *)mmap(NULL, sizeof(ShareMemSema_t), PROT_READ|PROT_WRITE, MAP_SHARED, shmfd, 0);
	if(!pSemaMem || pSemaMem==MAP_FAILED)
	{
		printf("%s\n", strerror(errno));
		shm_unlink(SHARED_MEM_PATH);
		return -1;
	}

	struct stat fst;
	memset(&fst, 0x00, sizeof(struct stat));
	fstat(shmfd, &fst);
	if(fst.st_size==0)
	{
		ftruncate(shmfd, sizeof(ShareMemSema_t));
	}
	
	return 0;
}

void shared_memory_destroy(void)
{
	if(pSemaMem)
	{
		munmap(pSemaMem, sizeof(ShareMemSema_t));
	}
	
	close(gShmfd);

	printf("shm_unlink %s\n", SHARED_MEM_PATH);
	shm_unlink(SHARED_MEM_PATH);
}

int SemaphoreInit(void)
{
	if(bIsSemaInit)
	{
		return 0;
	}

	if(shared_memory_init() != 0)
	{

		printf("shared_memory_init error.\n");
		return -1;
	}

	int rc=sem_init(&pSemaMem->stSemaHandle, 1, 1);
	if(rc!=0)
	{
		perror("sem_init");
		shared_memory_destroy();
		return -1;
	}

	bIsSemaInit=1;
	return 0;
}

void SemaphoreDeInit(void)
{
	if(pSemaMem)
	{
		sem_destroy(&pSemaMem->stSemaHandle);
	}

	shared_memory_destroy();
	bIsSemaInit=0;
}

int SemaphoreWait(sem_t *pSema, unsigned int timeout)
{
	if(!pSema)
	{
		printf("pSema is null.%s %d\n", __FUNCTION__, __LINE__);
		return -1;
	}

	struct timespec stTimeout;
	memset(&stTimeout, 0x00, sizeof(struct timespec));
	
	stTimeout.tv_sec=(long)timeout;
	int rc = sem_timedwait(pSema, &stTimeout);
	if(rc != 0)
	{
		if(errno==ETIMEDOUT)
		{
			printf("sem_timedwait timeout.\n");
		}

		return -1;
	}

	return 0;
}

int SemaphoreRelease(sem_t *pSema)
{
	if(!pSema)
	{
		printf("pSema is null.%s %d\n", __FUNCTION__, __LINE__);
		return -1;
	}

	int rc=sem_post(pSema);
	if(rc!=0)
	{
		printf("sem_post error.%s %d\n", __FUNCTION__, __LINE__);
		return -1;
	}

	return 0;
}

int main(void)
{
	int rc=SemaphoreInit();
	if(rc!=0)
	{
		return -1;
	}

	while(1)
	{
		SemaphoreWait(&pSemaMem->stSemaHandle, INDEFINITE);
		printf("Produce commodity:%d\n", pSemaMem->commodity);
		if(!pSemaMem->bIsFinished)
		{
			pSemaMem->commodity++;
		}
		else
		{
			printf("Produce finished:%d\n", pSemaMem->commodity);
			SemaphoreRelease(&pSemaMem->stSemaHandle);
			break;
		}
		SemaphoreRelease(&pSemaMem->stSemaHandle);
		sleep(1);
	}

	SemaphoreDeInit();
	return 0;
}

消费者 sema_consume.c

#include <stdio.h>
#include <semaphore.h>
#include <pthread.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <fcntl.h>


#define INDEFINITE		(0xFFFFFFFFu)
#define SHARED_MEM_PATH	"/proc_sema_mem"


typedef struct stShareMemSema{
	sem_t stSemaHandle;
	int commodity;
	int bIsFinished;
}ShareMemSema_t;

ShareMemSema_t *pSemaMem=NULL;

int bIsSemaInit=0;
int gShmfd=-1;

int shared_memory_init(void)
{
	gShmfd = shm_open(SHARED_MEM_PATH, O_RDWR|O_CREAT, 0666);
	if(gShmfd<0)
	{
		if(errno!=EEXIST)
		{
			printf("shm_open failed\n");
		}
		return -1;
	}

	pSemaMem=(ShareMemSema_t *)mmap(NULL, sizeof(ShareMemSema_t), PROT_READ|PROT_WRITE, MAP_SHARED, gShmfd, 0);
	if(pSemaMem==MAP_FAILED)
	{
		printf("mmap error.\n");
		shm_unlink(SHARED_MEM_PATH);
		return -1;
	}

	return 0;
}

void shared_memory_destroy(void)
{
	if(pSemaMem)
	{
		munmap(pSemaMem, sizeof(ShareMemSema_t));
	}

	close(gShmfd);
	#if 0
	shm_unlink(SHARED_MEM_PATH);
	#endif
}

int SemaphoreInit(void)
{
	if(bIsSemaInit)
	{
		return 0;
	}

	if(shared_memory_init() != 0)
	{

		printf("shared_memory_init error.\n");
		return -1;
	}

	int rc=sem_init(&pSemaMem->stSemaHandle, 0, 1);
	if(rc!=0)
	{
		perror("sem_init");
		shared_memory_destroy();
		return -1;
	}

	bIsSemaInit=1;
	return 0;
}

void SemaphoreDeInit(void)
{
	if(pSemaMem)
	{
		sem_destroy(&pSemaMem->stSemaHandle);
	}

	shared_memory_destroy();
	bIsSemaInit=0;
}

int SemaphoreWait(sem_t *pSema, unsigned int timeout)
{
	if(!pSema)
	{
		printf("pSema is null.%s %d\n", __FUNCTION__, __LINE__);
		return -1;
	}

	struct timespec stTimeout;
	memset(&stTimeout, 0x00, sizeof(struct timespec));
	
	stTimeout.tv_sec=(long)timeout;
	int rc = sem_timedwait(pSema, &stTimeout);
	if(rc != 0)
	{
		if(errno==ETIMEDOUT)
		{
			printf("sem_timedwait timeout.\n");
		}

		return -1;
	}

	return 0;
}

int SemaphoreRelease(sem_t *pSema)
{
	if(!pSema)
	{
		printf("pSema is null.%s %d\n", __FUNCTION__, __LINE__);
		return -1;
	}

	int rc=sem_post(pSema);
	if(rc!=0)
	{
		printf("sem_post error.%s %d\n", __FUNCTION__, __LINE__);
		return -1;
	}

	return 0;
}

int main(void)
{
	int rc=SemaphoreInit();
	if(rc!=0)
	{
		return -1;
	}

	while(1)
	{
		SemaphoreWait(&pSemaMem->stSemaHandle, INDEFINITE);
		printf("Consume commodity:%d\n", pSemaMem->commodity);
		if(pSemaMem->commodity==20)
		{
			pSemaMem->bIsFinished=1;
			SemaphoreRelease(&pSemaMem->stSemaHandle);
			sleep(1);
			break;
		}
		SemaphoreRelease(&pSemaMem->stSemaHandle);
		sleep(1);
	}

	SemaphoreDeInit();
	return 0;
}

简易Makefile

all:sema_produce sema_consume

.PHONY=all

sema_produce:sema_produce.o
	cc -g -o sema_produce sema_produce.o -pthread -lrt
sema_consume:sema_consume.o
	cc -g -o sema_consume sema_consume.o -pthread -lrt

.PHONY=clean

clean:
	rm -rf sema_produce sema_consume *.o

运行效果:
在这里插入图片描述
在这里插入图片描述

2. 有名信号量

sem_t *sem_open(const char *name, int oflag);

创建一个新的或打开一个已经存在的以name为路径的POSIX信号量。创建的信号量保存在虚拟文件系统中,一般保存在/dev/shm/目录下,名字通常为sem.name。‘name’的格式为“/name”,长度必须不超过251个字节(肯定足够用了,一般用不了这么多)。
下面借助共享内存来演示用于进程间同步的有名信号量:
name_sema_produce.c

#include <stdio.h>
#include <semaphore.h>
#include <pthread.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>


#define INDEFINITE			(0xFFFFFFFFu)
#define NAMED_SEMA_PATH		"/named_sema"
#define SHARE_MEM_PATH		"/shared_memory"

typedef struct stShareMem{
	int commodity;
	int bIsFinished;
}ShareMem_t;

typedef sem_t* SEMA_HANDLE;

int gShmfd=-1;
int bIsSemaInit=0;
SEMA_HANDLE pSemaMem=NULL;
ShareMem_t *pShareMemory=NULL;

int shared_memory_init(void)
{
	gShmfd = shm_open(SHARE_MEM_PATH, O_RDWR|O_CREAT, 0666);
	if(gShmfd<0)
	{
		if(errno!=EEXIST)
		{
			printf("shm_open failed\n");
		}
		return -1;
	}

	pShareMemory=(ShareMem_t *)mmap(NULL, sizeof(ShareMem_t), PROT_READ|PROT_WRITE, MAP_SHARED, gShmfd, 0);
	if(pShareMemory==MAP_FAILED)
	{
		printf("mmap error.\n");
		shm_unlink(SHARE_MEM_PATH);
		return -1;
	}

	ftruncate(gShmfd, sizeof(ShareMem_t));
	
	return 0;
}

void shared_memory_destroy(void)
{
	if(pShareMemory)
	{
		munmap(pShareMemory, sizeof(ShareMem_t));
	}

	close(gShmfd);

	shm_unlink(SHARE_MEM_PATH);
}

int SemaphoreInit(void)
{
	if(bIsSemaInit)
	{
		return 0;
	}

	pSemaMem = sem_open(NAMED_SEMA_PATH, O_RDWR|O_CREAT, 0666, 1);
	if(pSemaMem == SEM_FAILED)
	{
		perror("sem_open");
		printf("%s\n", strerror(errno));
		return -1;
	}

	int rc=sem_init(pSemaMem, 1, 1);
	if(rc!=0)
	{
		perror("sem_init");
		sem_close(pSemaMem);
		return -1;
	}

	bIsSemaInit=1;
	return 0;
}

void SemaphoreDeInit(void)
{
	if(pSemaMem)
	{
		sem_close(pSemaMem);
	}
	
	sem_unlink(NAMED_SEMA_PATH);
	bIsSemaInit=0;
}

int SemaphoreWait(SEMA_HANDLE pSema, unsigned int timeout)
{
	if(!pSema)
	{
		printf("pSema is null.%s %d\n", __FUNCTION__, __LINE__);
		return -1;
	}

	struct timespec stTimeout;
	memset(&stTimeout, 0x00, sizeof(struct timespec));
	
	stTimeout.tv_sec=(long)timeout;
	int rc = sem_timedwait(pSema, &stTimeout);
	if(rc != 0)
	{
		if(errno==ETIMEDOUT)
		{
			printf("sem_timedwait timeout.\n");
		}

		return -1;
	}

	return 0;
}

int SemaphoreRelease(SEMA_HANDLE pSema)
{
	if(!pSema)
	{
		printf("pSema is null.%s %d\n", __FUNCTION__, __LINE__);
		return -1;
	}

	int rc=sem_post(pSema);
	if(rc!=0)
	{
		printf("sem_post error.%s %d\n", __FUNCTION__, __LINE__);
		return -1;
	}

	return 0;
}

int main(void)
{
	int rc=SemaphoreInit();
	if(rc!=0)
	{
		return -1;
	}

	rc = shared_memory_init();
	if(rc != 0)
	{
		SemaphoreDeInit();
		return -1;
	}
	
	while(1)
	{
		SemaphoreWait(pSemaMem, INDEFINITE);
		printf("Produce commodity:%d\n", pShareMemory->commodity);
		if(!pShareMemory->bIsFinished)
		{
			pShareMemory->commodity++;
		}
		else
		{
			printf("Produce finished:%d\n", pShareMemory->commodity);
			SemaphoreRelease(pSemaMem);
			break;
		}
		SemaphoreRelease(pSemaMem);
		sleep(1);
	}

	SemaphoreDeInit();
	shared_memory_destroy();
	
	return 0;
}

name_sema_consume.c

#include <stdio.h>
#include <semaphore.h>
#include <pthread.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>

#define INDEFINITE			(0xFFFFFFFFu)
#define NAMED_SEMA_PATH		"/named_sema"
#define SHARE_MEM_PATH		"/shared_memory"

typedef struct stShareMem{
	int commodity;
	int bIsFinished;
}ShareMem_t;

typedef sem_t* SEMA_HANDLE;

int gShmfd=-1;
int bIsSemaInit=0;
SEMA_HANDLE pSemaMem=NULL;
ShareMem_t *pShareMemory=NULL;

int shared_memory_init(void)
{
	gShmfd = shm_open(SHARE_MEM_PATH, O_RDWR, 0666);
	if(gShmfd<0)
	{
		if(errno!=EEXIST)
		{
			printf("shm_open failed\n");
		}
		return -1;
	}

	pShareMemory=(ShareMem_t *)mmap(NULL, sizeof(ShareMem_t), PROT_READ|PROT_WRITE, MAP_SHARED, gShmfd, 0);
	if(pShareMemory==MAP_FAILED)
	{
		printf("mmap error.\n");
		return -1;
	}
	
	return 0;
}

void shared_memory_destroy(void)
{
	if(pShareMemory)
	{
		munmap(pShareMemory, sizeof(ShareMem_t));
	}

	close(gShmfd);
}

int SemaphoreInit(void)
{
	if(bIsSemaInit)
	{
		return 0;
	}

	pSemaMem = sem_open(NAMED_SEMA_PATH, O_RDWR|O_CREAT, 0666, 1);
	if(pSemaMem == SEM_FAILED)
	{
		perror("sem_open");
		printf("%s\n", strerror(errno));
		return -1;
	}

	int rc=sem_init(pSemaMem, 1, 1);
	if(rc!=0)
	{
		perror("sem_init");
		sem_close(pSemaMem);
		return -1;
	}

	bIsSemaInit=1;
	return 0;
}

void SemaphoreDeInit(void)
{
	if(pSemaMem)
	{
		sem_close(pSemaMem);
	}
	
	bIsSemaInit=0;
}

int SemaphoreWait(SEMA_HANDLE pSema, unsigned int timeout)
{
	if(!pSema)
	{
		printf("pSema is null.%s %d\n", __FUNCTION__, __LINE__);
		return -1;
	}

	struct timespec stTimeout;
	memset(&stTimeout, 0x00, sizeof(struct timespec));
	
	stTimeout.tv_sec=(long)timeout;
	int rc = sem_timedwait(pSema, &stTimeout);
	if(rc != 0)
	{
		if(errno==ETIMEDOUT)
		{
			printf("sem_timedwait timeout.\n");
		}

		return -1;
	}

	return 0;
}

int SemaphoreRelease(SEMA_HANDLE pSema)
{
	if(!pSema)
	{
		printf("pSema is null.%s %d\n", __FUNCTION__, __LINE__);
		return -1;
	}

	int rc=sem_post(pSema);
	if(rc!=0)
	{
		printf("sem_post error.%s %d\n", __FUNCTION__, __LINE__);
		return -1;
	}

	return 0;
}

int main(void)
{
	int rc=SemaphoreInit();
	if(rc!=0)
	{
		return -1;
	}

	rc = shared_memory_init();
	if(rc != 0)
	{
		SemaphoreDeInit();
		return -1;
	}
	
	while(1)
	{
		SemaphoreWait(pSemaMem, INDEFINITE);
		printf("Consume commodity:%d\n", pShareMemory->commodity);
		if(pShareMemory->commodity==20)
		{
			pShareMemory->bIsFinished=1;
			printf("Consume finished:%d\n", pShareMemory->commodity);
			SemaphoreRelease(pSemaMem);
			break;
		}
		SemaphoreRelease(pSemaMem);
		sleep(1);
	}

	SemaphoreDeInit();
	shared_memory_destroy();
	
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值