LWIP协议栈中使用了两种主要的内存管理方法,动态内存池和动态内存堆。
LwIP 也支持 C 标准库的 malloc() 和 free(),但是容易产生内存碎片,不建议使用,可以在lwipopt.h中配置
动态内存池
这种内存管理方法下,用户只能申请固定大小的空间,在LWIP中主要对固定数据结构的分配,例如:TCP控制块、UDP控制块等
LwIP 中有很多固定的数据结构空间,如 TCP 首部、UDP 首部,IP 首部,以太网首部等都是固定的数据结构,其大小就是一个固定的值,那么我们就能采用这种方式分配这些固定大小的内存空间,这样子的效率就会大大提高,并且无论怎么申请与释放,都不会产生内存碎片,这就让系统能很稳定地运行。这种分配策略在 LwIP 中被称之为动态内存池分配策略
申请大小必须是指定固定大小字节的值(如 4、8、16 等等),系统将所有可用区域以固定大小
的字节单位进行划分,然后用单链表将所有空闲内存块连接起来。链表中所有节点大小相同,分
配,释放都非常简单。
LwIP 源文件中 memp.c 和 memp.h 就是动态内存池分配策略
内存池的预处理
在内核初始化时,会事先在内存中初始化相应的内存池,内核会将所有可用的区域根据宏定义的
配置以固定的大小为单位进行划分,然后用一个简单的链表将所有空闲块连接起来,这样子就组
成一个个的内存池。由于链表中所有节点的大小相同,所以分配时不需要查找,直接取出第一个
节点中的空间分配给用户即可
注意了,内核在初始化内存池的时候,是根据用户配置的宏定义进行初始化的,比如,用户定义了
LWIP_UDP 这个宏定义,在编译的时候,编译器就会将与 UDP 协议控制块相关的数据构编译编译
进去,这样子就将 LWIP_MEMPOOL(UDP_PCB, MEMP_NUM_UDP_PCB, sizeof(struct udp_pcb),”UDP_PCB”) 包含进去,在初始化的时候,UDP 协议控制块需要的 POOL 资源就会被初始化。
其数量由 MEMP_NUM_UDP_PCB 宏定义决定
不同协议的PCB数量都可以在lwipopts.h文件中定义,
注意了,不同协议的 POOL 内存块的大小是不一样的,这由协议的性质决定,如 UDP 协议控制块的内存块大小是 sizeof(struct udp_pcb),而 TCP 协议控制块的 POOL 大小则为 sizeof(struct tcp_pcb)
关键文件:memp_std.h,位于 include/lwip/priv 目录下
它 里面全是宏定义,LwIP 为什么要这样子写呢,其实很简单,当然是为了方便,在不同的地方调
用 #include “lwip/priv/memp_std.h”就能产生不同的效果
该文件中的宏值定义全部依赖于宏 LWIP_MEMPOOL(name,num,size,desc),这样,只要外部提供的该宏值不同,则包含该文件的源文件在编译器的预处理后,就会产生不一样的结果。这样,就
可以通过在不同的地方多次包含该文件,前面必定提供宏值 MEMPOOL 以产生不同结果
简单来说,就是在外边提供 LWIP_MEMPOOL 宏定义,然后在包含 memp_std.h 文件,编译器就
会帮我们处理
数据结构
与动态内存池相关的全局数据变量或数据类型如下表所示:
名称 | 类型 | 描述 |
memp_t | 枚举体 | 为每类POOL定义一个名称 |
memp_tab[] | 全局指针数组 | 指向一类POOL中第一个POOL |
memp_sizes[] | 全局数组 | 一类POOL中单个POOL的大小 |
memp_num[] | 全局数组 | 一类POOL中POOL的个数 |
memp_desc[] | 全局指针数组 | 指向一类POOL的描述字符串 |
memp_memory[] | 全局数组 | 为所有POOL分配内存空间 |
宏定义中 ## ,双#号在C中是连接符,用来连接Token的,注意这里连接的对象是 Token 就行,而不一定是宏的变量。在编译器编译的时候,它会扫描源码,将代码分解为一个个的 Token,Token 可以是 C 语言的关键字,如 int、for、while 等,也可以是用户自定义的变量,如,a、num、name 等
比如遇到下面的语句:
LWIP_MEMPOOL(huoshan,num1,size1,desc1)
那么编译器会替换为 MEMP_huoshan 。
这个包含头文件语句,放在这里话,使编译器去查找头文件中相关语句,当编译完后,这个枚举体也就定义完了。
编译完后,应该枚举体memp_t应该是如下定义:
typedef enum{
MEMP_RAW_PCB,
MEMP_UDP_PCB,
...,
MEMP_MAX
}memp_t;
memp_t 类型在整个内存池的管理中是最重要的存在,通过内存池申请函数申请内存的时候,唯
一的参数就是 memp_t 类型的,它将告诉分配的函数在哪种类型的 POOL 中去分配对应的内存块,这样子就直接管理了系统中所有类型的 POOL
这个枚举变量的 MEMP_MAX 不代表任何类型的 POOL,它只是记录这系统中所有的 POOL 的
数量,比如例子中的 MEMP_RAW_PCB 的值为 0,而 MEMP_MAX 的值为 9,就表示当前系统中
有 9 种 POOL
不过还有一点需要注意的是,在 memp_std.h 文件的最后需要对 LWIP_MEMPOOL 宏定义进行撤销,因为该文件很会被多个地方调用,在每个调用的地方会重新定义这个宏定义的功能,所以在文件的末尾添加这句 #undef LWIP_MEMPOOL 代码是非常有必要的。
按照这种包含头文件的原理,只需要定义 LWIP_MEMPOOL 宏的作用,就能产生很大与内存池
相关的操作,如在 memp.c 文件的开头就定义了如下代码:
假如在memp_std.h中定义了LWIP_RAW,经过转换后就会得到如下结果,其他宏定义也是一样的道理
每种 POOL 在经过编译器都会得到一个结构体,memp_desc memp_XXXX,XXXX 表示对应的 POOL 类型,如RAW_PCB 的结构体就是 memp_desc memp_RAW_PCB,这里面就记录了该内存块对其后的大小 LWIP_MEM_ALIGN_SIZE(sizeof(struct raw_pcb))。也就是说,在经过编译器的处理,该结构体就保存了每种 POOL 的内存对齐后的大小。
同 理 该 结 构 体 也 记 录 了 每 种 POOL 的 其 他 参 数, 如 内 存 块 的 个 数 num, 比 如
MEMP_NUM_RAW_PCB,这些就是用户配置的宏定义,都会被记录在里面,还有每种
POOL 的描述“DECLARE_LWIP_MEMPOOL_DESC(“RAW_PCB”)”,当然这个参数可用可不
用,这只是一个字符串,在输出信息的时候用到。
除 了 这 些 信 息, 还 有 一 个 最 重 要 的 信 息, 那 就 是 真 正 的 内 存 池 区 域, 使 用 u8_t
memp_memory_XXXX_base 进行定义,XXXX 表示对应的 POOL 类型,每个类型都有自己
的内存池区域,是编译器开辟出来的内存空间,简单来说就是一个数组,我们知道这个区域的的起始地址,就能对它进行操作。
同理memp_sizes[]、memp_num[] 、memp_desc[]、memp_memory[] 也是这么完成数组的定义。其中memp_memory[]咋一看不像数组定义,
static u8_t memp_memory[MEM_ALIGNMENT - 1
#define LWIP_MEMPOOL(name,num,size,desc) + ( (num) * (MEMP_SIZE + MEMP_ALIGN_SIZE(size) ) )
#include "lwip/memp_std.h"
];
实际上编译后,把头文件中相关宏控替换成 +( (num) * (MEMP_SIZE + MEMP_ALIGN_SIZE(size) ),最终数组定义为memp_memory[MEM_ALIGNMENT - 1 + ()+()+…]。(注释:MEM_ALIGNMENT - 1 是为了系统内存对齐)
内存初始化
内存管理函数主要有三个:memp_init()、 memp_malloc()、 memp_free()。
下面是memp_init()核心代码,内核空间被初始化如下图所示:
该函数是比较简单的函数,就是根据每种 POOL 的 memp_desc 描述进行初始化,在每种类型的
POOL 中将空闲内存块连接成单链表,并且使用 memset() 函数将其内容清零,这样子就初始化完
成了
内存分配
通过 memp_malloc 函数进行申请内存块,而内存块的大小就是指定的大小,其过程很简单,就是根据内存池的类型去选择从哪个内存池进行分配,因为不同类型的内存池中内存块大小是不一样的,比如 TCP_PCB与 UDP_PCB 的大小就不一样,所以申请内存的时候传入的参数是内存池的类型而并非要申请的内存大小,系统中所有的内存池类型都会被记录在 memp_pools 数组中,我们可以将该数组称之为内存池描述表,它负责将系统所有的内存池信息描述并且记录下来,这样子在申请内存的时候就能很迅速得到一个对应的内存块
内存池申请函数的核心代码就一句,那就是 memp = *desc->tab;,通过这句代码,能直接得到对应内存块中的第一个空闲内存块,并将其取出,并且移动 *desc->tab 指针,指向下一个空闲内存块,然后将 ((u8_t *)memp + MEMP_SIZE) 返回,MEMP_SIZE 偏移的空间大小,因为内存块需要一些空间存储内存块相关的信息,该宏定义的值是 (LWIP_MEM_ALIGN_SIZE(sizeof(struct memp)) + MEM_SANITY_REGION_BEFORE_ALIGNED),我们暂时无需理会它,只要知道申请内存块后返回的地址是直接可用的地址即可,而偏移的 MEMP_SIZE 这部分内容是内存分配器管理的空间,用户是不允许触碰的地方,否则就很可能发生错误
内存释放
同样的,内存释放函数也非常简单的,只需要把使用完毕的内存添加到对应内存池中的空闲内存
块链表即可,只不过释放内存有两个参数,一个是 POOL 的类型,还有就是内存块的起始地址
动态内存堆
动态内存堆分配策略本质是对事先定义好的内存块进行合理有效的组织和管理,内存分配的策略采用首次拟合方式,一旦找到比用户要求大的空闲块,就从中切割,并把剩余的部分返回到动态内存堆中。这种分配策略有点是内存浪费小,适合小内存管理,缺点是如果频繁分配和释放,可能导致大量的内存碎片。
动态内存堆管理(heap)又可以分为两种:一种是 C 标准库自带的内存管理策略,另一
种是 LwIP 自身实现的内存堆管理策略。这两者的选择需要通过宏值 MEM_LIBC_MALLOC 来选
择,且二者只能选择其一。
其次,LwIP 在自身内存堆和内存池的实现上设计得非常灵活。内存池可由内存堆实现,反之,内
存堆也可以由内存池实现。通过 MEM_USE_POOLS 和 MEMP_MEM_MALLOC 这两个宏定义来
选择,且二者只能选择其一
文件路径:src\core\mem.c
数据结构
这种策略下用户申请的内存块大小有最小限制,大小不能低于MIN_SIZE,这个值默认12,用户也可以自己定义。
动态内存堆的相关数据结构如下表:
名称 | 类型 | 描述 |
ram_heap[] | 全局型数组 | 系统内存堆空间 |
ram | 全局型指针 | 指向内存堆空间对其后的起始地址 |
mem | 结构体 | 内核附加在各个内存块前面的结构体 |
ram_end | mem型指针 | 指向系统最后一个内存块 |
lfree | mem型指针 | 指向当前系统具有最低地址的空闲内存块 |
mem_sem | 信号量 | 用于保护内存堆的 互斥信号量 |
#define MIN_SIZE 12 //申请的内存最小为 12 字节,因为一个内存块最起码需要保持 mem 结构体的信息,以便于对内存块进行操作,而该结构体在对齐后的内存大小就是 12 字节
内存初始化
在内核初始化的时候,会调用 mem_init() 函数进行内存堆的初始化,内存堆初始化主要的过程就
是对上述所属的内存堆组织结构进行初始化,主要设置内存堆的起始地址,以及初始化空闲列
表。根据用户配置的宏定义进行相关初始化,配置不同其实现也不同(可能为空)
经过 mem_init() 函数后,内存堆会被初始化为两个内存块,第一个内存块的大小就是整个内存堆
的大小,而第二个内存块就是介绍内存块,其大小为 0,并且被标记为已使用状态,无法进行分
配。值得注意的是,系统在运行的时候,随着内存的分配与释放,lfree 指针的指向地址不断改变,都指向内存堆中低地址空闲内存块,而 ram_end 则不会改变,它指向系统中最后一个内存块,也就是内存堆的结束地址
内存分配、释放
分配函数核心源码如下:
void * mem_malloc(mem_size_t size)
{
mem_size_t ptr, ptr2;
struct mem *mem, *mem2;
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 < MIN_SIZE_ALIGNED) {
/* 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();
/* 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 < MEM_SIZE_ALIGNED - size;
ptr = ((struct mem *)(void *)&ram[ptr])->next) {
mem = (struct mem *)(void *)&ram[ptr];
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)) {
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 {
mem->used = 1;
MEM_STATS_INC_USED(used, mem->next - (mem_size_t)((u8_t *)mem - ram));
}
if (mem == lfree) {
/* Find next free block after mem and update lowest free pointer */
while (lfree->used && lfree != ram_end) {
LWIP_MEM_ALLOC_UNPROTECT();
/* prevent high interrupt latency... */
LWIP_MEM_ALLOC_PROTECT();
lfree = (struct mem *)(void *)&ram[lfree->next];
}
}
LWIP_MEM_ALLOC_UNPROTECT();
sys_mutex_unlock(&mem_mutex);
return (u8_t *)mem + SIZEOF_STRUCT_MEM;
}
}
LWIP_MEM_ALLOC_UNPROTECT();
sys_mutex_unlock(&mem_mutex);
return NULL;
}
当分配成功后,内存分配函数会马上在已分配的数据区域后面形成一个mem结构体,以构成新的空闲块。mem_malloc参数是请求分配的字节数,返回值是成功分配的内存起始地址,若未分配成功,返回NULL。内存的申请释放做了线程保护,如果多个线程同时进行,那么可能因为线程锁导致内存操作延时。
释放函数核心源码如下:
void
mem_free(void *rmem)
{
struct mem *mem;
LWIP_MEM_FREE_DECL_PROTECT();
if (rmem == NULL) {
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();
/* Get the corresponding struct mem ... */
mem = (struct mem *)(void *)((u8_t *)rmem - SIZEOF_STRUCT_MEM);
/* ... and is now unused. */
mem->used = 0;
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)));
/* finally, see if prev or next are free also */
plug_holes(mem);
LWIP_MEM_FREE_UNPROTECT();
}
内存块释放时,根据用户提供的释放地址寻找系统结构mem,然后利用这个结构体实现内存块释放、合并等操作,内存块回收后,标志位清零。防止内存碎片产生。上下内存块的使用标志位会被检测,如果有未使用的,将进行合并操作
LWIP中的配置
LwIP 中,内存的选择是通过以下这几个宏值来决定的,根据用户对宏值的定义值来判断使用那
种内存管理策略,具体如下:
• MEM_LIBC_MALLOC:该宏定义是否使用 C 标准库自带的内存分配策略。该值默认情况
下为 0,表示不使用 C 标准库自带的内存分配策略。即默认使用 LwIP 提供的内存堆分配策
略。如果要使用 C 标准库自带的分配策略,则需要把该值定义为 1。
当该宏定义为 0 表示使用 LwIP 自己实现的动态内存管理策略。LwIP 的动态内存管理策略又分
为两种实现形式:一种通过内存堆 (HEAP) 管理策略来实现内存管理 (大数组),另一种是通过内
存池 (POOL) 管理策略来实现内存管理 (事先开辟好的内存池)
• MEMP_MEM_MALLOC:该宏定义表示是否使用 LwIP 内存堆分配策略实现内存池分配
(即:要从内存池中获取内存时,实际是从内存堆中分配)。默认情况下为 0,表示不从内存
堆中分配,内存池为独立一块内存实现。与 MEM_USE_POOLS 只能选择其一。
• MEM_USE_POOLS:该宏定义表示是否使用 LwIP 内存池分配策略实现内存堆的分配(即:
要从内存堆中获取内存时,实际是从内存池中分配)。默认情况下为 0,表示不使用从内存
池中分配,内存堆为独立一块内存实现。与 MEMP_MEM_MALLOC 只能选择其一。
