LWIP v2.1.0内存管理之内存堆管理(mem.c/mem.h)

1、内存堆相关的几个重要数据结构

MIN_SIZE 是12个字节:这谁能给我说说这个最新内存字节为什么是12,想破脑袋了也布吉岛啊

内存池的链表头:LWIP中内存堆的管理的链表头中使用的 next 和 prev 是地址偏移量而不是指针,这么做的原因是在堆大小满足条件 MEM_SIZE <= 64000L 时可以起到节省内存的效果,不要问为什么,因为指针啊。。。。

LWIP v2.1.0内存管理之内存堆管理(mem.c/mem.h)_第1张图片

内存堆大小:内存堆的大小是由下面的一个宏函数定义的,是由:对齐后的堆大小 + 2倍的链表头大小 + 对齐字节

LWIP_DECLARE_MEMORY_ALIGNED(ram_heap, MEM_SIZE_ALIGNED + (2U * SIZEOF_STRUCT_MEM));

需要注意的是用户定义的HEAP大小 MEM_SIZE_ALIGNED ,在其实能被使用的空间是 MEM_SIZE_ALIGNED - SIZEOF_STRUCT_MEM,因为要在 ram_heap[] 数组头部放一个链表头结构体,具体内存分布会在后面介绍。

其他全局变量:

static u8_t *ram;//指向堆对齐的后的起始地址,地址不会改变

static struct mem *ram_end;//指向内存块的最后,地址不会改变

static struct mem * LWIP_MEM_LFREE_VOLATILE lfree;//指向最靠前的空闲内存块,LWIP_MEM_LFREE_VOLATILE 是volatile,在使用操作系统的时候会被定义,至于为什么,请自行百度。

 

2、内存堆管理的相关函数

以下函数介绍只介绍重要的几个,内存的溢出检查不在范围以内,且不涉 MEM_LIBC_MALLOC= 1和 MEM_USE_POOLS = 1的情况。

内存堆初始化函数 mem_init(void):

步骤1:获取对齐后的堆数组地址,然后把堆数组的前几个字节转换成 mem 结构体类型;

步骤2:把链表头的next 地址偏移到堆数组的第MEM_SIZE_ALIGNED 字节位置,把 prev 地址偏移到堆数组首个元素的位置,并标记内存块没有使用。

步骤3:把 MEM_SIZE_ALIGNED 字节之后的几个字节转换成 mem 结构体类型存放尾链表,再把链表尾的 next和prev 地址偏移到堆数组的第 MEM_SIZE_ALIGNED 个字节位置,然后标记数组已经被使用;

步骤4:最后在把空闲块指针指向堆数组的开始位置;

相关困惑:1、在步骤2中为什么不把next 地址偏移位置加上一个链表空间的大小?2、为什么不把步骤3中的尾链表位置向后移动一个链表空间大小?这样就不会浪费了一个结构体大小的空间了,可用堆空间会变成 (ram_end - ram),这个问题几乎没人解释,但是我查看了1.4.1\2.0.0\2.0.3这几个版本都是这样的,可能是有我理解不了的原因。

初始化后的内存空间如下图:

LWIP v2.1.0内存管理之内存堆管理(mem.c/mem.h)_第2张图片

内存堆申请函数 mem_malloc(mem_size_t size_in):

本函数主要是申请 size 个字节的内存空间,申请成功则返回申请的内存块的起始地址,这个地址是 used 之后的地址,否则则返回 NULL。注意申请的内存空间 size_in 是我们要求的可用内存大小,不包括链表大小,可能比我们想要的要大点,因为要进行内存大小对齐,其次还可能剩余的内存空间不足以被分割为新的内存块;申请的最小内存大小至少是12字节;

步骤1:判断 size_in 的大小是否符合要求,否则直接返回NULL;

步骤2:从首个空闲内存块开始寻找一个没有被占用且大小符合 size+SIZEOF_STRUCT_MEM 的空闲结构体,注意这里的内存块遍历也是要遍历已经使用的内存块的,具体原因后面介绍;

