LWIP协议栈中使用了两种主要的内存管理方法,动态内存池和动态内存堆。
动态内存池
这种内存管理方法下,用户只能申请固定大小的空间,在LWIP中主要对固定数据结构的分配,例如:TCP控制块、UDP控制块等。
数据结构
与动态内存池相关的全局数据变量或数据类型如下表所示:
名称 | 类型 | 描述 |
---|---|---|
memp_t | 枚举体 | 为每类POOL定义一个名称 |
memp_tab[] | 全局指针数组 | 指向一类POOL中第一个POOL |
memp_sizes[] | 全局数组 | 一类POOL中单个POOL的大小 |
memp_num[] | 全局数组 | 一类POOL中POOL的个数 |
memp_desc[] | 全局指针数组 | 指向一类POOL的描述字符串 |
memp_memory[] | 全局数组 | 为所有POOL分配内存空间 |
首先我们看看源码如何定义memp_t ,
typedef enum {
#define LWIP_MEMPOOL(name,num,size,desc) MEMP_##name,
#include "lwip/memp_std.h"
MEMP_MAX
} memp_t;
咋一看,一脸懵逼,萌就对了。
疑点1是这个宏定义中 ## ,双#号在C中是连接符,用来连接Token的,这个Token可以是关键字、自定义变量。
比如遇到下面的语句:
LWIP_MEMPOOL(huoshan,num1,size1,desc1)
那么编译器会替换为 MEMP_huoshan 。
疑点2就是这个包含头文件语句,放在这里话,使编译器去查找头文件中相关语句,当编译完后,这个枚举体也就定义完了。
编译完后,应该枚举体memp_t应该是如下定义:
typedef enum{
MEMP_RAW_PCB,
MEMP_UDP_PCB,
...,
MEMP_MAX
}memp_t;
同理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()核心代码,内核空间被初始化如下图所示:
对照上图,代码逻辑一目了然。
struct memp {
struct memp *next;
};
void memp_init(void)
{
struct memp *memp;
u16_t i, j;
memp = (struct memp *)LWIP_MEM_ALIGN(memp_memory);
/* for every pool: */
for (i = 0; i < MEMP_MAX; ++i) {
memp_tab[i] = NULL;
/* create a linked list of memp elements */
for (j = 0; j < memp_num[i]; ++j) {
memp->next = memp_tab[i];
memp_tab[i] = memp;
memp = (struct memp *)(void *)((u8_t *)memp + MEMP_SIZE + memp_sizes[i]
);
}
}
}
分配函数memp_malloc()和释放函数memp_free()代核心源码如下,代码逻辑很简单,就是把对应有效的地址分配出去和回收回来。
void * memp_malloc(memp_t type)
{
struct memp *memp;
SYS_ARCH_DECL_PROTECT(old_level);
SYS_ARCH_PROTECT(old_level);
memp = memp_tab[type];
if (memp != NULL) {
memp_tab[type] = memp->next;
MEMP_STATS_INC_USED(used, type);
memp = (struct memp*)(void *)((u8_t*)memp + MEMP_SIZE);
} else {
MEMP_STATS_INC(err, type);
}
SYS_ARCH_UNPROTECT(old_level);
return memp;
}
void memp_free(memp_t type, void *mem)
{
struct memp *memp;
SYS_ARCH_DECL_PROTECT(old_level);
if (mem == NULL) {
return;
}
memp = (struct memp *)(void *)((u8_t*)mem - MEMP_SIZE);
SYS_ARCH_PROTECT(old_level);
MEMP_STATS_DEC(used, type);
memp->next = memp_tab[type];
memp_tab[type] = memp;
SYS_ARCH_UNPROTECT(old_level);
}
动态内存堆
动态内存堆分配策略本质是对事先定义好的内存块进行合理有效的组织和管理,内存分配的策略采用首次拟合方式,一旦找到比用户要求大的空闲块,就从中切割,并把剩余的部分返回到动态内存堆中。这种分配策略有点是内存浪费小,适合小内存管理,缺点是如果频繁分配和释放,可能导致大量的内存碎片。
数据结构
这种策略下用户申请的内存块大小有最小限制,大小不能低于MIN_SIZE,这个值默认12,用户也可以自己定义。动态内存堆的相关数据结构如下表:
名称 | 类型 | 描述 |
---|---|---|
ram_heap[] | 全局型数组 | 系统内存堆空间 |
ram | 全局型指针 | 指向内存堆空间对其后的起始地址 |
mem | 结构体 | 内核附加在各个内存块前面的结构体 |
ram_end | mem型指针 | 指向系统最后一个内存块 |
lfree | mem型指针 | 指向当前系统具有最低地址的空闲内存块 |
mem_sem | 信号量 | 用于保护内存堆的 互斥信号量 |
mem定义与整个内存堆大小密切相关,
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;
};
内存堆组织就结构如下图所示,注意:内存块大小可以随时变化;下图是多次分配释放后的结果;used是分配与否的唯一标志。
函数实现
同内存池一样,系统函数主要是内存堆初始化函数mem_init(),内存堆分配函数mem_malloc(),内存堆释放函数mem_free()。
mem_init()核心源码如下:
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;
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);
}
}
经过初始化函数后,内存堆区域划分为两个内存块,第一块包含了所有空用空间,第二块没有内存空间,只有一个结构体组成,且被标记为已用。同时初始化两个指针,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,然后利用这个结构体实现内存块释放、合并等操作,内存块回收后,标志位清零。防止内存碎片产生。上下内存块的使用标志位会被检测,如果有未使用的,将进行合并操作。