动态内存堆实现-移植修改LwIP
在嵌入式开发中,内存管理以及使用是至关重要的,内存使用的多少、内存泄漏等时刻需要注意。合理的内存管理策略将从根本上决定内存分配和回收效率,最终决定系统的整体性能。LwIP 为了能够灵活的使用内存,为使用者提供两种简单却又高效的动态内存管理策略:动态内存堆管理(heap)、动态内存池管理(pool),而内存池管理策略在前面的章节已经讲解,那么现在就来看看内存堆的管理。其中,动态内存堆管理(heap)又可以分为两种:一种是 C 标准库自带的内存管理策略,另一种是 LwIP 自身实现的内存堆管理策略。这里介绍使用 LwIP自身实现的内存堆。
动态内存堆分配
动态内存堆分配策略的本质就是对一个事先定义好的内存块进行合理有效的组织和管理,其内存分配的策略采用首次拟合( First Fit)方式,只要找到一个比用户请求空间大的空闲块,就从中切割出合适的块,并把剩余的部分返回到动态内存堆中(如果大小满足要求)
数据结构概述
在这种策略下,用户申请的内存块大小具有最小限制,即请求的大小不能小于 MIN_SIZE则系统自动将请求大小设置为 MIN_SIZE。通常MIN_SIZE被定义为12字节,用户也可以定义为其他值,减小该值会在内存分配时达到节省内存空间的目的,但是会导致大的内存块被不断细分为小内存块。内存释放的过程是相反的过程,内存回收函数会査看该节点前后相邻的内存块是否空闲,如果空闲则合并成一个大的内存空闲块。采用这种分配策略,其优点就是内存浪费小,比较简单适合用于小内存的管理,其缺点就是如果频繁的动态分配和释放,可能会造成严重的内存碎片,如果在碎片情况严重的话,可能会导致内存分配不成功。对于动态内存的使用,比较推荐的使用方法为:分配一释放一分配一释放,这种方法能够有效减少内存碎片。
| 名称 | 类型 | 所在文件 | 描述 |
|---|---|---|---|
| rz_ram_heap | 全局型数组 | rz_mem_heap.c | 系统内存堆空间 |
| ram_start | 全局型指针 | rz_mem_heap.c | 指向内存堆空间对齐后的起始地址 |
| mem | 结构体 | rz_mem_heap.c | 内核附加在各个内存块前面的结构体 |
| ram_end | mem型指针 | rz_mem_heap.c | 指向系统最后一个内存块 |
| lfree | mem型指针 | rz_mem_heap.c | 指向当前系统具有最低地址的空闲内存块 |
| mem_sem | 信号量 | rz_mem_heap.c | 用于保护内存堆的互斥信号量,暂未用到 |
mem 结构体分析
struct mem {
mem_size_t next;
mem_size_t prev;
uint8_t used;
};
| 变量名称 | 描述 |
|---|---|
| next | 指向当前内存块的下一个 |
| prev | 指向上一个内存块 |
| used | 表示该内存块是否已被分配 |
编译阶段
#define MIN_SIZE (12)
#define USER_MEM_SIZE (2 * 1024)
#define MEMP_SIZE (0)
#define MEM_ALIGNMENT (4)
#define LWIP_MEM_ALIGN_SIZE(size) (((size) + MEM_ALIGNMENT - 1) & ~(MEM_ALIGNMENT - 1))
#define MEMP_ALIGN_SIZE(x) (LWIP_MEM_ALIGN_SIZE(x))
#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(USER_MEM_SIZE)
static uint8_t rz_ram_heap[MEM_SIZE_ALIGNED + (2U*SIZEOF_STRUCT_MEM) + MEM_ALIGNMENT];
| 宏 | 描述 |
|---|---|
| MEM_SIZE_ALIGNED | 是内存堆大小MEM_SIZE进行内存对齐后的值 |
| SIZEOF_STRUCT_MEM | 是结构体mem进行内存对齐后的大小 |
| MEM_ALIGNMENT | 是为了后续的内存对齐而附加进去的字节 |
| MIN_SIZE | 申请的内存最小为 12 字节,因为一个内存块最起码需要保持 mem结构体的信息,以便于对内存块进行操作,而该结构体在对齐后的内存大小就是 12 字节。 |
| USER_MEM_SIZE | 用户可定义的堆空间总大小 |
| LWIP_MEM_ALIGN_SIZE | 数据占用空间大小对齐 |
| MEMP_SIZE | 对齐后的mem结构的大小(用于管理memp) |
| MEMP_ALIGN_SIZE | 计算x进行内存对齐后的大小 |
函数实现
与内存堆管理相关的系统函数主要有三个:
| 函数名称 | 描述 |
|---|---|
| rz_mem_init | 内存堆初始化函数,在内核初始化时,该函数必须被调用,以完成内存堆的初始化; |
| rz_mem_malloc | 内存堆分配函数 |
| rz_mem_free | 内存堆释放函数 |
rz_mem_init
void rz_mem_init(void)
{
struct mem *mem = NULL;
LWIP_ASSERT("Sanity check alignment",(SIZEOF_STRUCT_MEM & (MEM_ALIGNMENT - 1)) == 0);
ram_start = (uint8_t *)LWIP_MEM_ALIGN(LWIP_RZ_RAM_HEAP_POINTER); /* 内存堆空间对齐,ram记录对齐后的起始地址 */
printf("mem_init: ram_start 0x%08x\r\n",ram_start);
mem = (struct mem *)(void *)ram_start; /* 在ram_start起始处放置一个mem类型的结构体 */
mem->next = MEM_SIZE_ALIGNED; /* 下一个内存块偏移量为 MEM SIZE_ ALIGNED */
mem->prev = 0; /* 上一个内存块为空*/
mem->used = 0; /* 设置未使用标志 */
/* 初始化内存堆中的最后一个内存块, ram_end记录最后一个块的地 */
ram_end = ptr_to_mem(MEM_SIZE_ALIGNED);
printf("mem_init: ram_end 0x%08x\r\n",ram_end);
ram_end->used = 1;
ram_end->next = MEM_SIZE_ALIGNED; /* 上一个内存块指向 */
ram_end->prev = MEM_SIZE_ALIGNED; /* 下一个内存块指向 */
/* 最低地址空闲块指向第一个控制块 */
lfree = (struct mem *)(void *)ram_start;
}
经过 rz_mem_init()函数后,内存堆会被初始化为两个内存块,第一个内存块的大小就是整个内存堆的大小,而第二个内存块就是介绍内存块,其大小为 0,并且被标记为已使用状态,无法进行分配。值得注意的是,系统在运行的时候,随着内存的分配与释放,lfree指针的指向地址不断改变,都指向内存堆中低地址空闲内存块,而 ram_end 则不会改变,它指向系统中最后一个内存块,也就是内存堆的结束地址。初始化完成的示意图具体见图
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WhCSDCxb-1596330459555)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200801223942958.png)]](https://i-blog.csdnimg.cn/blog_migrate/3e0f129358876bd29382ca40c1f5d41b.png)
rz_mem_malloc
内存分配函数根据用户指定申请大小的内存空间进行分配内存,其大小要大于MIN_SIZE,LwIP中使用内存分配算法是首次拟合方法,其分配原理就是在空闲内存块链表中遍历寻找,直到找到第一个合适用户需求大小的内存块进行分配,如果该内存块能进行分割,则将用户需要大小的内存块分割出来,剩下的空闲内存块则重新插入空闲内存块链表中。经过多次的分配与释放,很可能会出现内存碎片,当然,LwP也有解决的方法,在内存释放中会进行讲解。
rz_mem malloc(0函数是LwIP中内存分配函数,其参数是用户指定大小的内存字节数,如果申请成功则返回内存块的地址,如果内存没有分配成功,则返回NULL,分配的内存空间会受到内存对其的影响,可能会比申请的内存略大,比如用户需要申请22个字节的内存,而CPU是按照4字节内存对齐的,那么分配的时候就会申请24个字节的内存块。
void *rz_mem_malloc(mem_size_t size_in)
{
mem_size_t ptr,ptr2; /* 两个局部变量,用于保存某个内存块起始地址的偏移量*/
mem_size_t size;
struct mem *mem,*mem2;
if (0 == size_in) {
/* 如果申请的长度为0,返回NULL */
printf("function is %s()-----line %d, malloc size is zero \r\n",__FUNCTION__,__LINE__);
return NULL;
}
/* 将size修正为内存对齐字节数的整数倍 */
size = (mem_size_t)LWIP_MEM_ALIGN_SIZE(size_in);
/* 申请空间至少为 MIN_SIZE_ALIGNED*/
if (size < MIN_SIZE_ALIGNED) {
/* 否则设置为默认的最小值 */
size = MIN_SIZE_ALIGNED;
}
/* 申请长度大于内存堆总大小,失败 */
if ((size > MEM_SIZE_ALIGNED) || (size < size_in )) {
printf("function is %s()-----line %d, (size > MEM_SIZE_ALIGNED) || (size < size_in ) \r\n",__FUNCTION__,__LINE__);
return NULL;
}
/* 从lfree开始遍历,找出第一个长度大于size的空闲内存块*/
for (ptr = mem_to_ptr(lfree); ptr < MEM_SIZE_ALIGNED - size; ptr = ptr_to_mem(ptr)->next) {
mem = ptr_to_mem(ptr); /* 获得一个内存块的起始地址 */
/* 若该内存块未用,且其空间不小于(用户请求大小+系统结构体mem) */
if ((!mem->used) && ((mem->next - (ptr+SIZEOF_STRUCT_MEM))>=size)) {
/*到这里,则满足了从该内存块中分配空间的条件,但是接下来必须要判断是将该
*内存块全部分配给用户,还是截取其中的一部分给用户,判断标准:
* 若做截取判断剩下的部分是否能组成一个最小的内存块,即是否能剩下
*/
if ((mem->next - (ptr+SIZEOF_STRUCT_MEM)) >(size+SIZEOF_STRUCT_MEM+MIN_SIZE_ALIGNED)) {
/* 到这里,需要将内存块截取一部分给用户,剩下的重新组织为一个空闲内存块 */
ptr2 = ptr + SIZEOF_STRUCT_MEM +size; /* 分配后,剩余空间起始处的偏移量*/
LWIP_ASSERT("invalid next ptr",ptr2 != MEM_SIZE_ALIGNED);
mem2 = ptr_to_mem(ptr2); /*剩余空间起始处转为mem结构*/
mem2->used = 0;/* 标记未使用 */
mem2->next = mem->next;/*将新的空闲块插入到原来的链表中 */
mem2->prev = ptr;
mem->next = ptr2;/* */
mem->used = 1;/* 配给用户的内存块标记为已用 */
if(MEM_SIZE_ALIGNED != mem2->next) { /*若新空闲块不是链表中最后一个 */
ptr_to_mem(mem2->next)->prev = ptr2; /*将其后一个空闲块的prev指针指向它 */
}
} else {
mem->used = 1;/* 标记为已用 */
}
/*到这里分配完毕,调整指针 lfree */
if (mem == lfree) {
struct mem *cur = lfree; /*只有 lfree 指向的内存块被分配出去了,才更新 lfree*/
/*查找链表,得到下一个位置最低的*/
while(cur->used && cur!=ram_end) {
cur = ptr_to_mem(cur->next);
}
lfree = cur;
LWIP_ASSERT("mem_malloc: !lfree->used",((lfree == ram_end) || (!lfree->used)));
}
LWIP_ASSERT("mem_malloc: allocated memory not above ram_end.",
(mem_ptr_t)mem + SIZEOF_STRUCT_MEM + size <= (mem_ptr_t)ram_end);
LWIP_ASSERT("mem_malloc: allocated memory properly aligned.",
((mem_ptr_t)mem + SIZEOF_STRUCT_MEM) % MEM_ALIGNMENT == 0);
LWIP_ASSERT("mem_malloc: sanity check alignment",
(((mem_ptr_t)mem) & (MEM_ALIGNMENT - 1)) == 0);
/* 分配成功,返回可用起始区域 */
return (uint8_t*)mem + SIZEOF_STRUCT_MEM;
}
}
printf("function is %s()-----line %d, could not allocate %d \r\n",__FUNCTION__,__LINE__,size);
return NULL; /* 返回空指针 */
}
如果在初始化后的内存堆中分配了一个大小为 24 字节的内存块出去,则分配完成的示意图具体见图
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cEDgsr3J-1596330377710)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200802085549771.png)]](https://i-blog.csdnimg.cn/blog_migrate/3c24215e3b4542b5c2011e8b7e96d484.png)
rz_mem_free
void rz_mem_free(void *rmem)
{
struct mem *mem = NULL;
if (NULL == rmem) { /*判断释放地址是否为空,则直接返回 */
printf("function is %s()-----line %d, (rmem == NULL) \r\n",__FUNCTION__,__LINE__);
return;
}
/* 对释放的地址进行偏移,得到真正内存块的起始地址 */
mem = (struct mem *)(void *)((uint8_t *)rmem - (SIZEOF_STRUCT_MEM ));
/*判断一下内存块的起始地址是否合法,如果不合法则直接返回。 */
if ((uint8_t*)mem <(uint8_t*)ram_start || (uint8_t*)rmem + MIN_SIZE_ALIGNED >= (uint8_t*)ram_end) {
printf("function is %s()-----line %d, illegal memory \r\n",__FUNCTION__,__LINE__);
return;
}
/* 判断一下内存块是否被使用,如果是未使用的也直接返回。 */
if (!mem->used) {
printf("function is %s()-----line %d, illegal memory: double free \r\n",__FUNCTION__,__LINE__);
printf("mem: 0x%08x, used flag: %d, \n", mem, mem->used);
return;
}
/* 程序执行到这里,表示内存块能正常释放,就将 used 置 0 表示已经释放了内存块。 */
mem->used = 0;
/*如果刚刚释放的内存块地址比 lfree 指向的内存块地址低,则更新lfree 指针。 */
if (mem<lfree) {
lfree = mem;
}
/* 调用 plug_holes()函数尝试进行内存块合并,如果能合并则合并,该函数就是说的内存块合并算法,
* 只要新释放的内存块与上一个或者下一个空闲内存块在地址上是连续的,则进行合并 */
plug_holes(mem);
}
对于内存的释放要非常小心,尤其要注意的是传递给函数的参数,该参数必须是用户在内存申请时通过调用函数 rz_mem malloc返回的地址,因为系统会根据该地址去寻找系统内存管理结构mem。通过操作mem结构,才能实现内存块的有效回收,这点用户必须尤其注意,否则整个内存堆区域都会被打乱,最终导致致命性问题。
plug_holes
static void plug_holes(struct mem *mem)
{
struct mem *nmem = NULL;
struct mem *pmem = NULL;
LWIP_ASSERT("plug_holes: mem >= ram_start", (uint8_t *)mem >= ram_start);
LWIP_ASSERT("plug_holes: mem < ram_end", (uint8_t *)mem < (uint8_t *)ram_end);
LWIP_ASSERT("plug_holes: mem->used == 0", mem->used == 0);
/* plug hole forward */
LWIP_ASSERT("plug_holes: mem->next <= MEM_SIZE_ALIGNED", mem->next <= MEM_SIZE_ALIGNED);
/* 查找相邻的下一个内存块 */
nmem = ptr_to_mem(mem->next);
/* 若下一个内存块空闲且不是系统最后一个内存块 */
if (mem != nmem && nmem->used == 0 && (uint8_t *)nmem != (uint8_t *)ram_end) {
/* 若lfree 指向 nmem,则将 lfree 指向 mem*/
if(lfree == nmem) {
lfree = mem; /* 合并后,nmem将消失 */
}
mem->next = nmem->next;/* 直线合并操作合并后,即在链表中删除nmem */
if (nmem->next != MEM_SIZE_ALIGNED) {
ptr_to_mem(nmem->next)->prev = mem_to_ptr(mem);
}
}
/* 查找相邻的上一个内存块 */
pmem = ptr_to_mem(mem->prev);
if (pmem != mem && pmem->used == 0) { /* 前一个内存块存在且未用 */
if (lfree == mem) { /* 若 lfree指向mem,则将 Ifree指向pmem */
lfree = pmem; /* 合并后,mem将消失 */
}
pmem->next = mem->next;
if (mem->next != MEM_SIZE_ALIGNED) { /* 执行合并操作,即在链表中删除mem */
ptr_to_mem(mem->next)->prev = mem_to_ptr(pmem);
}
}
}
rz_mem_calloc
void *rz_mem_calloc(mem_size_t count, mem_size_t size)
{
void *p = NULL;
/* allocate 'count' objects of size 'size' */
p = rz_mem_malloc((mem_size_t)count* size);
if (p) {
/* zero the memory */
memset(p, 0, count*size);
}
return p;
}
用例说明
int user_test(void)
{
unsigned char *test1 = NULL;
/* 初始化堆 */
rz_mem_init();
/* 申请堆空间 */
test1 = (unsigned char *)rz_mem_calloc(1,1024);
if (NULL == test1) {
printf("mem_malloc err!\r\n");
}
for (uint16_t i=0;i<1024; i++) {
printf("mem_malloc: test1 addr[%d] 0x%08x\r\n", i, &test1[i]);
}
printf("mem_malloc: test1 addr 0x%08xx\r\n",test1);
/* 释放申请的堆空间 */
rz_mem_free(test1);
return 0;
}
if (NULL == test1) {
printf("mem_malloc err!\r\n");
}
for (uint16_t i=0;i<1024; i++) {
printf("mem_malloc: test1 addr[%d] 0x%08x\r\n", i, &test1[i]);
}
printf("mem_malloc: test1 addr 0x%08xx\r\n",test1);
/* 释放申请的堆空间 */
rz_mem_free(test1);
return 0;
}
本文详细介绍了LwIP中的动态内存堆管理策略,包括内存堆的初始化、分配、释放过程,以及内存碎片处理机制。重点分析了内存堆的数据结构、内存分配算法和内存释放算法,展示了如何在嵌入式系统中高效地管理和使用内存。
1万+

被折叠的 条评论
为什么被折叠?



