热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

LwIP系列内存管理(堆内存)详解

一、目的小型嵌入式系统中的内存资源(SRAM)一般都比较有限,LwIP的运行平台一般都是资源受限的MCU。为了能够更加高效的运行ÿ


一、目的

小型嵌入式系统中的内存资源(SRAM)一般都比较有限,LwIP的运行平台一般都是资源受限的MCU。为了能够更加高效的运行,LwIP设计了基于内存池、内存堆的内存管理以及在处理数据包时的pbuf数据结构。


本篇的主要目的是介绍基于内存堆的内存管理原理。


内存堆内存管理的特点:


  • 按需分配,需要多少内存就分配多少内存(存在最小分配内存限制)


  • 内存易碎片化


  • 内存回收时一般会进行头部和尾部拼接,尽量减少内存碎片的产生


内存堆本质上是一大块连续内存(可以理解为数组),当需要内存时从这个数组中按照特定算法切分一块所需大小的内存块,将这块内存的地址提供出去;剩余内存仍然存放在内存堆中。


二、介绍

在正式介绍之前,我们需要理解几个宏定义和内存堆相关的数据结构


内存块管理结构


/**
* The heap is made up as a list of structs of this type.
* This does not have to be aligned since for getting its size,
* we only use the macro SIZEOF_STRUCT_MEM, which automatically aligns.
*/
struct mem {
/** index (-> ram[next]) of the next struct */
mem_size_t next;
/** index (-> ram[prev]) of the previous struct */
mem_size_t prev;
/** 1: this area is used; 0: this area is unused */
u8_t used;
};

其中mem_size_t的定义如下


#if MEM_SIZE > 64000L
typedef u32_t mem_size_t;
#define MEM_SIZE_F U32_F
#else
typedef u16_t mem_size_t;
#define MEM_SIZE_F U16_F
#endif /* MEM_SIZE > 64000 */
#endif

根据业务需要如果MEM_SIZE是否大于64000字节,决定mem_size_t是u32_t还是u16_t。


struct mem内存块各字段描述如下:


next:下一个内存块的位置(注意此处是数组下标,并不是地址)


prev:上一个内存块的位置


used:表明此内存块是否已经使用(分配)




为了帮助大家更好的理解,大家可以按照下图所示从整体上理解内存堆中的内存块









内存堆示意图




上图中struct mem A和X都是内存块管理结构,紧跟着struct mem A的是A实际管理的内存区域,并且A当前管理的内存块是未使用的。


struct mem X是最后一个内存管理结构(作为tailer标志,作为内存堆中的最后一个内存管理块),其next/prev都是指向自身位置,并且是标记为已经使用的。




内存堆管理结构


为了更好的管理内存堆,在LwIP中还额外定义了几个全局方便管理内存堆,其定义如下


/** pointer to the heap (ram_heap): for alignment, ram is now a pointer instead of an array */
static u8_t *ram;
/** the last entry, always unused! */
static struct mem *ram_end;
/** pointer to the lowest free block, this is used for faster search */
static struct mem *lfree;
/** concurrent access protection */
#if !NO_SYS
static sys_mutex_t mem_mutex;
#endif

ram指向整个对齐后的内存堆首地址(开发者提供的内存空间的首地址未必是4字节对齐,所以需要对齐)




ram_end指向内存堆的末尾地址(结合上图就是指向struct mem X的位置,因为struct mem X是作为一个占位符,本身不管理实际内存数据)




lfree指向内存堆中可用内存块的第一个









上图中黄色的块代表已经使用的内存,白色的块代表未使用的数据块;可以看到白色的快和黄色的块是会出现随机交叉的情况的(经过多次内存分配后会出现内存空间碎片化的情况),此处的lfree就是指向上图的中第一个白色的数据块




mem_mutex是在rtos中通过互斥锁保护内存操作的




宏定义说明


