时间轮定时器的实现(参考Linux源码)

      时间轮 (Timing-Wheel) 算法类似于一以恒定速度旋转的左轮手枪,枪的撞针则撞击枪膛,如果枪膛中有子弹,则会被击发;与之相对应的是:对于 PerTickBookkeeping,其最本质的工作在于以 Tick 为单位增加时钟,如果发现有任何定时器到期,则调用相应的 ExpiryProcessing。设定一个循环为 N 个 Tick 单元,当前时间是在 S 个循环之后指向元素 i (i>=0 and i<= N - 1),则当前时间 (Current Time)Tc 可以表示为:Tc = S*N + i ;如果此时插入一个时间间隔 (Time Interval) 为 Ti 的定时器,设定它将会放入元素 n(Next) 中,则 n = (Tc + Ti)mod N = (S*N + i + Ti) mod N = (i + Ti) mod N 。如果我们的 N 足够的大,显然 StartTimer,StopTimer,PerTickBookkeeping 时,算法复杂度分别为 O(1),O(1),O(1) 。下图是一个简单的时间轮定时器:

如果需要支持的定时器范围非常的大,上面的实现方式则不能满足这样的需求。因为这样将消耗非常可观的内存,假设需要表示的定时器范围为:0 – 2^3-1ticks,则简单时间轮需要 2^32 个元素空间,这对于内存空间的使用将非常的庞大。也许可以降低定时器的精度,使得每个 Tick 表示的时间更长一些,但这样的代价是定时器的精度将大打折扣。现在的问题是,度量定时器的粒度,只能使用唯一粒度吗?想想日常生活中常遇到的水表,如下图 :

在上面的水表中,为了表示度量范围,分成了不同的单位,比如 1000,100,10 等等,相似的,表示一个 32bits 的范围,也不需要 2^32 个元素的数组。实际上,Linux 的内核把定时器分为 5 组,每组的粒度(对应水表例子的单位)分别表示为:1 jiffies,256 jiffies,256*64 jiffies,256*64*64 jiffies,256*64*64*64 jiffies,每组中桶的数量分别为:256,64,64,64,64,能表示的范围为 2^32 。有了这样的实现,驱动内核定时器的机制也可以通过水表的例子来理解了,就像水表,每个粒度上都有一个指针指向当前时间,时间以固定 tick 递增,而当前时间指针则也依次递增,如果发现当前指针的位置可以确定为一个注册的定时器,就触发其注册的回调函数。 Linux 内核定时器本质上是 Single-Shot Timer,如果想成为 Repeating Timer,可以在注册的回调函数中再次的注册自己。以下是实现代码:

// def.h

#ifndef _DEF_H_
#define _DEF_H_

#if defined(_WIN32) || defined(_WIN64)
	#define STDCALL	__stdcall
#else
	#define STDCALL __attribute__((stdcall))
#endif

//基本数据类型定义
typedef char int8 ;
typedef unsigned char uint8 ;
typedef uint8 byte ;
typedef short int16 ;
typedef unsigned short uint16 ;
typedef long int32 ;
typedef unsigned long uint32 ;

#endif //_DEF_H_



// Lock.h 同步锁

#ifndef _LOCK_H_
#define _LOCK_H_

#if defined(_WIN32) || defined(_WIN64)
	#include <Windows.h>
	typedef CRITICAL_SECTION LOCK ;
#else
	#include <pthread.h>
	typedef pthread_mutex_t LOCK ;
#endif

void InitLock(LOCK *pLock) ;
void UninitLock(LOCK *pLock) ;
void Lock(LOCK *pLock) ;
void Unlock(LOCK *pLock) ;

#endif //_LOCK_H_



// Lock.c 同步锁

#include "Lock.h"

void InitLock(LOCK *pLock)
{
#if defined(_WIN32) || defined(_WIN64)
	InitializeCriticalSection(pLock) ;
#else
	pthread_mutex_init(pLock, NULL);
#endif
}

void UninitLock(LOCK *pLock)
{
#if defined(_WIN32) || defined(_WIN64)
	DeleteCriticalSection(pLock) ;
#else
	pthread_mutex_destroy(pLock);
#endif
}

