上一篇文章讨论了LWIP中动态内存池的概念。动态内存池只能分配固定大小的内存区域,这种情况主要用在分配IP、UDP、TCP等协议的头部区域,对于不是固定大小的内存区域,我们使用动态内存堆的方法,本文就来看LWIP中动态内存堆的实现。
动态内存堆分配策略的本质就是对一个实现定义好的内存块进行合理有效的组织和管理,其内存分配策略采用首次拟合方式,只要找到一个比用户请求空间大的空闲块,就从中切割出合适的块,并把剩余的部分返回到动态内存堆当中。
在这种策略下,用户申请的内存块大小具有最小限制,定义为MIN_SIZE,这个值通常被定义为12个字节,如果申请的空间小于这个值,则自动分配MIN_SIZE大小的空间。这个值用户也是可以自定义的,减小这个值可以节省内存空间,但另一方面会导致大的内存块被不断细分为小内存块。
释放则是一个相反的过程,回收的时候会查看一下该节点相邻的内存块是否是空闲的,如果是的话就把俩合并一个大的内存块。
对于动态内存的使用,推荐:分配---释放---分配---释放。有效减少内存碎片。
同上一章一样,我们来看看内存分配堆中会用到的一些全局性数据。
结构体mem形如:
struct mem { /** 下一个内存块*/ mem_size_t next; /**上一个内存块 */ mem_size_t prev; /** 当前内存块是否已经使用 */ u8_t used; };next和prev并不是指针,而是一个偏移量,是基于整个内存堆起始地址的偏移量。
下面再来看看内存堆的组织结构
这里注意以下几点:
1、每个内存块的大小是不同而且可能随时发生变化的
2、系统初始化的时候,整个内存堆可以看成一个唯一的内存块
内存堆的空间是通过定义数组ram_heap来实现的
u8_t ram_heap[MEM_SIZE_ALIGNED + (2*SIZEOF_STRUCT_MEM) + MEM_ALIGNMENT];
MEM_SIZE_ALIGNED:对齐后的内存堆大小
SIZEOF_STRUCT_MEM:一个struct mem大小,
MEM_ALIGNMENT:附加大小
这里多加2个struct mem大小,我们在后面详细解释
下面我们来看看具体函数的实现
1、初始化
初始化后的内存堆的结构如下
系统初始化后,整个内存堆初始化为两个内存块,第一块包含了所有的可用内存空间。used被标记为未使用。next指向了第二块内存块。prev指向的是自己。同时,lfree指向这个内存块,要分配的时候就会从这个内存块开始分配。
第二块则不包含任何可用的内存空间,它的作用是指示内存块的末尾,所以ram_end指针指向了它,并且它的next和prev都指向了自己。
函数的实现
/** * Zero the heap and initialize start, end and lowest-free */ void mem_init(void) { struct mem *mem; /* align the heap */ ram = (u8_t *)LWIP_MEM_ALIGN(LWIP_RAM_HEAP_POINTER); //对齐一下 /* initialize the start of the heap */ //初始化第一个内存块 mem = (struct mem *)(void *)ram; //ram的起始地方放一个mem结构体 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; //next指向自己 ram_end->prev = MEM_SIZE_ALIGNED; //prev也指向自己 /* initialize the lowest-free pointer to the start of the heap */ lfree = (struct mem *)(void *)ram; //lfree指向第一个内存块,也就是内存块的起始 MEM_STATS_AVAIL(avail, MEM_SIZE_ALIGNED); if(sys_mutex_new(&mem_mutex) != ERR_OK) { LWIP_ASSERT("failed to create mem_mutex", 0); } }
2、内存堆分配
内存堆分配函数根据用户所申请的空间大小来搜索所有未被使用的内存块,检索到的最先满足条件的内存块被分配给用户。当分配成功后,内存分配函数会马上在已分配的数据区域后面形成一个mem结构体。成功分配的内存区域是没有初始化的,这块内存可能包含任何随机数据。用户可以马上用有效数据或0来初始化它。
在分配内存时,假如剩余的空间还能分配一个最小的内存块,我们还需要对当前内存块进行切割。
/** * Adam's mem_malloc() plus solution for bug #17922 * 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; LWIP_MEM_ALLOC_DECL_PROTECT(); if (size == 0) { //申请长度为0,返回NULL return NULL; } /* Expand the size of the allocated memory region so that we can adjust for alignment. */ size = LWIP_MEM_ALIGN_SIZE(size); //将size修正为对齐字节数目的整数倍 if(size < MIN_SIZE_ALIGNED) { //申请大小至少为MIN_SIZE_ALIGNED /* every data block must be at least MIN_SIZE_ALIGNED long */ size = MIN_SIZE_ALIGNED; } if (size > MEM_SIZE_ALIGNED) { //size比最大值还大,返回NULL return NULL; } /* protect the heap from concurrent access */ sys_mutex_lock(&mem_mutex); LWIP_MEM_ALLOC_PROTECT(); /* 从lfree开始遍历,找到第一个长度大于size的内存块 */ for (ptr = (mem_size_t)((u8_t *)lfree - ram); ptr < MEM_SIZE_ALIGNED - size; ptr = ((struct mem *)(void *)&ram[ptr])->next) { mem = (struct mem *)(void *)&ram[ptr]; //获得一个内存块的起始地址 LWIP_MEM_ALLOC_UNPROTECT(); /* allow mem_free or mem_trim to run */ LWIP_MEM_ALLOC_PROTECT(); /*该内存块未被使用,空间也大于size——sizeof(strct mem)*/ if ((!mem->used) && (mem->next - (ptr + SIZEOF_STRUCT_MEM)) >= size) { //能到这里就说明是有可用空间可以分配的,但如果除了给size空间外,剩余的空间比较大(还能再分配最小的内存空间),我们就需要对这个内存块进行截取。 if (mem->next - (ptr + SIZEOF_STRUCT_MEM) >= (size + SIZEOF_STRUCT_MEM + MIN_SIZE_ALIGNED)) { //到这里需要截取 ptr2 = ptr + SIZEOF_STRUCT_MEM + size; //从ptr2开始截取,ptr2之后的要成为新的内存块,ptr到ptr2之间的空间是要分配的空间 /* create mem2 struct */ mem2 = (struct mem *)(void *)&ram[ptr2]; //对新的内存块初始化 mem2->used = 0; mem2->next = mem->next; mem2->prev = ptr; /* 把新的内存空闲块放到链表中 */ 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 { //这个else对应的是判断是否需要截取的if,这里表示不用截取,直接分配就可以了 mem->used = 1; MEM_STATS_INC_USED(used, mem->next - (mem_size_t)((u8_t *)mem - ram)); } //分配完成了,把lfree指向的地址调整一下 if (mem == lfree) { //只有在lfree所指的地址已经被分配出去了,才会调整lfree struct mem *cur = lfree; /*找到下一个空闲的内存块 */ while (cur->used && cur != ram_end) { cur = (struct mem *)(void *)&ram[cur->next]; } lfree = cur; LWIP_ASSERT("mem_malloc: !lfree->used", ((lfree == ram_end) || (!lfree->used))); } sys_mutex_unlock(&mem_mutex); return (u8_t *)mem + SIZEOF_STRUCT_MEM; } } sys_mutex_unlock(&mem_mutex); return NULL; }
3、内存释放
在内存释放的时候,它根据用户提供的释放地址寻找到系统结构mem,然后利用这个结构体来实现内存的释放、合并等操作,内存块被回收之后,used被置为0.同时,为了降低内存碎片的产生,会检查上一个和下一个内存块的使用标志,如果他们中的任何一个没有被使用,那就将当前内存块与他们进行合并操作。
/** * Put a struct mem back on the heap * * @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 == NULL) { LWIP_DEBUGF(MEM_DEBUG | LWIP_DBG_TRACE | LWIP_DBG_LEVEL_SERIOUS, ("mem_free(p == NULL) was called.\n")); return; } //判断地址是否合法 if ((u8_t *)rmem < (u8_t *)ram || (u8_t *)rmem >= (u8_t *)ram_end) { SYS_ARCH_DECL_PROTECT(lev); /* 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(); /* 合法,获取系统结构体mem */ mem = (struct mem *)(void *)((u8_t *)rmem - SIZEOF_STRUCT_MEM); /* 使用标志置0 */ mem->used = 0; /*若要释放的内存比lfree低,更新一下lfree*/ if (mem < lfree) { /* the newly freed struct is now the lowest */ lfree = mem; } MEM_STATS_DEC_USED(used, mem->next - (mem_size_t)(((u8_t *)mem - ram))); /* 执行合并操作*/ plug_holes(mem); #if LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT mem_free_count = 1; #endif /* LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT */ LWIP_MEM_FREE_UNPROTECT(); }
plug_holes是执行内存块合并的函数,所做工作是检查要释放内存块的上一个和下一个内存块是否空闲,若空闲则合并
/** * "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. * * @param mem this points to a struct mem which just has been freed * @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; //上一个内存块 nmem = (struct mem *)(void *)&ram[mem->next]; if (mem != nmem && nmem->used == 0 && (u8_t *)nmem != (u8_t *)ram_end) { /* 下一个内存块空闲且不是最后一个内存块,执行合并 */ if (lfree == nmem) { lfree = mem; } mem->next = nmem->next;//执行合并,也就是删除了nmem节点 ((struct mem *)(void *)&ram[nmem->next])->prev = (mem_size_t)((u8_t *)mem - ram); } /* 上一个内存块*/ pmem = (struct mem *)(void *)&ram[mem->prev]; if (pmem != mem && pmem->used == 0) { /* if mem->prev is unused, combine mem and mem->prev */ if (lfree == mem) { lfree = pmem; } pmem->next = mem->next; //删除上一个节点 ((struct mem *)(void *)&ram[mem->next])->prev = (mem_size_t)((u8_t *)pmem - ram); } }