当前位置:   article > 正文

FreeRTOS内存管理之heap_4.c

heap_4.c

FreeRTOS内存管理之heap_4.c源码解析


每当创建任务、队列、互斥量、软件定时器、信号量或事件组时,RTOS内核会为它们分配RAM。标准函数库中的malloc()和free()函数有些时候能够用于完成这个任务,但是:

  • 在嵌入式系统中,它们并不总是可以使用的;
  • 它们会占用更多宝贵的代码空间;
  • 它们没有线程保护;
  • 它们不具有确定性(每次调用执行的时间可能会不同);

当RTOS内核需要RAM时,调用**pvPortMalloc()函数来代替malloc()函数。当RAM要被释放时,调用vPortFree()**函数来代替free()函数。

xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) );
xWantedSize 是要申请的字节数
portBYTE_ALIGNMENT是字节对齐数
portBYTE_ALIGNMENT_MASK 是字节对齐掩码
  • 1
  • 2
  • 3
  • 4

一、heap_4.c
第四种内存分配方法与第二种比较相似,只不过增加了一个合并算法,将相邻的空闲内存块合并成一个大块。与第一种和第二种内存管理策略一样,内存堆仍然是一个大数组,定义为:

static uint8_t ucHeap[ configTOTAL_HEAP_SIZE];
  • 1

1.1 内存申请:pvPortMalloc()
使用一个链表结构来跟踪记录空闲内存块。结构体定义为:

typedef struct A_BLOCK_LINK
{
    struct A_BLOCK_LINK *pxNextFreeBlock;   /*指向列表中下一个空闲块*/
    size_t xBlockSize;                      /*当前空闲块的大小,包括链表结构大小*/
} BlockLink_t;
  • 1
  • 2
  • 3
  • 4
  • 5

test
第四种内存管理策略和第二种内存管理策略还有一个很大的不同是:第四种内存管理策略的空闲块链表不是以内存块大小为存储顺序,而是以内存块起始地址大小为存储顺序,地址小的在前,地址大的在后。这也是为了适应合并算法而作的改变。
函数中会用到几个局部静态变量在这里简单说明一下:

  • xFreeBytesRemaining:表示当前未分配的内存堆大小
  • xMinimumEverFreeBytesRemaining:表示未分配内存堆空间历史最小值。这个值跟xFreeBytesRemaining有很大区别,只有记录未分配内存堆的最小值,才能知道最坏情况下内存堆的使用情况。
  • xBlockAllocatedBit:这个变量在第一次调用内存申请函数时被初始化,将它能表示的数值的最高位置1。比如对于32位系统,这个变量被初始化为0x80000000(最高位为1)。内存管理策略使用这个变量来标识一个内存块是否空闲。如果内存块被分配出去,则内存块链表结构成员xBlockSize按位或上这个变量(即xBlockSize最高位置1),在释放一个内存块时,会把xBlockSize的最高位清零。

test
内存申请过程:

  • 首先计算实际要分配的内存大小,判断申请内存合法性,如果合法则从链表头xStart开始查找,如果某个空闲块的xBlockSize字段大小能容得下要申请的内存,则将这块内存取出合适的部分返回给申请者,剩下的内存块组成一个新的空闲块,按照空闲块起始地址大小顺序插入到空闲块链表中,地址小的在前,地址大的在后。
  • 在插入到空闲块链表的过程中,还会执行合并算法:判断这个块是不是可以和上一个空闲块合并成一个大块,如果可以则合并;然后再判断能不能和下一个空闲块合并成一个大块,如果可以则合并!合并算法是第四种内存管理策略和第二种内存管理策略最大的不同!经过几次内存申请和释放后。
    源代码解析:
/* 定义一个结构体 */
typedef struct A_BLOCK_LINK
{
#ifdef MTK_SUPPORT_HEAP_DEBUG
    uint32_t magic_header;
#endif
	struct A_BLOCK_LINK *pxNextFreeBlock;	/*<< The next free block in the list. */
	size_t xBlockSize;						/*<< The size of the free block. */
#ifdef MTK_SUPPORT_HEAP_DEBUG
	uint32_t xLinkRegAddr;
#endif /* MTK_SUPPORT_HEAP_DEBUG */
} BlockLink_t;