/** All allocated blocks will be MIN_SIZE bytes big, at least!
* MIN_SIZE can be overridden to suit your needs. Smaller values save space,
* larger values could prevent too small blocks to fragment the RAM too much. */
#ifndef MIN_SIZE
#define MIN_SIZE 12
#endif /* MIN_SIZE */
/* some alignment macros: we define them here for better source code layout */
#define MIN_SIZE_ALIGNED LWIP_MEM_ALIGN_SIZE(MIN_SIZE)
#define SIZEOF_STRUCT_MEM LWIP_MEM_ALIGN_SIZE(sizeof(struct mem))
#define MEM_SIZE_ALIGNED LWIP_MEM_ALIGN_SIZE(MEM_SIZE)
/** If you want to relocate the heap to external memory, simply define
* LWIP_RAM_HEAP_POINTER as a void-pointer to that location.
* If so, make sure the memory at that location is big enough (see below on
* how that space is calculated). */
#ifndef LWIP_RAM_HEAP_POINTER
/** the heap. we need one struct mem at the end and some room for alignment */
LWIP_DECLARE_MEMORY_ALIGNED(ram_heap, MEM_SIZE_ALIGNED + (2U*SIZEOF_STRUCT_MEM));
#define LWIP_RAM_HEAP_POINTER ram_heap
#endif /* LWIP_RAM_HEAP_POINTER */

MIN_SIZE:表示内存块最小管理的内存空间不能小于此值


MIN_SIZE_ALIGNED:表示MIN_SIZE四字节对齐的值


SIZEOF_STRUCT_MEM:表示结构体struct mem大小对齐后的值


MEM_SIZE:定义了内存堆最大可以分配的大小


MEM_SIZE_ALIGNED:表示MEM_SIZE四字节对齐后的值


LWIP_RAM_HEAP_POINTER:用户可以定义内存堆的首地址;如果未定义,是使用LwIP定义的ram_heap数组作为内存堆,数组大小为MEM_SIZE_ALIGNED + (2U*SIZEOF_STRUCT_MEM);


这边的这个数组大小是有讲究的,再次看下内存堆示意图,struct mem A和struct mem X就是这边的2U*SIZEOF_STRUCT_MEM,struct mem A管理的内存就是这边的MEM_SIZE_ALIGNED大小。


内存堆的初始化


/**
* Zero the heap and initialize start, end and lowest-free
*/
void
mem_init(void)
{
struct mem *mem;
LWIP_ASSERT("Sanity check alignment",
(SIZEOF_STRUCT_MEM & (MEM_ALIGNMENT-1)) == 0);     //①
/* align the heap */
ram = (u8_t *)LWIP_MEM_ALIGN(LWIP_RAM_HEAP_POINTER);    //②
/* initialize the start of the heap */
mem = (struct mem *)(void *)ram;
mem->next = MEM_SIZE_ALIGNED;
mem->prev = 0;
mem->used = 0;                                        //③
/* initialize the end of the heap */
ram_end = (struct mem *)(void *)&ram[MEM_SIZE_ALIGNED];
ram_end->used = 1;
ram_end->next = MEM_SIZE_ALIGNED;
ram_end->prev = MEM_SIZE_ALIGNED;                        //④
/* initialize the lowest-free pointer to the start of the heap */
lfree = (struct mem *)(void *)ram;                    //⑤
MEM_STATS_AVAIL(avail, MEM_SIZE_ALIGNED);                //⑥
if (sys_mutex_new(&mem_mutex) != ERR_OK) {                //⑦
LWIP_ASSERT("failed to create mem_mutex", 0);
}
}

mem_init负责内存堆的初始化。


①对SIZEOF_STRUCT_MEM的值检查是否4字节对齐


②将全局ram指针设置为内存堆的内存首地址,注意需要对LWIP_RAM_HEAP_POINTER地址对齐;


③将第一个内存块A进行各个字段初始化,其next的值设置为MEM_SIZE_ALIGNED(就是设置为struct mem X在数组中的位置,prev设置为0,说明struct mem A是第一个内存管理块,并且used设置为0,说明是未使用的内存块区域;),也就是说struct mem A管理的内存空间有MEM_SIZE_ALIGNED字节。


