当前位置:   article > 正文

LwIP 的内存管理——笔记_lwip 内存池

lwip 内存池

目录

1、LwIP的内存分配策略

1.1、动态内存池策略(POOL)

原理

动态内存池代码分析

动态内存池的内存分配

动态内存池的内存释放

1.2、动态内存堆策略

原理

动态内存堆代码分析

 动态内存堆的内存分配

动态内存堆的内存释放

2、LwIP 中的配置


1、LwIP的内存分配策略

        LwIP的内存分配策略有3种,分别为:动态内存池策略、 动态内存堆策略、C标准库的malloc和free内存分配策略。

1.1、动态内存池策略(POOL)

原理

       用户只能申请大小固定的内存块,在内存初始化的时候,系统会将所有可用的内存区域划分为 N 块固定大小的内存,然后将这些内存块通过单链表的方式连接起来,用户在申请内存块的时候就直接从链表的头部取出一个内存块进行分配,同理释放内存块的时候也是很简单,直接将内存块释放到链表的头部即可,这样子分配内存的时间就是固定的,非常高效。但是缺点也是很明显的,用户只能申请固定大小的内存块,如果内存块无法满足用户的需求,那么则无法申请成功,而如果将内存块大小变大,那么在用户需要极小的内存的时候就会造成内存的浪费,这也是不适合的。 
        但由于协议栈里面有大量的协议首部,如 TCP 首部、UDP 首部,IP 首部,以太网首部等,这些协议首部长度都是固定不变的,那么我们就能采用这种方式分配这些固定大小的内存空间,这样子的效率就会大大提高,并且无论怎么申请与释放,都不会产生内存碎片,这就让系统能很稳定地运行。内存池示意图具体见下图:

动态内存池代码分析

        在 LwIP 协议栈初始化(lwip_init(void))的时候, memp_init()会对内存池进行初始化,代码如下:

  1. memp_init(void)
  2. {
  3. u16_t i;
  4. /* for every pool: */
  5. for (i = 0; i < LWIP_ARRAYSIZE(memp_pools); i++) {
  6. memp_init_pool(memp_pools[i]);
  7. #if LWIP_STATS && MEMP_STATS
  8. lwip_stats.memp[i] = memp_pools[i]->stats;
  9. #endif
  10. }
  11. #if MEMP_OVERFLOW_CHECK >= 2
  12. /* check everything a first time to see if it worked */
  13. memp_overflow_check_all();
  14. #endif /* MEMP_OVERFLOW_CHECK >= 2 */
  15. }

        memp_pools[MEMP_MAX]结构体数组解析如下:

  1. const struct memp_desc *const memp_pools[MEMP_MAX] = {
  2. #define LWIP_MEMPOOL(name,num,size,desc) &memp_ ## name,
  3. #include "lwip/priv/memp_std.h"
  4. };
  5. ......
  6. //MEMP_MAX由memp_std.h中的部分宏决定
  7. typedef enum {
  8. #define LWIP_MEMPOOL(name,num,size,desc) MEMP_##name,
  9. #include "lwip/priv/memp_std.h"
  10. MEMP_MAX
  11. } memp_t;
  12. ......
  13. struct memp_desc {
  14. #if defined(LWIP_DEBUG) || MEMP_OVERFLOW_CHECK || LWIP_STATS_DISPLAY
  15. /** Textual description */
  16. const char *desc;
  17. #endif /* LWIP_DEBUG || MEMP_OVERFLOW_CHECK || LWIP_STATS_DISPLAY */
  18. #if MEMP_STATS
  19. /** Statistics */
  20. struct stats_mem *stats;
  21. #endif
  22. /** Element size */
  23. u16_t size;
  24. #if !MEMP_MEM_MALLOC
  25. /** Number of elements */
  26. u16_t num;
  27. /** Base address */
  28. u8_t *base;
  29. /** First free element of each pool. Elements form a linked list. */
  30. struct memp **tab;
  31. #endif /* MEMP_MEM_MALLOC */
  32. };

        最终memp_pools[MEMP_MAX]编译内容如下(伪代码):

  1. #define LWIP_MEMPOOL_DECLARE(name,num,size,desc) \
  2. LWIP_DECLARE_MEMORY_ALIGNED(memp_memory_ ## name ## _base, ((num) * (MEMP_SIZE + MEMP_ALIGN_SIZE(size)))); \
  3. \
  4. LWIP_MEMPOOL_DECLARE_STATS_INSTANCE(memp_stats_ ## name) \
  5. \
  6. static struct memp *memp_tab_ ## name; \
  7. \
  8. const struct memp_desc memp_ ## name = { \
  9. DECLARE_LWIP_MEMPOOL_DESC(desc) \
  10. LWIP_MEMPOOL_DECLARE_STATS_REFERENCE(memp_stats_ ## name) \
  11. LWIP_MEM_ALIGN_SIZE(size), \
  12. (num), \
  13. memp_memory_ ## name ## _base, \
  14. &memp_tab_ ## name \
  15. };
  16. eg:#define LWIP_MEMPOOL_DECLARE(TCP_PCB,5,sizeof(tcp_pcb),"TCP_PCB")
  17. LWIP_DECLARE_MEMORY_ALIGNED(memp_memory_TCP_PCB_base, (5 * (0 + sizeof(tcp_pcb)字节对齐)));
  18. #define LWIP_DECLARE_MEMORY_ALIGNED(variable_name, size) u8_t memp_memory_TCP_PCB_base[5 * sizeof(tcp_pcb)字节对齐 + 4 - 1]
  19. LWIP_MEMPOOL_DECLARE_STATS_INSTANCE(memp_stats_TCP_PCB)
  20. memp_stats_TCP_PCB {
  21. const char *name;
  22. uin16_t err;
  23. uin16_t avail;
  24. uin16_t used;
  25. uin16_t max;
  26. uin16_t illegal;
  27. };
  28. static struct memp *memp_tab_TCP_PCB;
  29. struct memp {
  30. struct memp *next;
  31. };
  32. const struct memp_desc memp_TCP_PCB = { \
  33. DECLARE_LWIP_MEMPOOL_DESC(desc) \
  34. "TCP_PCB"
  35. LWIP_MEMPOOL_DECLARE_STATS_REFERENCE(memp_stats_ ## name) \
  36. &memp_stats_TCP_PCB
  37. LWIP_MEM_ALIGN_SIZE(size), \
  38. sizeof(tcp_pcb)字节对齐
  39. (num), \
  40. 5
  41. memp_memory_ ## name ## _base, \
  42. memp_memory_TCP_PCB_base
  43. &memp_tab_ ## name \
  44. &memp_tab_TCP_PCB
  45. }
  46. //--------------------------最终内容如下----------------------------------
  47. 编译#define LWIP_MEMPOOL_DECLARE(TCP_PCB,5,sizeof(tcp_pcb),"TCP_PCB")时,最终的结果为:
  48. const struct memp_desc memp_TCP_PCB = { \
  49. "TCP_PCB"
  50. &memp_stats_TCP_PCB
  51. sizeof(tcp_pcb)字节对齐
  52. 5
  53. memp_memory_TCP_PCB_base
  54. &memp_tab_TCP_PCB
  55. }
  56. 而memp_pools[MEMP_MAX]中保存的就是编译出来的memp_TCP_PCB的地址,有多个地址,由memp_std.h中的宏定义决定;

        memp_init_pool(const struct memp_desc *desc)函数比较简单,就是根据memp_pools[ ]中的每种memp_desc结构体描述进行初始化,即在每种memp_desc类型中将空闲内存块连接成单链表,并且使用 memset()函数将其内容清零,这样子就初始化完成了。如下图所示:

动态内存池的内存分配

        用户通过memp_malloc 函数进行内存块申请,而内存块的大小就是指定的大小。其过程很简单,就是根据内存池的类型去选择从哪个内存池进行分配,因为不同类型的内存池中内存块大小是不一样的,比如 TCP_PCB 与 UDP_PCB 的大小就不一样,所以申请内存的时候传入的参数是内存池的类型而并非要申请的内存大小,系统中所有的内存池类型都会被记录在memp_pools 数组中,我们可以将该数组称之为内存池描述表。memp_malloc返回的memp是申请成功的内存地址。

  1. memp_malloc(memp_t type)
  2. {
  3. void *memp;
  4. LWIP_ERROR("memp_malloc: type < MEMP_MAX", (type < MEMP_MAX), return NULL;);
  5. memp = do_memp_malloc_pool(memp_pools[type]);
  6. return memp;
  7. }
  8. static void *do_memp_malloc_pool(const struct memp_desc *desc)
  9. {
  10. struct memp *memp;
  11. SYS_ARCH_DECL_PROTECT(old_level); //定义一个int类型的old_level
  12. SYS_ARCH_PROTECT(old_level);
  13. memp = *desc->tab;//取出对应内存块中的第一个空闲内存块
  14. if (memp != NULL) {
  15. *desc->tab = memp->next;//移动*desc->tab 指针,指向下一个空闲内存块
  16. LWIP_ASSERT("memp_malloc: memp properly aligned",((mem_ptr_t)memp % MEM_ALIGNMENT) == 0);
  17. desc->stats->used++;
  18. if (desc->stats->used > desc->stats->max) {
  19. desc->stats->max = desc->stats->used;
  20. }
  21. SYS_ARCH_UNPROTECT(old_level);
  22. return ((u8_t *)memp + MEMP_SIZE);//返回内存块地址
  23. } else {
  24. desc->stats->err++;
  25. SYS_ARCH_UNPROTECT(old_level);
  26. LWIP_DEBUGF(MEMP_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("memp_malloc: out of memory in pool %s\n", desc->desc));
  27. }
  28. return NULL;
  29. }

动态内存池的内存释放

        用户通过memp_free函数进行内存释放,只需要把使用完毕的内存块的起始地址 内存块类型 传入memp_free函数即可。

  1. memp_free(memp_t type, void *mem)
  2. {
  3. LWIP_ERROR("memp_free: type < MEMP_MAX", (type < MEMP_MAX), return;);
  4. if (mem == NULL) {
  5. return;
  6. }
  7. do_memp_free_pool(memp_pools[type], mem);
  8. }
  9. static void do_memp_free_pool(const struct memp_desc *desc, void *mem)
  10. {
  11. struct memp *memp;
  12. SYS_ARCH_DECL_PROTECT(old_level);
  13. LWIP_ASSERT("memp_free: mem properly aligned",((mem_ptr_t)mem % MEM_ALIGNMENT) == 0);
  14. /* cast through void* to get rid of alignment warnings */
  15. memp = (struct memp *)(void *)((u8_t *)mem - MEMP_SIZE);//根据内存块的地址偏移得到内存块的起始地址,因为前面也说了,内存块中有一部分内容是内存分配器操作的,所以需要进行偏移。
  16. SYS_ARCH_PROTECT(old_level);
  17. desc->stats->used--;
  18. memp->next = *desc->tab;//内存块的下一个就是链表中的第一个空闲内存块
  19. *desc->tab = memp;//将内存块插入到对应内存池的*desc->tab 中
  20. SYS_ARCH_UNPROTECT(old_level);
  21. }

1.2、动态内存堆策略

原理

        内存堆的的组织结构,它包括了内存数据结构与某些重要的全局变量,代码如下:

  1. struct mem {
  2. mem_size_t next;//下一个内存块的地址偏移量,基地址是整个内存堆的起始地址
  3. mem_size_t prev;//上一个内存块的地址偏移量,基地址是整个内存堆的起始地址
  4. u8_t used; //used 字段用于标记该内存是否已经被使用
  5. };
  6. #define MIN_SIZE 12
  7. #define MIN_SIZE_ALIGNED LWIP_MEM_ALIGN_SIZE(MIN_SIZE)
  8. #define SIZEOF_STRUCT_MEM LWIP_MEM_ALIGN_SIZE(sizeof(struct mem))
  9. #define MEM_SIZE_ALIGNED LWIP_MEM_ALIGN_SIZE(MEM_SIZE)
  10. LWIP_DECLARE_MEMORY_ALIGNED(ram_heap, MEM_SIZE_ALIGNED + (2U * SIZEOF_STRUCT_MEM));//内存堆的大小是由此宏定义,ram_heap[10240 + 2 * 8]
  11. #define LWIP_RAM_HEAP_POINTER ram_heap
  12. #endif /* LWIP_RAM_HEAP_POINTER *///:ram_heap[]就是内核的内存堆空间,LWIP_RAM_HEAP_POINTER这个宏定义相当于重新命名 ram_heap。
  13. static u8_t *ram;//ram 是一个全局指针变量,指向内存堆对齐后的起始地址
  14. static struct mem *ram_end;//mem 类型指针,指向内存堆中最后一个内存块
  15. static sys_mutex_t mem_mutex;//互斥量,用户保护内存堆的互斥量,暂时未用
  16. static struct mem * LWIP_MEM_LFREE_VOLATILE lfree;//mem 类型指针,指向内存堆中低地址的空闲内存块,简单来说就是空闲内存块链表指针。
  17. ....

动态内存堆代码分析

        在 LwIP 协议栈初始化(lwip_init(void))的时候, mem_init()会对内存堆进行初始化
        内存堆初始化的过程就是对所属的内存堆组织结构进行初始化,主要设置内存堆的起始地址,以及初始化空闲列表。根据用户配置的宏定义进行相关初始化,配置不同其实现也不同(可能为空),该函数源码如下:

  1. mem_init(void)
  2. {
  3. struct mem *mem;
  4. LWIP_ASSERT("Sanity check alignment",
  5. (SIZEOF_STRUCT_MEM & (MEM_ALIGNMENT - 1)) == 0);
  6. /* align the heap */
  7. ram = (u8_t *)LWIP_MEM_ALIGN(LWIP_RAM_HEAP_POINTER);//内存堆空间对齐,LWIP_RAM_HEAP_POINTER 宏定义就是ram_mem,内存堆对齐后的起始地址被记录在 ram 中
  8. /* initialize the start of the heap */
  9. mem = (struct mem *)(void *)ram;//在内存堆起始位置放置一个 mem 类型的结构体,因为初始化后的内存堆就是一个大的空闲内存块,每个空闲内存块的前面都需要放置一个 mem 结构体。
  10. mem->next = MEM_SIZE_ALIGNED;//下一个内存块的偏移量为 MEM_SIZE_ALIGNED,这相对于直接到内存堆的结束地址了。
  11. mem->prev = 0;//上一个内存块为空。
  12. mem->used = 0;//标记未被使用。
  13. /* initialize the end of the heap */
  14. ram_end = ptr_to_mem(MEM_SIZE_ALIGNED);//指针移动到内存堆末尾的位置,并且在那里放置一个 mem 类型的结构体,并初始化表示内存堆结束的内存块。
  15. ram_end->used = 1;//:标记已经使用了该内存块,因为结束的地方是没有内存块的,不能被分配出去,只能表示已经使用。
  16. ram_end->next = MEM_SIZE_ALIGNED;//此处仅表示已经到了内存堆的结束的地方,并无内存可以分配
  17. ram_end->prev = MEM_SIZE_ALIGNED;//此处仅表示已经到了内存堆的结束的地方,并无内存可以分配
  18. MEM_SANITY();
  19. /* initialize the lowest-free pointer to the start of the heap */
  20. lfree = (struct mem *)(void *)ram;//空闲内存块链表指针指向内存堆的起始地址,因为当前只有一个内存块。
  21. MEM_STATS_AVAIL(avail, MEM_SIZE_ALIGNED);//创建一个内存堆分配时候使用的互斥量,如果是无操作系统的情况,该语句等效于空。
  22. if (sys_mutex_new(&mem_mutex) != ERR_OK) {
  23. LWIP_ASSERT("failed to create mem_mutex", 0);
  24. }
  25. }

        经过 mem_init() 函数后,内存堆会被初始化为两个内存块,第一个内存块(mem)的大小就是整个内存堆的大小,而第二个内存块(ram_end)就是结束内存块,其大小为 0,并且被标记为已使用状态,无法进行分配。值得注意的是,系统在运行的时候,随着内存的分配与释放,lfree指针的指向地址不断改变,都指向内存堆中低地址空闲内存块,而 ram_end 则不会改变,它指向系统中最后一个内存块,也就是内存堆的结束地址。初始化完成的示意图见下图所示。 

 动态内存堆的内存分配

        内存分配函数根据用户指定申请大小的内存空间进行分配内存,最小为MIN_SIZE。LwIP 中使用内存分配算法是First Fit,其分配原理就是在空闲内存块链表中遍历寻找,直到找到第一个合适用户需求大小的内存块进行分配,如果该内存块能进行分割,则将用户需要大小的内存块分割出来,剩下的空闲内存块则重新插入空闲内存块链表中。
        mem_malloc()函数是 LwIP 中内存分配函数,其参数是用户指定大小的内存字节数,如果申请成功则返回内存块的地址,如果内存没有分配成功,则返回 NULL。分配的内存空间会受到内存对齐的影响,可能会比申请的内存略大,比如用户需要申请 22 个字节的内存,而 CPU 是按照 4 字节内存对齐的,那么分配的时候就会申请 24 个字节的内存块。 内存块在申请成功后返回的是内存块的起始地址,但是该内存并未进行初始化,可能
包含任意的随机数据,用户可以立即对其进行初始化或者写入有效数据以防止数据错误。
        此外内存堆是一个全局变量,在操作系统的环境中进行申请内存块是不安全的,所以 LwIP使用互斥量实现了对临界资源的保护,在多个线程同时申请或者释放的时候,会因为互斥量的保护而产生延迟。内存分配函数具体见代码如下:

  1. void *mem_malloc(mem_size_t size_in)
  2. {
  3. mem_size_t ptr, ptr2, size;
  4. struct mem *mem, *mem2;
  5. LWIP_MEM_ALLOC_DECL_PROTECT();
  6. if (size_in == 0) {
  7. return NULL;
  8. }
  9. size = (mem_size_t)LWIP_MEM_ALIGN_SIZE(size_in);//将用户申请的内存大小进行字节对齐操作。
  10. if (size < MIN_SIZE_ALIGNED) { //如果用户申请的内存大小小于最小的内存对齐大小MIN_SIZE_ALIGNED,则设为最小的默认值。
  11. /* every data block must be at least MIN_SIZE_ALIGNED long */
  12. size = MIN_SIZE_ALIGNED;
  13. }
  14. if ((size > MEM_SIZE_ALIGNED) || (size < size_in)) {//如果申请的内存大小大于整个内存堆对齐后的大小,则返回 NULL,申请内存失败。
  15. return NULL;
  16. }
  17. /* protect the heap from concurrent access */
  18. sys_mutex_lock(&mem_mutex);//获得互斥量,这一句代码在操作系统环境才起作用。
  19. LWIP_MEM_ALLOC_PROTECT();
  20. //遍历空闲内存块链表,直到找到第一个适合用户需求的内存块大小。
  21. for (ptr = mem_to_ptr(lfree); ptr < MEM_SIZE_ALIGNED - size; ptr = ptr_to_mem(ptr)->next) {
  22. mem = ptr_to_mem(ptr);//得到这个内存块起始地址
  23. if ((!mem->used) && (mem->next - (ptr + SIZEOF_STRUCT_MEM)) >= size) {//如果该内存块是未使用的,并且它的大小不小于用户需要的大小加上 mem 结构体的大小,那么就满足用户的需求
  24. if (mem->next - (ptr + SIZEOF_STRUCT_MEM) >= (size + SIZEOF_STRUCT_MEM + MIN_SIZE_ALIGNED)) {
  25. //既然满足用户需求,那么这个内存块可能很大,不能直接分配给用户,否则就是太浪费了,那就看看这个内存块能不能切开,
  26. //如果能就将一部分分配给用户即可,程序能执行到这里,说明内存块能进行分割,那就通过内存块的起始地址与用户需求大小进行偏移,
  27. //得到剩下的的内存起始块地址 ptr2。
  28. ptr2 = (mem_size_t)(ptr + SIZEOF_STRUCT_MEM + size);
  29. LWIP_ASSERT("invalid next ptr",ptr2 != MEM_SIZE_ALIGNED);
  30. /* create mem2 struct */
  31. mem2 = ptr_to_mem(ptr2);//将该地址后的内存空间作为分割之后新内存块 mem2,将起始地址转换为 mem 结构体用于记录内存块的信息。
  32. mem2->used = 0;//标记为未使用的内存块,并且将其插入空闲内存块链表中
  33. mem2->next = mem->next;
  34. mem2->prev = ptr;
  35. /* and insert it between mem and mem->next */
  36. mem->next = ptr2;
  37. mem->used = 1;//被分配出去的内存块 mem 标记为已使用状态。
  38. if (mem2->next != MEM_SIZE_ALIGNED) {//如果 mem2 内存块的下一个内存块不是链表中最后一个内存块(结束地址),那就将它下一个的内存块的 prve 指向 mem2。
  39. ptr_to_mem(mem2->next)->prev = ptr2;
  40. }
  41. MEM_STATS_INC_USED(used, (size + SIZEOF_STRUCT_MEM));
  42. } else {
  43. mem->used = 1;//如果不能分割,直接将分配的内存块标记为已使用即可。
  44. MEM_STATS_INC_USED(used, mem->next - mem_to_ptr(mem));
  45. }
  46. if (mem == lfree) {//如果被分配出去的内存块是 lfree 指向的内存块,那么就需要重新给 lfree 赋值。
  47. struct mem *cur = lfree;
  48. /* Find next free block after mem and update lowest free pointer */
  49. while (cur->used && cur != ram_end) {//找到第一个低地址的空闲内存块。
  50. cur = ptr_to_mem(cur->next);
  51. }
  52. lfree = cur;//将 lfree 指向该内存块。
  53. LWIP_ASSERT("mem_malloc: !lfree->used", ((lfree == ram_end) || (!lfree->used)));
  54. }
  55. LWIP_MEM_ALLOC_UNPROTECT();
  56. sys_mutex_unlock(&mem_mutex);//释放互斥量。
  57. LWIP_ASSERT("mem_malloc: allocated memory not above ram_end.",(mem_ptr_t)mem + SIZEOF_STRUCT_MEM + size <= (mem_ptr_t)ram_end);
  58. LWIP_ASSERT("mem_malloc: allocated memory properly aligned.",((mem_ptr_t)mem + SIZEOF_STRUCT_MEM) % MEM_ALIGNMENT == 0);
  59. LWIP_ASSERT("mem_malloc: sanity check alignment",(((mem_ptr_t)mem) & (MEM_ALIGNMENT - 1)) == 0);
  60. MEM_SANITY();
  61. return (u8_t *)mem + SIZEOF_STRUCT_MEM + MEM_SANITY_OFFSET;//返回内存块可用的起始地址,因为内存块的块头需要使用 mem结构体保存内存块的基本信息。
  62. }
  63. }
  64. MEM_STATS_INC(err);
  65. LWIP_MEM_ALLOC_UNPROTECT();
  66. sys_mutex_unlock(&mem_mutex);
  67. LWIP_DEBUGF(MEM_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("mem_malloc: could not allocate %"S16_F" bytes\n", (s16_t)size));
  68. return NULL;
  69. }

动态内存堆的内存释放

        内存释放的操作也是比较简单的,LwIP 是这样子做的:它根据用户释放的内存块地址,通过偏移 mem 结构体大小得到正确的内存块起始地址,并且根据 mem 中保存的内存块信息进行释放、合并等操作,并将 used 字段清零,表示该内存块未被使用。 LwIP 为了防止内存碎片的出现,通过算法将内存相邻的两个空闲内存块进行合并,在释放内存块的时候,如果内存块与上一个或者下一个空闲内存块在地址上是连续的,那么就将这两个内存块进行合并。具体参见 mem_free() 函数。

2、LwIP 中的配置

        LwIP 中,内存的选择是通过以下这几个宏值来决定的,根据用户对宏值的定义值来判断使用那种内存管理策略,具体如下: 

        MEM_LIBC_MALLOC:该宏定义是否使用 C 标准库自带的内存分配策略。该值默认情况下为 0,表示不使用 C 标准库自带的内存分配策略。

        MEMP_MEM_MALLOC:该宏定义表示是否使用 LwIP 内存堆分配策略实现内存池分配(即:要从内存池中获取内存时,实际是从内存堆中分配)。默认情况下为 0,表示不从内存堆中分配,内存池为独立一块内存实现。与MEM_USE_POOLS 只能选择其一。 

        MEM_USE_POOLS:该宏定义表示是否使用 LwIP 内存池分配策略实现内存堆的分配(即:要从内存堆中获取内存时,实际是从内存池中分配)。默认情况下为 0,表示不使用从内存池中分配,内存堆为独立一块内存实现。与MEMP_MEM_MALLOC 只能选择其一。 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/神奇cpp/article/detail/909356
推荐阅读
相关标签
  

闽ICP备14008679号