/* 创建两个结构体变量,分别指向单链表的头和尾部 */
static BlockLink_t xStart, *pxEnd = NULL;

void *pvPortMalloc( size_t xWantedSize )
{
    BlockLink_t *pxBlock, *pxPreviousBlock, *pxNewBlockLink;
    void *pvReturn = NULL;
	vTaskSuspendAll();      //将所有的任务挂起
	{
		if( pxEnd == NULL )
		{
			prvHeapInit(); //进入堆初始化,根据系统的需要,分配合法合理的堆空间
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}

		/* 检查申请分配的内存是否具有合法性和检查申请的内存是否过大 */
		if( ( xWantedSize & xBlockAllocatedBit ) == 0 )
		{
			/* 计算实际要分配的内存大小,包含链接结构体BlockLink_t在内,并且要向上字节对齐 */
			if( xWantedSize > 0 )
			{
				xWantedSize += xHeapStructSize;

				/* 确保块始终与所需的字节数对齐。 */
				if( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) != 0x00 )
				{
					/* 字节对齐 */
					xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) );
					configASSERT( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) == 0 );
				}
				else
				{
					mtCOVERAGE_TEST_MARKER();
				}
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}

            /*  首先遍历链表,找到第1块能比申请空间大小大的空闲块,修改空闲块的信息,记录用户可用的内存首地址。 */
			if( ( xWantedSize > 0 ) && ( xWantedSize <= xFreeBytesRemaining ) )
			{
				/* 从xStart起始(最低地址)块遍历列表,直到找到一个足够大的空闲块。 */
				pxPreviousBlock = &xStart;
				pxBlock = xStart.pxNextFreeBlock;
				while( ( pxBlock->xBlockSize < xWantedSize ) && ( pxBlock->pxNextFreeBlock != NULL ) )
				{
					pxPreviousBlock = pxBlock;
					pxBlock = pxBlock->pxNextFreeBlock;
				}

				/* 如果找到结束标记,则没有找到足够大小的块;否则则进行内存分配工作 */
				if( pxBlock != pxEnd )
				{
					/* 返回分配的内存指针,要跳过内存开始处的BlockLink_t结构体 */
					pvReturn = ( void * ) ( ( ( uint8_t * ) pxPreviousBlock->pxNextFreeBlock ) + xHeapStructSize );
					#ifdef MTK_SUPPORT_HEAP_DEBUG
					
					pxPreviousBlock->pxNextFreeBlock->xLinkRegAddr = xLinkRegAddr;
                    configASSERT(pxPreviousBlock->pxNextFreeBlock->magic_header == MAGIC_HEAP_OVERHEADER);
					#endif /* MTK_SUPPORT_HEAP_DEBUG */

					/* 将已经分配出去的内存块从空闲块链表中删除 */
					pxPreviousBlock->pxNextFreeBlock = pxBlock->pxNextFreeBlock;

					/* 如果剩下的内存足够大,则组成一个新的空闲块 */
					/* 如果分配出去的空闲块比申请的空间大很多,则将该空闲块进行分割,把剩余的部分重新添加到链表中。*/
					if( ( pxBlock->xBlockSize - xWantedSize ) > heapMINIMUM_BLOCK_SIZE )
					{
						/* 在剩余内存块的起始位置放置一个链表结构并初始化链表成员*/
						pxNewBlockLink = ( void * ) ( ( ( uint8_t * ) pxBlock ) + xWantedSize );
						configASSERT( ( ( ( uint32_t ) pxNewBlockLink ) & portBYTE_ALIGNMENT_MASK ) == 0 );

				
						pxNewBlockLink->xBlockSize = pxBlock->xBlockSize - xWantedSize;
						pxBlock->xBlockSize = xWantedSize;
                        #ifdef MTK_SUPPORT_HEAP_DEBUG
                        pxNewBlockLink->magic_header = MAGIC_HEAP_OVERHEADER;
                        #endif
						/* 将剩余的空闲块插入到空闲块列表中,按照空闲块的地址大小顺序,地址小的在前,地址大的在后 */
						prvInsertBlockIntoFreeList( pxNewBlockLink );
					}
					else
					{
						mtCOVERAGE_TEST_MARKER();
					}

					xFreeBytesRemaining -= pxBlock->xBlockSize;
                    /* 保存未分配内存堆空间历史最小值 */
					if( xFreeBytesRemaining < xMinimumEverFreeBytesRemaining )
					{
						xMinimumEverFreeBytesRemaining = xFreeBytesRemaining;
					}
					else
					{
						mtCOVERAGE_TEST_MARKER();
					}

					/* 将已经分配的内存块标识为"已分配" */
					pxBlock->xBlockSize |= xBlockAllocatedBit;
					pxBlock->pxNextFreeBlock = NULL;
					xNumberOfSuccessfulAllocations++;
				}
				else
				{
					mtCOVERAGE_TEST_MARKER();
				}
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}
        /*  traceMALLOC( pvReturn, xWantedSize)是一个宏,用于输出内存分配的调试信息,这个宏定义在FreeRTOS.h中,默认为空,如果需要将这些调试信息输出到串口...*/
		traceMALLOC( pvReturn, xWantedSize );
	}
	( void ) xTaskResumeAll();
    /* 如果内存分配失败,调用钩子函数 */
	#if( configUSE_MALLOC_FAILED_HOOK == 1 )
	{
		if( pvReturn == NULL )
		{
			extern void vApplicationMallocFailedHook( void );
			vApplicationMallocFailedHook();
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}
	}
	#endif

	configASSERT( ( ( ( uint32_t ) pvReturn ) & portBYTE_ALIGNMENT_MASK ) == 0 );
	return pvReturn;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156