④将ram_end指向数组ram[MEM_SIZE_ALIGNED]的地址,也就是struct mem X的地址,并且其prev/next的值是自身在数组中的相对位置,表明其是内存堆中的最后一个位置(used设置为1)


⑤lfee指向第一个内存块的位置,也就是struct mem A


⑥LwIP中对各个模块都有特定的debug字段,此处avail字段记录的内存堆中的可用内存


⑦初始化互斥锁


最后初始化后的内存堆如下图所示









初始化后的内存堆




注意观察上图中各个字段的连线关系,也就是说初始状态下,只有一个空闲内存块,其管理的内存大小为MEM_SIZE_ALIGNED。




内存分配函数


/**
* Allocate a block of memory with a minimum of 'size' bytes.
*
* @param size is the minimum size of the requested block in bytes.
* @return pointer to allocated memory or NULL if no free memory was found.
*
* Note that the returned value will always be aligned (as defined by MEM_ALIGNMENT).
*/
void *
mem_malloc(mem_size_t size)
{
mem_size_t ptr, ptr2;
struct mem *mem, *mem2;
#if LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT
u8_t local_mem_free_count = 0;
#endif /* LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT */
LWIP_MEM_ALLOC_DECL_PROTECT();
if (size == 0) {
return NULL;
}
/* Expand the size of the allocated memory region so that we can
adjust for alignment. */
size = LWIP_MEM_ALIGN_SIZE(size);
if (size /* every data block must be at least MIN_SIZE_ALIGNED long */
size = MIN_SIZE_ALIGNED;
}                                                    //①
if (size > MEM_SIZE_ALIGNED) {                           //②
return NULL;
}
/* protect the heap from concurrent access */
sys_mutex_lock(&mem_mutex);                                //③
LWIP_MEM_ALLOC_PROTECT();
#if LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT
/* run as long as a mem_free disturbed mem_malloc or mem_trim */
do {
local_mem_free_count = 0;
#endif /* LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT */
/* Scan through the heap searching for a free block that is big enough,
* beginning with the lowest free block.
*/
for (ptr = (mem_size_t)((u8_t *)lfree - ram); ptr ptr = ((struct mem *)(void *)&ram[ptr])->next) {    //④
mem = (struct mem *)(void *)&ram[ptr];
#if LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT
mem_free_count = 0;
LWIP_MEM_ALLOC_UNPROTECT();
/* allow mem_free or mem_trim to run */
LWIP_MEM_ALLOC_PROTECT();
if (mem_free_count != 0) {
/* If mem_free or mem_trim have run, we have to restart since they
could have altered our current struct mem. */
local_mem_free_count = 1;
break;
}
#endif /* LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT */
if ((!mem->used) &&
(mem->next - (ptr + SIZEOF_STRUCT_MEM)) >= size) {    //⑤
/* mem is not used and at least perfect fit is possible:
* mem->next - (ptr + SIZEOF_STRUCT_MEM) gives us the 'user data size' of mem */
if (mem->next - (ptr + SIZEOF_STRUCT_MEM) >= (size + SIZEOF_STRUCT_MEM + MIN_SIZE_ALIGNED)) {    //⑥
/* (in addition to the above, we test if another struct mem (SIZEOF_STRUCT_MEM) containing
* at least MIN_SIZE_ALIGNED of data also fits in the 'user data space' of 'mem')
* -> split large block, create empty remainder,
* remainder must be large enough to contain MIN_SIZE_ALIGNED data: if
* mem->next - (ptr + (2*SIZEOF_STRUCT_MEM)) == size,
* struct mem would fit in but no data between mem2 and mem2->next
* @todo we could leave out MIN_SIZE_ALIGNED. We would create an empty
* region that couldn't hold data, but when mem->next gets freed,
* the 2 regions would be combined, resulting in more free memory
*/
ptr2 = ptr + SIZEOF_STRUCT_MEM + size;            //⑦
/* create mem2 struct */
mem2 = (struct mem *)(void *)&ram[ptr2];
mem2->used = 0;
mem2->next = mem->next;
mem2->prev = ptr;
/* and insert it between mem and mem->next */
mem->next = ptr2;
mem->used = 1;
if (mem2->next != MEM_SIZE_ALIGNED) {
((struct mem *)(void *)&ram[mem2->next])->prev = ptr2;
}
MEM_STATS_INC_USED(used, (size + SIZEOF_STRUCT_MEM));
} else {
/* (a mem2 struct does no fit into the user data space of mem and mem->next will always
* be used at this point: if not we have 2 unused structs in a row, plug_holes should have
* take care of this).
* -> near fit or exact fit: do not split, no mem2 creation
* also can't move mem->next directly behind mem, since mem->next
* will always be used at this point!
*/
mem->used = 1;
MEM_STATS_INC_USED(used, mem->next - (mem_size_t)((u8_t *)mem - ram));
}
#if LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT
mem_malloc_adjust_lfree:
#endif /* LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT */
if (mem == lfree) {        //⑦
struct mem *cur = lfree;
/* Find next free block after mem and update lowest free pointer */
while (cur->used && cur != ram_end) {
#if LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT
mem_free_count = 0;
LWIP_MEM_ALLOC_UNPROTECT();
/* prevent high interrupt latency... */
LWIP_MEM_ALLOC_PROTECT();
if (mem_free_count != 0) {
/* If mem_free or mem_trim have run, we have to restart since they
could have altered our current struct mem or lfree. */
goto mem_malloc_adjust_lfree;
}
#endif /* LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT */
cur = (struct mem *)(void *)&ram[cur->next];
}
lfree = cur;
LWIP_ASSERT("mem_malloc: !lfree->used", ((lfree == ram_end) || (!lfree->used)));
}
LWIP_MEM_ALLOC_UNPROTECT();
sys_mutex_unlock(&mem_mutex);
LWIP_ASSERT("mem_malloc: allocated memory not above ram_end.",
(mem_ptr_t)mem &#43; SIZEOF_STRUCT_MEM &#43; size <&#61; (mem_ptr_t)ram_end);
LWIP_ASSERT("mem_malloc: allocated memory properly aligned.",
((mem_ptr_t)mem &#43; SIZEOF_STRUCT_MEM) % MEM_ALIGNMENT &#61;&#61; 0);
LWIP_ASSERT("mem_malloc: sanity check alignment",
(((mem_ptr_t)mem) & (MEM_ALIGNMENT-1)) &#61;&#61; 0);
return (u8_t *)mem &#43; SIZEOF_STRUCT_MEM;        //⑧
}
}
#if LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT
/* if we got interrupted by a mem_free, try again */
} while (local_mem_free_count !&#61; 0);
#endif /* LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT */
LWIP_DEBUGF(MEM_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("mem_malloc: could not allocate %"S16_F" bytes\n", (s16_t)size));
MEM_STATS_INC(err);
LWIP_MEM_ALLOC_UNPROTECT();
sys_mutex_unlock(&mem_mutex);
return NULL;
}

①首先对输入的size参数进行对齐并且对其大小进行限定&#xff0c;如果小于MIN_SIZE_ALIGNED&#xff0c;则设置为size为MIN_SIZE_ALIGNED&#xff08;避免过多的小内存块&#xff09;


②如果需要分配的内存大于MEM_SIZE_ALIGNED&#xff08;最大可分配的内存&#xff09;&#xff0c;则直接返回NULL


③内存分配之前先上锁&#xff0c;防止多线程访问问题


④从lfree位置开始搜索未使用的内存块


for (ptr &#61; (mem_size_t)((u8_t *)lfree - ram); ptr ptr &#61; ((struct mem *)(void *)&ram[ptr])->next) {
}

注意这边的ptr是索引号&#xff08;即数组下标&#xff09;&#xff0c;lfree是指针&#xff0c;ram是内存堆的首地址&#xff0c;lfree - ram获取的是内存堆的空闲块在数组中的索引值&#xff1b;


⑤查找空闲的内存块并且其大小要满足


if ((!mem->used) &&
(mem->next - (ptr &#43; SIZEOF_STRUCT_MEM)) >&#61; size) {

因为将一个内存块切割一个新的内存块出来&#xff0c;除了需要的内存size外&#xff0c;还要一个struct mem结构体用于管理剩余的块&#xff0c;所以这边判断条件中需要加入SIZEOF_STRUCT_MEM


⑥如果找到的空闲块足够大&#xff0c;分配需要的内存后还比最小内存块&#xff08;SIZEOF_STRUCT_MEM &#43; MIN_SIZE_ALIGNED&#xff09;还要大&#xff0c;就需要进行分割&#xff1b;否则直接将找到的空闲块设置为使用的&#xff0c;并将这边内存空间返回


if (mem->next - (ptr &#43; SIZEOF_STRUCT_MEM) >&#61; (size &#43; SIZEOF_STRUCT_MEM &#43; MIN_SIZE_ALIGNED)) {
}

⑦ptr2为切割后剩余内存块的数组索引号


mem2 &#61; (struct mem *)(void *)&ram[ptr2];

通过下标ptr2获取内存地址&#xff0c;mem2就是新分割后的内存块的首地址


mem2->used &#61; 0;
mem2->next &#61; mem->next;
mem2->prev &#61; ptr;
/* and insert it between mem and mem->next */
mem->next &#61; ptr2;
mem->used &#61; 1;

将mem2的next的值设置为mem->next&#xff1b;mem2->prev设置为ptr&#xff0c;mem->next设置为ptr2&#xff0c;并标记为使用的。


⑦更新lfree指针


⑧返回分配的内存指针&#xff08;注意返回的地址需要便宜内存管理块的结构体大小(u8_t *)mem &#43; SIZEOF_STRUCT_MEM;&#xff09;


为了帮助大家理解这个分割过程&#xff0c;大家可以根据下图进行理解









上图中struct mem A是已经分配的内存&#xff0c;struct mem B是新切割出来的内存块&#xff0c;注意一一对应其prev/next的赋值



内存释放函数


/**
* Put a struct mem back on the heap
*
* &#64;param rmem is the data portion of a struct mem as returned by a previous
* call to mem_malloc()
*/
void
mem_free(void *rmem)
{
struct mem *mem;
LWIP_MEM_FREE_DECL_PROTECT();
if (rmem &#61;&#61; NULL) {                //①
LWIP_DEBUGF(MEM_DEBUG | LWIP_DBG_TRACE | LWIP_DBG_LEVEL_SERIOUS, ("mem_free(p &#61;&#61; NULL) was called.\n"));
return;
}
LWIP_ASSERT("mem_free: sanity check alignment", (((mem_ptr_t)rmem) & (MEM_ALIGNMENT-1)) &#61;&#61; 0);
LWIP_ASSERT("mem_free: legal memory", (u8_t *)rmem >&#61; (u8_t *)ram &&
(u8_t *)rmem <(u8_t *)ram_end);
if ((u8_t *)rmem <(u8_t *)ram || (u8_t *)rmem >&#61; (u8_t *)ram_end) { //②
SYS_ARCH_DECL_PROTECT(lev);
LWIP_DEBUGF(MEM_DEBUG | LWIP_DBG_LEVEL_SEVERE, ("mem_free: illegal memory\n"));
/* protect mem stats from concurrent access */
SYS_ARCH_PROTECT(lev);
MEM_STATS_INC(illegal);
SYS_ARCH_UNPROTECT(lev);
return;
}
/* protect the heap from concurrent access */
LWIP_MEM_FREE_PROTECT();
/* Get the corresponding struct mem ... */
/* cast through void* to get rid of alignment warnings */
mem &#61; (struct mem *)(void *)((u8_t *)rmem - SIZEOF_STRUCT_MEM);   //③
/* ... which has to be in a used state ... */
LWIP_ASSERT("mem_free: mem->used", mem->used);
/* ... and is now unused. */
mem->used &#61; 0;
if (mem /* the newly freed struct is now the lowest */
lfree &#61; mem;
}
MEM_STATS_DEC_USED(used, mem->next - (mem_size_t)(((u8_t *)mem - ram)));
/* finally, see if prev or next are free also */
plug_holes(mem);                                            //⑤
#if LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT
mem_free_count &#61; 1;
#endif /* LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT */
LWIP_MEM_FREE_UNPROTECT();
}

①②检查rmem指针的合法性


③根据rmem指针找到管理此内存块的struct mem的地址&#xff0c;并标记此内存块为未使用的


④如果当前未使用的内存块的地址比空闲块的指针还小&#xff0c;则将lfree指向这个新释放的内存块


⑤拼接当前内存块前后的空闲内存块&#xff08;动态内存堆中容易出现内存碎片&#xff0c;故在释放时需要检查当前块前一个和后一个内存块是否是空闲块&#xff0c;如果是则进行拼接&#xff09;


/**
* "Plug holes" by combining adjacent empty struct mems.
* After this function is through, there should not exist
* one empty struct mem pointing to another empty struct mem.
*
* &#64;param mem this points to a struct mem which just has been freed
* &#64;internal this function is only called by mem_free() and mem_trim()
*
* This assumes access to the heap is protected by the calling function
* already.
*/
static void
plug_holes(struct mem *mem)
{
struct mem *nmem;
struct mem *pmem;
LWIP_ASSERT("plug_holes: mem >&#61; ram", (u8_t *)mem >&#61; ram);        //①
LWIP_ASSERT("plug_holes: mem LWIP_ASSERT("plug_holes: mem->used &#61;&#61; 0", mem->used &#61;&#61; 0);
/* plug hole forward */
LWIP_ASSERT("plug_holes: mem->next <&#61; MEM_SIZE_ALIGNED", mem->next <&#61; MEM_SIZE_ALIGNED);
nmem &#61; (struct mem *)(void *)&ram[mem->next];                        //②-1
if (mem !&#61; nmem && nmem->used &#61;&#61; 0 && (u8_t *)nmem !&#61; (u8_t *)ram_end) {
/* if mem->next is unused and not end of ram, combine mem and mem->next */
if (lfree &#61;&#61; nmem) {                                            //②-2
lfree &#61; mem;
}
mem->next &#61; nmem->next;
((struct mem *)(void *)&ram[nmem->next])->prev &#61; (mem_size_t)((u8_t *)mem - ram);
}
/* plug hole backward */
pmem &#61; (struct mem *)(void *)&ram[mem->prev];            //③-1
if (pmem !&#61; mem && pmem->used &#61;&#61; 0) {                    //③-2
/* if mem->prev is unused, combine mem and mem->prev */
if (lfree &#61;&#61; mem) {            //③-3
lfree &#61; pmem;
}
pmem->next &#61; mem->next;                        //③-4
((struct mem *)(void *)&ram[mem->next])->prev &#61; (mem_size_t)((u8_t *)pmem - ram);
}
}

①检查mem地址的合法性


②-1检查当前块的后一个块是否是空闲块


②-2如果当前块的后一个块是空闲块则将当前的next设置为下一个块的next&#xff0c;下一个块的prev设置为当前块的索引值&#xff1b;如果lfree指向当前块的下一个块&#xff0c;则lfree需要指向当前块


③-1检查当前块的上一个块


③-2如果当前块的上一个块未使用&#xff0c;则进行拼接


③-3如果lfree指向当前块&#xff0c;则lfree需要指向当前块的上一个块


③-4设置上一个块的next的值为当前块的next&#xff0c;当前块的下一个块的prev设置为当前块的上一个块的下标值




至此&#xff0c;我们基本上对LwIP的内存堆实现算法进行了分析&#xff0c;总体上看还是比较简单的。


其每个内存统一管理&#xff0c;在寻找一个新的空闲块时&#xff0c;需要编译整个内存堆中的所有块&#xff0c;但为了尽量加快搜索速度&#xff0c;专门有个lfree指针定位当前空闲块中地址最低的一个块。





推荐阅读
author-avatar
糖糖糖开水
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有