void Lock(LOCK *pLock)
{
#if defined(_WIN32) || defined(_WIN64)
	EnterCriticalSection(pLock) ;
#else
	pthread_mutex_lock(pLock);
#endif
}

void Unlock(LOCK *pLock)
{
#if defined(_WIN32) || defined(_WIN64)
	LeaveCriticalSection(pLock) ;
#else
	pthread_mutex_unlock(pLock);
#endif
}



// Thread.h  定时器调度线程
#ifndef _THREAD_H_
#define _THREAD_H_

#if defined(_WIN32) || defined(_WIN64)
	#include <Windows.h>
	#include <process.h> //_beginthreadex()
	typedef HANDLE THREAD ;
#else
	#include <pthread.h>
	typedef pthread_t THREAD ;
#endif

#include "def.h"

typedef void* (*FNTHREAD)(void *pParam);

THREAD ThreadCreate(FNTHREAD fnThreadProc, void *pParam) ;
void ThreadJoin(THREAD thread) ;
void ThreadDestroy(THREAD thread) ;

#endif //_THREAD_H_



// Thread.c 定时器调度线程

#include "Thread.h"

THREAD ThreadCreate(FNTHREAD fnThreadProc, void *pParam)
{
#if defined(_WIN32) || defined(_WIN64)
	if(fnThreadProc == NULL)
		return NULL ;
	return (THREAD)_beginthreadex(NULL, 0, (LPTHREAD_START_ROUTINE)fnThreadProc, pParam, 0, NULL) ;
#else
	THREAD t ;
	if(fnThreadProc == NULL)
		return 0 ;
	if(pthread_create(&t, NULL, fnThreadProc, pParam) == 0)
		return t ;
	else
		return (THREAD)0 ;
#endif
}

void ThreadJoin(THREAD thread)
{
#if defined(_WIN32) || defined(_WIN64)
	WaitForSingleObject(thread, INFINITE) ;
#else
	pthread_join(thread, NULL) ;
#endif
}

void ThreadDestroy(THREAD thread)
{
#if defined(_WIN32) || defined(_WIN64)
	CloseHandle(thread) ;
#else
	//
#endif
}



// Timer.h

#ifndef _TIMER_H_
#define _TIMER_H_

#include "def.h"
#include "Lock.h"
#include "Thread.h"

#define CONFIG_BASE_SMALL 0 //TVN_SIZE=64  TVR_SIZE=256
#define TVN_BITS (CONFIG_BASE_SMALL ? 4 : 6)
#define TVR_BITS (CONFIG_BASE_SMALL ? 6 : 8)
#define TVN_SIZE (1 << TVN_BITS)
#define TVR_SIZE (1 << TVR_BITS)
#define TVN_MASK (TVN_SIZE - 1)
#define TVR_MASK (TVR_SIZE - 1)
#define MAX_TVAL ((unsigned long)((1ULL << (TVR_BITS + 4*TVN_BITS)) - 1))

#define TIME_AFTER(a,b) ((long)(b) - (long)(a) < 0)
#define TIME_BEFORE(a,b) TIME_AFTER(b,a)
#define TIME_AFTER_EQ(a,b) ((long)(a) - (long)(b) >= 0)
#define TIME_BEFORE_EQ(a,b) TIME_AFTER_EQ(b,a)

typedef void (STDCALL *FNTIMRCALLBACK)(void *pParam) ;

typedef struct LIST_TIMER
{
	struct LIST_TIMER *pPrev ;
	struct LIST_TIMER *pNext ;
} LISTTIMER, *LPLISTTIMER ;

typedef struct TIMER_NODE
{
	struct LIST_TIMER ltTimer ;	//定时器链表的入口
	uint32 uExpires ;			//定时器超时的时刻
	uint32 uPeriod ;			//间隔多长时间触发一次
	FNTIMRCALLBACK fnTimer ;	//定时器处理函数
	void *pParam ;				//回调函数的参数
} TIMERNODE, *LPTIMERNODE ;

typedef struct TIMER_MANAGER
{
	LOCK lock ;				//同步锁
	THREAD thread ;			//线程句柄
	uint32 uExitFlag ;		//退出标识(0:Continue, other: Exit)
	uint32 uJiffies ;		//基准时间(当前时间)
	struct LIST_TIMER arrListTimer1[TVR_SIZE] ;
	struct LIST_TIMER arrListTimer2[TVN_SIZE] ;
	struct LIST_TIMER arrListTimer3[TVN_SIZE] ;
	struct LIST_TIMER arrListTimer4[TVN_SIZE] ;
	struct LIST_TIMER arrListTimer5[TVN_SIZE] ;
} TIMERMANAGER, *LPTIMERMANAGER ;