下面是链表的初始化
heap_2.c中链表的尾部数据并未保存在链表内,是以变量的形式存在的。heap_4.c中的链表尾部数据结构保存在链表空间尾部。

//关于堆栈的初始化
//第一步:起始地址做字节对齐,保存pucAlignedHeap 可用空间大小为xTotalHeapSize 
//第二步:计算首尾 ,这里需要注意的是链表的尾部指针是保存到该地址尾部的
//第三部:完成链表的初始化,记录内存块信息
 
static void prvHeapInit( void )
{
    BlockLink_t *pxFirstFreeBlock;
    uint8_t *pucAlignedHeap;
    size_t uxAddress;
    size_t xTotalHeapSize = configTOTAL_HEAP_SIZE;
 
	//起始地址做字节对齐处理
	uxAddress = ( size_t ) ucHeap;
	if( ( uxAddress & portBYTE_ALIGNMENT_MASK ) != 0 )
	{
		uxAddress += ( portBYTE_ALIGNMENT - 1 );
		uxAddress &= ~( ( size_t ) portBYTE_ALIGNMENT_MASK );
		xTotalHeapSize -= uxAddress - ( size_t ) ucHeap;  //减去对齐舍弃的字节
	}
 
	pucAlignedHeap = ( uint8_t * ) uxAddress;         //对齐后可以用的起始地址

	//xStart链表的头 
	xStart.pxNextFreeBlock = ( void * ) pucAlignedHeap; 
	xStart.xBlockSize = ( size_t ) 0;

	//pxEnd链表的尾
	uxAddress = ( ( size_t ) pucAlignedHeap ) + xTotalHeapSize;
	uxAddress -= xHeapStructSize;
	uxAddress &= ~( ( size_t ) portBYTE_ALIGNMENT_MASK );
	pxEnd = ( void * ) uxAddress;                       
	pxEnd->xBlockSize = 0;
	pxEnd->pxNextFreeBlock = NULL;
 
	//开始时候将内存堆整个看作为一个空闲内存块
	pxFirstFreeBlock = ( void * ) pucAlignedHeap;
	pxFirstFreeBlock->xBlockSize = uxAddress - ( size_t ) pxFirstFreeBlock;
	pxFirstFreeBlock->pxNextFreeBlock = pxEnd;
 

	xMinimumEverFreeBytesRemaining = pxFirstFreeBlock->xBlockSize; //记录最小的空闲内存块大小
	xFreeBytesRemaining = pxFirstFreeBlock->xBlockSize;            //剩余内存堆大小
 
	/* Work out the position of the top bit in a size_t variable. */
	xBlockAllocatedBit = ( ( size_t ) 1 ) << ( ( sizeof( size_t ) * heapBITS_PER_BYTE ) - 1 );//用来标记内存块是否被使用
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47

空闲块链表的插入:会判断前后的空闲块能否合并,解决内存碎片化的问题。

static void prvInsertBlockIntoFreeList( BlockLink_t *pxBlockToInsert )
{
    BlockLink_t *pxIterator;
    uint8_t *puc;
 
	//遍历空闲内存块链表,找出内存块插入点。内存块是按照地址从低到高连接在一起的
	for( pxIterator = &xStart; pxIterator->pxNextFreeBlock < pxBlockToInsert; pxIterator = pxIterator->pxNextFreeBlock )
	{
		/* Nothing to do here, just iterate to the right position. */
	}

	//插入内存块,检查和前面的内存是否可以合并,如果内存可以合并则合并
	puc = ( uint8_t * ) pxIterator;
	if( ( puc + pxIterator->xBlockSize ) == ( uint8_t * ) pxBlockToInsert )
	{
		pxIterator->xBlockSize += pxBlockToInsert->xBlockSize;
		pxBlockToInsert = pxIterator;
	}
	else
	{
		mtCOVERAGE_TEST_MARKER();
	}
 
	//检查是否可以与后面的内存合并
	puc = ( uint8_t * ) pxBlockToInsert;
	if( ( puc + pxBlockToInsert->xBlockSize ) == ( uint8_t * ) pxIterator->pxNextFreeBlock )
	{
		if( pxIterator->pxNextFreeBlock != pxEnd )
		{
		    //合并
			pxBlockToInsert->xBlockSize += pxIterator->pxNextFreeBlock->xBlockSize;
			pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock->pxNextFreeBlock;
		}
		else
		{
			pxBlockToInsert->pxNextFreeBlock = pxEnd;
		}
	}
	else
	{
		pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock;
	}
 
	//如果不能合并的话,就普通处理
	if( pxIterator != pxBlockToInsert )
	{
		pxIterator->pxNextFreeBlock = pxBlockToInsert;
	}
	else
	{
		mtCOVERAGE_TEST_MARKER();
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53

在pvPortMalloc()中,若分配出去的空闲块比申请的内存大太多,则需要将内存进行分割,并把分割出的部分重新添加至链表中。
在heap_4.c中的重点为:
将分割出来的空闲块重新添加到链表中的过程,即使用prvInsertBlockIntoFreeList()将其空闲块添加至原本的链表中,则不会产生内存碎片。

static void prvInsertBlockIntoFreeList( BlockLink_t *pxBlockToInsert )
{
    BlockLink_t *pxIterator;
    uint8_t *puc;

    /*  遍历找到分割出来的内存块的下一个内存块,将pxIterator指向它  */
	for( pxIterator = &xStart; pxIterator->pxNextFreeBlock < pxBlockToInsert; pxIterator = pxIterator->pxNextFreeBlock )
	{
		/* Nothing to do here, just iterate to the right position. */
	}

	puc = ( uint8_t * ) pxIterator;
	/* 可以合并的标准为pxIterator的首地址加上pxIterator的块大小之后等于pxBlockToInsert的首地址。相等就说明两个块是相邻的。 */
	if( ( puc + pxIterator->xBlockSize ) == ( uint8_t * ) pxBlockToInsert )
	{
		pxIterator->xBlockSize += pxBlockToInsert->xBlockSize;
		pxBlockToInsert = pxIterator;
	}
	else
	{
		mtCOVERAGE_TEST_MARKER();
	}

	puc = ( uint8_t * ) pxBlockToInsert;
    /* FreeRTOS再试着将pxBlockToInsert和pxIterator指向的下一个空闲块进行合并。可合并的标准和刚刚说的一样,只是这次用pxBlockToInsert的首地址加上pxBlockToInsert的块大小与pxIterator指向的下一个块地址比较。*/
	if( ( puc + pxBlockToInsert->xBlockSize ) == ( uint8_t * ) pxIterator->pxNextFreeBlock )
	{
		if( pxIterator->pxNextFreeBlock != pxEnd )
		{
			pxBlockToInsert->xBlockSize += pxIterator->pxNextFreeBlock->xBlockSize;
			pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock->pxNextFreeBlock;
		}
		else
		{
			pxBlockToInsert->pxNextFreeBlock = pxEnd;//若是没有合并,则需要修改链表的next指针
		}
	}
	else
	{
		pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock;
	}

	if( pxIterator != pxBlockToInsert )
	{
		pxIterator->pxNextFreeBlock = pxBlockToInsert;
	}
	else
	{
		mtCOVERAGE_TEST_MARKER();
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51

1.2 内存释放vPortFree()
判断指针合法性的时候多了两个条件,一个是检查回收的块大小最高位是否为1,为1才是合法的,毕竟是分配出去了嘛。第二个是Next指针是否为空,为空了说明那是pxEnd,那就不能回收了。在这两个判断之前也有这两个条件的断言configASSERT(),定义在FreeRTOS.h里,同样也是定义为空,可能是留给用户另外用的吧。
源代码解析:

/* 放置在每个分配的内存块开头的结构的大小必须正确对齐字节。*/
static const size_t xHeapStructSize	= ( ( sizeof( BlockLink_t ) + ( portBYTE_ALIGNMENT - 1 ) ) & ~portBYTE_ALIGNMENT_MASK );

void vPortFree( void *pv )
{
uint8_t *puc = ( uint8_t * ) pv;
BlockLink_t *pxLink;

	if( pv != NULL )
	{
		/* 被释放的内存在它之前会有一个BlockLink_t结构,向前偏移,重新找回BlockLink_t */
		/* 根据参数地址找出内存块链表结构 */
		puc -= xHeapStructSize;

		/* 这种类型转换是为了防止编译器发出警告 */
		pxLink = ( void * ) puc;

		/* 检查块是否被实际分配。 */
		configASSERT( ( pxLink->xBlockSize & xBlockAllocatedBit ) != 0 );
		configASSERT( pxLink->pxNextFreeBlock == NULL );
#ifdef MTK_SUPPORT_HEAP_DEBUG
        configASSERT(pxLink->magic_header == MAGIC_HEAP_OVERHEADER);
#endif
        /*    。*/
		if( ( pxLink->xBlockSize & xBlockAllocatedBit ) != 0 )
		{
			if( pxLink->pxNextFreeBlock == NULL )
			{
				/* The block is being returned to the heap - it is no longer allocated. */
				pxLink->xBlockSize &= ~xBlockAllocatedBit;
#ifdef MTK_SUPPORT_HEAP_DEBUG
                configASSERT(((BlockLink_t *)((uint8_t *)puc + pxLink->xBlockSize))->magic_header == MAGIC_HEAP_OVERHEADER);
#endif

				vTaskSuspendAll();//通过挂起调度器来创建临界区,挂起调度器有些时候也被称为锁定调度器
				{
					/* Add this block to the list of free blocks. */
					xFreeBytesRemaining += pxLink->xBlockSize;
					traceFREE( pv, pxLink->xBlockSize );
					prvInsertBlockIntoFreeList( ( ( BlockLink_t * ) pxLink ) );
					xNumberOfSuccessfulFrees++;
				}
				( void ) xTaskResumeAll();//挂起,直到调度器被唤醒后才会得到执行
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/weixin_40725706/article/detail/75934
推荐阅读