步骤3:通过以上步骤找到了这样一个符合要求的内存块,之后还要判断这个空闲内存块能不能被分割出一个新的内存块,也就是判断剩余的空间是否大于等于 SIZEOF_STRUCT_MEM + MIN_SIZE_ALIGNED 。如果不能被分割了,那么就直接把当前内存块的 used=1 即可;如果可以被分割,则在内存中会多出一个分割后的空闲内存块,然后调整相应内存块的前后项偏移,再把已被申请的内存块 used=1,这里要注意非空闲内存块依然是在内存链表上的,并不是像FreeRTOS的内存管理那样,使用一个链表把所有空闲内存块串在一起。

步骤4:如果申请走的是首个空闲内存块,那还需要把空闲内存块指针向后移动,找到下一个空闲内存块,在这之间或许要跨过多个已经被申请的内存块。

步骤5:然后返回申请的地址,注意这个地址使用跨过了链表头内存的,不是跨过鸭绿江的。

注意这里的内存块申请后是没有进行内存空间初始化的。

内存堆释放函数 mem_free(void *rmem):

此函数传入的参数是要释放的内存块的地址,这个地址是由 mem_malloc() 函数申请来的地址。整个过程非常简单就是在满足条件的情况下把这个要释放的内存块的 used=0 即可,其次就是进行内存合并;

步骤1:更具以下条件判断内存是否能够释放:① 地址是否为空;② 地址是否是对齐地址;③ 释放的地址是否在ram_heap[]空间内;④ 地址是内存是否被占用;⑤ 整个链表是否有问题;如果以上有一条没有满足则释放不成功。

步骤2:传入的地址满足释放条件后,直接把当前内存块的 used = 0,内存释放到此就结束了,这就是在申请的时候不把占用内存块从链表移除的好处,不需要进行插入操作啦,不过这也导致在申请的时候遍历链表的开销变大;

步骤3:然后把进入 plug_holes() 函数去进行内存合并,先获取当前内存块前一个内存块和后一个内存块是否被占用,如果没有被占用就直接进行内存块的合并,这也是非空闲内存块不提出带来的好处,而对于FreeRTOS的内存管理进行合并时还需要进行前后内存地址计算和检查,这也是要花费开销的。

内存堆裁剪函数mem_trim(void *rmem, mem_size_t new_size):

这个函数是对已经申请的内存块进行裁剪的函数,第一个参数是要裁剪的内存块的地址,必须是由 mem_malloc() 函数申请来的地址,第二个参数是新内存块的大小,也就是从第一个参数向后申请多大空间,这个空间不能大于原先内存块的空间,否则裁剪失败。

mem_calloc(mem_size_t count, mem_size_t size):

此函数是 mem_malloc() 的重新封装,申请的内存大小就是两个参数的乘积,与 mem_malloc() 的主要区别就是他会在内存申请后对内存进行初始化,这是非常有用的。

以下用图例说明内存堆的申请和释放过程,注意这里忽略内存对齐和内存碎片等情况,只是便于理解而已,这里是内存堆中某一时刻的内存分布,可以看出不论内存块是否使用,都是在链表上的:

LWIP v2.1.0内存管理之内存堆管理(mem.c/mem.h)_第3张图片

从内存堆中申请一个40 Bytes大小的内存,之后的内存分布情况如下图从这里可以看出,内存被分割且Ifree指针发送了移动:

LWIP v2.1.0内存管理之内存堆管理(mem.c/mem.h)_第4张图片


从内存堆中释放一个60 Bytes的内存块,释放之后的内存分布如下图,从这里可以看出内存释放后,与前后的空闲内存进行了合并,形成了一个大的内存块:

LWIP v2.1.0内存管理之内存堆管理(mem.c/mem.h)_第5张图片

 

你可能感兴趣的:(LWIP2.1.0)