void STDCALL SleepMilliseconds(uint32 uMs) ;

//创建定时器管理器
LPTIMERMANAGER STDCALL CreateTimerManager(void) ;

//删除定时器管理器
void STDCALL DestroyTimerManager(LPTIMERMANAGER lpTimerManager) ;

//创建一个定时器。fnTimer回调函数地址。pParam回调函数的参数。uDueTime首次触发的超时时间间隔。uPeriod定时器循环周期,若为0,则该定时器只运行一次。
LPTIMERNODE STDCALL CreateTimer(LPTIMERMANAGER lpTimerManager, FNTIMRCALLBACK fnTimer, void *pParam, uint32 uDueTime, uint32 uPeriod) ;

//删除定时器
int32 STDCALL DeleteTimer(LPTIMERMANAGER lpTimerManager, LPTIMERNODE lpTimer) ;

#endif //_TIMER_H_



// Timer.c 定时器实现

#include <stddef.h>
#include <stdlib.h>
#if defined(_WIN32) || defined(_WIN64)
	#include <time.h>
#else
	#include <sys/time.h>
#endif
#include "Timer.h"

//获取基准时间。
static uint32 GetJiffies_old(void)
{
#if defined(_WIN32) || defined(_WIN64)
	SYSTEMTIME st ;
	struct tm t ;
	GetLocalTime(&st);
	t.tm_year = st.wYear - 1900;
	t.tm_mon  = st.wMonth - 1;
	t.tm_mday = st.wDay;
	t.tm_hour = st.wHour;
	t.tm_min  = st.wMinute;
	t.tm_sec  = st.wSecond;
	return (uint32)mktime(&t) * 1000 + st.wMilliseconds ;
#else
	struct timeval tv ;
	gettimeofday(&tv, NULL) ;
	return tv.tv_sec * 1000 + tv.tv_usec / 1000 ;
#endif
}

static uint32 GetJiffies(void)
{
#if defined(_WIN32) || defined(_WIN64)
	return GetTickCount() ;
#else
	//需连接 rt库,加-lrt参数
	struct timespec ts;
	clock_gettime(CLOCK_MONOTONIC, &ts);
	return (ts.tv_sec * 1000 + ts.tv_nsec / 1000000);
#endif
}

static void ListTimerInsert(struct LIST_TIMER *pNew, struct LIST_TIMER *pPrev, struct LIST_TIMER *pNext)
{
	pNext->pPrev = pNew;
	pNew->pNext = pNext;
	pNew->pPrev = pPrev;
	pPrev->pNext = pNew;
}

static void ListTimerInsertHead(struct LIST_TIMER *pNew, struct LIST_TIMER *pHead)
{
	ListTimerInsert(pNew, pHead, pHead->pNext);
}

static void ListTimerInsertTail(struct LIST_TIMER *pNew, struct LIST_TIMER *pHead)
{
	ListTimerInsert(pNew, pHead->pPrev, pHead);
}

static void ListTimerReplace(struct LIST_TIMER *pOld, struct LIST_TIMER *pNew)
{
	pNew->pNext = pOld->pNext ;
	pNew->pNext->pPrev = pNew ;
	pNew->pPrev = pOld->pPrev ;
	pNew->pPrev->pNext = pNew ;
}

static void ListTimerReplaceInit(struct LIST_TIMER *pOld, struct LIST_TIMER *pNew)
{
	ListTimerReplace(pOld, pNew) ;
	pOld->pNext = pOld ;
	pOld->pPrev = pOld ;
}

static void InitArrayListTimer(struct LIST_TIMER *arrListTimer, uint32 nSize)
{
	uint32 i ;
	for(i=0; i<nSize; i++)
	{
		arrListTimer[i].pPrev = &arrListTimer[i] ;
		arrListTimer[i].pNext = &arrListTimer[i] ;
	}
}

static void DeleteArrayListTimer(struct LIST_TIMER *arrListTimer, uint32 uSize)
{
	struct LIST_TIMER listTmr, *pListTimer ;
	struct TIMER_NODE *pTmr ;
	uint32 idx ;

	for(idx=0; idx<uSize; idx++)
	{
		ListTimerReplaceInit(&arrListTimer[idx], &listTmr) ;
		pListTimer = listTmr.pNext ;
		while(pListTimer != &listTmr)
		{
			pTmr = (struct TIMER_NODE *)((uint8 *)pListTimer - offsetof(struct TIMER_NODE, ltTimer)) ;
			pListTimer = pListTimer->pNext ;
			free(pTmr) ;
		}
	}
}

static void AddTimer(LPTIMERMANAGER lpTimerManager, LPTIMERNODE pTmr)
{
	struct LIST_TIMER *pHead ;
	uint32 i, uDueTime, uExpires ;
	//
	uExpires = pTmr->uExpires ; //定时器到期的时刻
	uDueTime = uExpires - lpTimerManager->uJiffies ;
	if (uDueTime < TVR_SIZE) //idx < 256 (2的8次方)
	{
		i = uExpires & TVR_MASK; //expires & 255
		pHead = &lpTimerManager->arrListTimer1[i] ;
	}
	else if (uDueTime < 1 << (TVR_BITS + TVN_BITS)) //idx < 16384 (2的14次方)
	{
		i = (uExpires >> TVR_BITS) & TVN_MASK; // i = (expires>>8) & 63
		pHead = &lpTimerManager->arrListTimer2[i] ;
	}
	else if (uDueTime < 1 << (TVR_BITS + 2 * TVN_BITS)) // idx < 1048576 (2的20次方)
	{
		i = (uExpires >> (TVR_BITS + TVN_BITS)) & TVN_MASK; // i = (expires>>14) & 63
		pHead = &lpTimerManager->arrListTimer3[i] ;
	}
	else if (uDueTime < 1 << (TVR_BITS + 3 * TVN_BITS)) // idx < 67108864 (2的26次方)
	{
		i = (uExpires >> (TVR_BITS + 2 * TVN_BITS)) & TVN_MASK; // i = (expires>>20) & 63
		pHead = &lpTimerManager->arrListTimer4[i] ;
	}
	else if ((signed long) uDueTime < 0)
	{
		/*
		* Can happen if you add a timer with expires == jiffies,
		* or you set a timer to go off in the past
		*/
		pHead = &lpTimerManager->arrListTimer1[(lpTimerManager->uJiffies & TVR_MASK)];
	}
	else
	{
		/* If the timeout is larger than 0xffffffff on 64-bit
		* architectures then we use the maximum timeout:
		*/
		if (uDueTime > 0xffffffffUL)
		{
			uDueTime = 0xffffffffUL;
			uExpires = uDueTime + lpTimerManager->uJiffies;
		}
		i = (uExpires >> (TVR_BITS + 3 * TVN_BITS)) & TVN_MASK; // i = (expires>>26) & 63
		pHead = &lpTimerManager->arrListTimer5[i];
	}
	ListTimerInsertTail(&pTmr->ltTimer, pHead) ;
}

static uint32 CascadeTimer(LPTIMERMANAGER lpTimerManager, struct LIST_TIMER *arrListTimer, uint32 idx)
{
	struct LIST_TIMER listTmr, *pListTimer ;
	struct TIMER_NODE *pTmr ;

	ListTimerReplaceInit(&arrListTimer[idx], &listTmr) ;
	pListTimer = listTmr.pNext ;
	while(pListTimer != &listTmr)
	{
		pTmr = (struct TIMER_NODE *)((uint8 *)pListTimer - offsetof(struct TIMER_NODE, ltTimer)) ;
		pListTimer = pListTimer->pNext ;
		AddTimer(lpTimerManager, pTmr) ;
	}
	return idx ;
}

static void RunTimer(LPTIMERMANAGER lpTimerManager)
{
#define INDEX(N) ((lpTimerManager->uJiffies >> (TVR_BITS + (N) * TVN_BITS)) & TVN_MASK)
	uint32 idx, uJiffies ;
	struct LIST_TIMER listTmrExpire, *pListTmrExpire ;
	struct TIMER_NODE *pTmr ;

	if(NULL == lpTimerManager)
		return ;
	uJiffies = GetJiffies() ;
	Lock(&lpTimerManager->lock) ;
	while(TIME_AFTER_EQ(uJiffies, lpTimerManager->uJiffies))
	{
		idx = lpTimerManager->uJiffies & TVR_MASK ;
		if (!idx &&
			(!CascadeTimer(lpTimerManager, lpTimerManager->arrListTimer2, INDEX(0))) &&
			(!CascadeTimer(lpTimerManager, lpTimerManager->arrListTimer3, INDEX(1))) &&
			!CascadeTimer(lpTimerManager, lpTimerManager->arrListTimer4, INDEX(2)))
				CascadeTimer(lpTimerManager, lpTimerManager->arrListTimer5, INDEX(3));
		//lpTimerManager->uJiffies++ ;
		pListTmrExpire = &listTmrExpire ;
		ListTimerReplaceInit(&lpTimerManager->arrListTimer1[idx], pListTmrExpire) ;
		pListTmrExpire = pListTmrExpire->pNext ;
		while(pListTmrExpire != &listTmrExpire)
		{
			pTmr = (struct TIMER_NODE *)((uint8 *)pListTmrExpire - offsetof(struct TIMER_NODE, ltTimer)) ;
			pListTmrExpire = pListTmrExpire->pNext ;
			pTmr->fnTimer(pTmr->pParam) ;
			//
			if(pTmr->uPeriod != 0)
			{
				pTmr->uExpires = lpTimerManager->uJiffies + pTmr->uPeriod ;
				AddTimer(lpTimerManager, pTmr) ;
			}
			else free(pTmr) ;
		}
		lpTimerManager->uJiffies++ ;
	}
	Unlock(&lpTimerManager->lock) ;
}

static void *ThreadRunTimer(void *pParam)
{
	LPTIMERMANAGER pTimerMgr ;

	pTimerMgr = (LPTIMERMANAGER)pParam ;
	if(pTimerMgr == NULL)
		return NULL ;
	while(!pTimerMgr->uExitFlag)
	{
		RunTimer(pTimerMgr) ;
		SleepMilliseconds(1) ;
	}
	return NULL ;
}

void STDCALL SleepMilliseconds(uint32 uMs)
{
#if defined(_WIN32) || defined(_WIN64)
	Sleep(uMs) ;
#else
	struct timeval tv;
	tv.tv_sec = 0 ;
	tv.tv_usec = uMs * 1000 ;
	select(0, NULL, NULL, NULL, &tv);
#endif
}

//创建定时器管理器
LPTIMERMANAGER STDCALL CreateTimerManager(void)
{
	LPTIMERMANAGER lpTimerMgr = (LPTIMERMANAGER)malloc(sizeof(TIMERMANAGER)) ;
	if(lpTimerMgr != NULL)
	{
		lpTimerMgr->thread = (THREAD)0 ;
		lpTimerMgr->uExitFlag = 0 ;
		InitLock(&lpTimerMgr->lock) ;
		lpTimerMgr->uJiffies = GetJiffies() ;
		InitArrayListTimer(lpTimerMgr->arrListTimer1, sizeof(lpTimerMgr->arrListTimer1)/sizeof(lpTimerMgr->arrListTimer1[0])) ;
		InitArrayListTimer(lpTimerMgr->arrListTimer2, sizeof(lpTimerMgr->arrListTimer2)/sizeof(lpTimerMgr->arrListTimer2[0])) ;
		InitArrayListTimer(lpTimerMgr->arrListTimer3, sizeof(lpTimerMgr->arrListTimer3)/sizeof(lpTimerMgr->arrListTimer3[0])) ;
		InitArrayListTimer(lpTimerMgr->arrListTimer4, sizeof(lpTimerMgr->arrListTimer4)/sizeof(lpTimerMgr->arrListTimer4[0])) ;
		InitArrayListTimer(lpTimerMgr->arrListTimer5, sizeof(lpTimerMgr->arrListTimer5)/sizeof(lpTimerMgr->arrListTimer5[0])) ;
		lpTimerMgr->thread = ThreadCreate(ThreadRunTimer, lpTimerMgr) ;
	}
	return lpTimerMgr ;
}

//删除定时器管理器
void STDCALL DestroyTimerManager(LPTIMERMANAGER lpTimerManager)
{
	if(NULL == lpTimerManager)
		return ;
	lpTimerManager->uExitFlag = 1 ;
	if((THREAD)0 != lpTimerManager->thread)
	{
		ThreadJoin(lpTimerManager->thread) ;
		ThreadDestroy(lpTimerManager->thread) ;
		lpTimerManager->thread = (THREAD)0 ;
	}
	DeleteArrayListTimer(lpTimerManager->arrListTimer1, sizeof(lpTimerManager->arrListTimer1)/sizeof(lpTimerManager->arrListTimer1[0])) ;
	DeleteArrayListTimer(lpTimerManager->arrListTimer2, sizeof(lpTimerManager->arrListTimer2)/sizeof(lpTimerManager->arrListTimer2[0])) ;
	DeleteArrayListTimer(lpTimerManager->arrListTimer3, sizeof(lpTimerManager->arrListTimer3)/sizeof(lpTimerManager->arrListTimer3[0])) ;
	DeleteArrayListTimer(lpTimerManager->arrListTimer4, sizeof(lpTimerManager->arrListTimer4)/sizeof(lpTimerManager->arrListTimer4[0])) ;
	DeleteArrayListTimer(lpTimerManager->arrListTimer5, sizeof(lpTimerManager->arrListTimer5)/sizeof(lpTimerManager->arrListTimer5[0])) ;
	UninitLock(&lpTimerManager->lock) ;
	free(lpTimerManager) ;
}

//创建一个定时器。fnTimer回调函数地址。pParam回调函数的参数。uDueTime首次触发的超时时间间隔。uPeriod定时器循环周期,若为0,则该定时器只运行一次。
LPTIMERNODE STDCALL CreateTimer(LPTIMERMANAGER lpTimerManager, FNTIMRCALLBACK fnTimer, void *pParam, uint32 uDueTime, uint32 uPeriod)
{
	LPTIMERNODE pTmr = NULL ;
	if(NULL == fnTimer || NULL == lpTimerManager)
		return NULL ;
	pTmr = (LPTIMERNODE)malloc(sizeof(TIMERNODE)) ;
	if(pTmr != NULL)
	{
		pTmr->uPeriod = uPeriod ;
		pTmr->fnTimer = fnTimer ;
		pTmr->pParam = pParam ;
		//
		Lock(&lpTimerManager->lock) ;
		pTmr->uExpires = lpTimerManager->uJiffies + uDueTime ;
		AddTimer(lpTimerManager, pTmr) ;
		Unlock(&lpTimerManager->lock) ;
	}
	return pTmr ;
}

//删除定时器
int32 STDCALL DeleteTimer(LPTIMERMANAGER lpTimerManager, LPTIMERNODE lpTimer)
{
	struct LIST_TIMER *pListTmr ;
	if(NULL != lpTimerManager && NULL != lpTimer)
	{
		Lock(&lpTimerManager->lock) ;
		pListTmr = &lpTimer->ltTimer ;
		pListTmr->pPrev->pNext = pListTmr->pNext ;
		pListTmr->pNext->pPrev = pListTmr->pPrev ;
		free(lpTimer) ;
		Unlock(&lpTimerManager->lock) ;
		return 0 ;
	}
	else
		return -1 ;
}



// main.c 测试例子

#include <stdio.h>
#include "Timer.h"

void STDCALL TimerFun(void *pParam)
{
	LPTIMERMANAGER pMgr ;
	pMgr = (LPTIMERMANAGER)pParam ;
	printf("Timer expire! Jiffies: %lu\n", pMgr->uJiffies) ;
}

int main(void)
{
	LPTIMERMANAGER pMgr ;
	LPTIMERNODE pTn ;
	pMgr = CreateTimerManager() ;
	CreateTimer(pMgr, TimerFun, pMgr, 2000, 0) ;
	pTn = CreateTimer(pMgr, TimerFun, pMgr, 4000, 1000) ;
	SleepMilliseconds(10001) ;
	DeleteTimer(pMgr, pTn) ;
	SleepMilliseconds(3000) ;
	DestroyTimerManager(pMgr) ;
	return 0 ;
}



完整代码下载:百度云盘

转载于:https://my.oschina.net/osbin/blog/222287

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值