主要内容参照5. LwIP的内存管理 — [野火]LwIP应用开发实战指南—基于野火STM32 文档,整理出来自用。
一、动态内存堆的两种实现方式
LwIP 的动态内存堆管理(heap)分为两种:
- C 标准库自带的内存管理策略
- LwIP 自身实现的内存堆管理策略
这两种方式通过宏MEM_LIBC_MALLOC选择,且二者只能选其一。
此外,LwIP 在自身内存堆和内存池的实现上设计灵活:
- 内存池可由内存堆实现
- 内存堆也可由内存池实现
通过MEM_USE_POOLS和MEMP_MEM_MALLOC两个宏定义选择,二者同样只能选其一。
二、内存堆的组织结构
内存堆的组织结构包括内存数据结构与一些重要的全局变量,具体如下:
struct mem
{
/** index (-> ram[next]) of the next struct */
mem_size_t next; (1)
/** index (-> ram[prev]) of the previous struct */
mem_size_t prev; (2)
/** 1: this area is used; 0: this area is unused */
u8_t used; (3)
#if MEM_OVERFLOW_CHECK
/** this keeps track of the user allocation size for guard checks */
mem_size_t user_size;
#endif
};
#define MIN_SIZE 12 (4)
LWIP_DECLARE_MEMORY_ALIGNED(ram_heap, MEM_SIZE_ALIGNED+(2U*SIZEOF_STRUCT_MEM)); (5)
#define LWIP_RAM_HEAP_POINTER ram_heap (6)
/** pointer to the heap (ram_heap):
for alignment, ram is now a pointer instead of an array */
static u8_t *ram; (7)
/** the last entry, always unused! */
static struct mem *ram_end; (8)
#if !NO_SYS
static sys_mutex_t mem_mutex; (9)
#endif
static struct mem * LWIP_MEM_LFREE_VOLATILE lfree; (10)
- (1)(2):
next与prev并非指针,而是目的地址相对于整个内存堆起始地址的偏移量。 - (3):
used字段用于标记该内存是否已被使用。 - (4):申请的内存最小为 12 字节,因为一个内存块至少需要保存
mem结构体的信息,该结构体对齐后的内存大小为 12 字节。 - (5):定义内存堆大小,编译器处理后为
u8_t ram_heap[(((MEM_SIZE_ALIGNED + (2U * SIZEOF_STRUCT_MEM)) + MEM_ALIGNMENT - 1U))]。其中MEM_SIZE_ALIGNED是内存堆大小MEM_SIZE经过内存对齐后的大小;SIZEOF_STRUCT_MEM是结构体mem经过内存对齐后的大小;MEM_ALIGNMENT是 CPU 的对齐字节数,一般为 4。 - (6):
LWIP_RAM_HEAP_POINTER宏相当于给ram_heap[]重新命名,ram_heap[]是内核的内存堆空间。 - (7):
ram是全局指针变量,指向内存堆对齐后的起始地址,确保内存堆起始地址按 CPU 对齐方式对齐。 - (8):
ram_end是mem类型指针,指向内存堆中最后一个内存块。 - (9):用于保护内存堆的互斥量,暂时未用。
- (10):
lfree是mem类型指针,指向内存堆中低地址的空闲内存块,即空闲内存块链表指针。
三、内存堆初始化
内核初始化时,会调用mem_init()函数进行内存堆初始化,主要过程是对上述内存堆组织结构进行初始化,设置内存堆起始地址,初始化空闲列表。函数源码如下:
void
mem_init(void)
{
struct mem *mem;
LWIP_ASSERT("Sanity check alignment",
(SIZEOF_STRUCT_MEM & (MEM_ALIGNMENT - 1)) == 0);
/* align the heap */
ram = (u8_t *)LWIP_MEM_ALIGN(LWIP_RAM_HEAP_POINTER); (1)
/* initialize the start of the heap */
mem = (struct mem *)(void *)ram; (2)
mem->next = MEM_SIZE_ALIGNED; (3)
mem->prev = 0; (4)
mem->used = 0; (5)
/* initialize the end of the heap */
ram_end = ptr_to_mem(MEM_SIZE_ALIGNED); (6)
ram_end->used = 1; (7)
ram_end->next = MEM_SIZE_ALIGNED;
ram_end->prev = MEM_SIZE_ALIGNED;
MEM_SANITY();
/* initialize the lowest-free pointer to the start of the heap */
lfree = (struct mem *)(void *)ram; (8)
MEM_STATS_AVAIL(avail, MEM_SIZE_ALIGNED);
if (sys_mutex_new(&mem_mutex) != ERR_OK) (9)
{
LWIP_ASSERT("failed to create mem_mutex", 0);
}
}
- (1):进行内存堆空间对齐,
LWIP_RAM_HEAP_POINTER宏定义为ram_mem,内存堆对齐后的起始地址记录在ram中。 - (2):在内存堆起始位置放置一个
mem类型的结构体,因为初始化后的内存堆是一个大的空闲内存块,每个空闲内存块前面都需放置一个mem结构体。 - (3):下一个内存块的偏移量为
MEM_SIZE_ALIGNED,相当于直接到内存堆的结束地址。 - (4):上一个内存块为空。
- (5):标记为未被使用。
- (6):指针移动到内存堆末尾位置,放置一个
mem类型的结构体,初始化表示内存堆结束的内存块。 - (7):标记该内存块已被使用,因其是结束位置,不能被分配,
next与prev字段都指向自身,仅表示内存堆结束,无内存可分配。 - (8):空闲内存块链表指针指向内存堆起始地址,因当前只有一个内存块。
- (9):创建内存堆分配时使用的互斥量,无操作系统时该语句等效于空。
经过mem_init()函数后,内存堆被初始化为两个内存块:第一个是整个内存堆大小的空闲内存块,第二个是大小为 0、标记为已使用状态的结束内存块,无法分配。系统运行中,随着内存分配与释放,lfree指针指向地址不断改变,始终指向低地址空闲内存块,而ram_end不变,指向最后一个内存块即内存堆结束地址。
四、内存分配
内存分配函数根据用户指定申请大小分配内存空间,大小需大于MIN_SIZE。LwIP 使用首次拟合方法,在空闲内存块链表中遍历寻找第一个合适大小的内存块分配,若可分割,则分割出用户需要的大小,剩余空闲内存块重新插入链表。
mem_malloc()函数是 LwIP 中的内存分配函数,参数为用户指定的内存字节数,成功返回内存块地址,失败返回NULL。分配的内存空间受内存对齐影响,可能比申请的略大,例如申请 22 字节,按 4 字节对齐则分配 24 字节。
内存块申请成功后返回起始地址,但未初始化,可能含随机数据,用户需立即初始化或写入有效数据。在操作系统环境中,内存堆是全局变量,申请内存块不安全,LwIP 使用互斥量保护临界资源,多线程同时申请或释放时会因互斥量产生延迟。函数源码如下:
void *
mem_malloc(mem_size_t size_in)
{
mem_size_t ptr, ptr2, size;
struct mem *mem, *mem2;
LWIP_MEM_ALLOC_DECL_PROTECT();
if (size_in == 0)
{
return NULL;
}
size = (mem_size_t)LWIP_MEM_ALIGN_SIZE(size_in); (1)
if (size < MIN_SIZE_ALIGNED)
{
size = MIN_SIZE_ALIGNED; (2)
}
if ((size > MEM_SIZE_ALIGNED) || (size < size_in))
{
return NULL; (3)
}
sys_mutex_lock(&mem_mutex); (4)
LWIP_MEM_ALLOC_PROTECT();
/* 遍历空闲内存块链表 */
for (ptr = mem_to_ptr(lfree); ptr < MEM_SIZE_ALIGNED - size;
ptr = ptr_to_mem(ptr)->next) (5)
{
mem = ptr_to_mem(ptr); (6)
if((!mem->used)&&(mem->next-(ptr + SIZEOF_STRUCT_MEM))>= size) (7)
{
if (mem->next - (ptr + SIZEOF_STRUCT_MEM) >=
(size + SIZEOF_STRUCT_MEM + MIN_SIZE_ALIGNED))
{
ptr2 = (mem_size_t)(ptr + SIZEOF_STRUCT_MEM + size); (8)
LWIP_ASSERT("invalid next ptr",ptr2 != MEM_SIZE_ALIGNED);
/* create mem2 struct */
mem2 = ptr_to_mem(ptr2); (9)
mem2->used = 0; (10)
mem2->next = mem->next;
mem2->prev = ptr;
/* and insert it between mem and mem->next */
mem->next = ptr2;
mem->used = 1; (11)
if (mem2->next != MEM_SIZE_ALIGNED)
{
ptr_to_mem(mem2->next)->prev = ptr2; (12)
}
MEM_STATS_INC_USED(used, (size + SIZEOF_STRUCT_MEM));
}
else
{
mem->used = 1; (13)
MEM_STATS_INC_USED(used, mem->next - mem_to_ptr(mem));
}
if (mem == lfree) (14)
{
struct mem *cur = lfree;
/*Find next free block after mem and update lowest free pointer */
while (cur->used && cur != ram_end)
{
cur = ptr_to_mem(cur->next); (15)
}
lfree = cur; (16)
LWIP_ASSERT("mem_malloc: !lfree->used",
((lfree == ram_end) || (!lfree->used)));
}
LWIP_MEM_ALLOC_UNPROTECT();
sys_mutex_unlock(&mem_mutex); (17)
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);
MEM_SANITY();
return (u8_t *)mem + SIZEOF_STRUCT_MEM + MEM_SANITY_OFFSET; (18)
}
}
MEM_STATS_INC(err);
LWIP_MEM_ALLOC_UNPROTECT();
sys_mutex_unlock(&mem_mutex); (19)
LWIP_DEBUGF(MEM_DEBUG | LWIP_DBG_LEVEL_SERIOUS,
("mem_malloc: could not allocate %"S16_F" bytes\n", (s16_t)size));
return NULL;
}
- (1):对用户申请的内存大小进行对齐操作。
- (2):若申请的内存大小小于最小内存对齐大小
MIN_SIZE_ALIGNED,则设为默认最小值。 - (3):若申请的内存大小大于整个内存堆对齐后的大小,返回
NULL,申请失败。 - (4):获得互斥量,仅在操作系统环境起作用。
- (5):遍历空闲内存块链表,寻找第一个满足用户需求的内存块。
- (6):得到该内存块起始地址。
- (7):若内存块未使用,且大小不小于用户需求大小加上
mem结构体大小,则满足需求。 - (8):若内存块可分割,通过起始地址与用户需求大小偏移,得到剩余内存起始块地址
ptr2。 - (9):将该地址后的内存空间作为分割后的新内存块
mem2,转换为mem结构体记录信息。 - (10):标记
mem2为未使用,插入空闲内存块链表。 - (11):标记被分配的内存块
mem为已使用状态。 - (12):若
mem2的下一个内存块不是链表最后一个,将其prev指向mem2。 - (13):若不可分割,直接标记分配的内存块为已使用。
- (14):若被分配的内存块是
lfree指向的,需重新为lfree赋值。 - (15):找到第一个低地址的空闲内存块。
- (16):将
lfree指向该内存块。 - (17):释放互斥量。
- (18):返回内存块可用起始地址,因块头需用
mem结构体保存信息。 - (19):若分配失败,释放互斥量并退出。
五、内存释放
内存释放操作相对简单,LwIP 根据用户释放的内存块地址,偏移mem结构体大小得到正确起始地址,依据mem中信息进行释放、合并等操作,将used字段清零表示未使用。
为防止内存碎片,LwIP 通过算法合并相邻空闲内存块,释放时若与上一个或下一个空闲内存块地址连续,则进行合并。源码如下:
void
mem_free(void *rmem)
{
struct mem *mem;
LWIP_MEM_FREE_DECL_PROTECT();
if (rmem == NULL) (1)
{
LWIP_DEBUGF(MEM_DEBUG | LWIP_DBG_TRACE |
LWIP_DBG_LEVEL_SERIOUS,
("mem_free(p == NULL) was called.\n"));
return;
}
if ((((mem_ptr_t)rmem) & (MEM_ALIGNMENT - 1)) != 0)
{
LWIP_MEM_ILLEGAL_FREE("mem_free: sanity check alignment");
LWIP_DEBUGF(MEM_DEBUG | LWIP_DBG_LEVEL_SEVERE,
("mem_free: sanity check alignment\n"));
/* protect mem stats from concurrent access */
MEM_STATS_INC_LOCKED(illegal);
return;
}
mem = (struct mem *)(void *)((u8_t *)rmem -
(SIZEOF_STRUCT_MEM + MEM_SANITY_OFFSET)); (2)
if ((u8_t *)mem < ram ||
(u8_t *)rmem + MIN_SIZE_ALIGNED > (u8_t *)ram_end) (3)
{
LWIP_MEM_ILLEGAL_FREE("mem_free: illegal memory");
LWIP_DEBUGF(MEM_DEBUG | LWIP_DBG_LEVEL_SEVERE,
("mem_free: illegal memory\n"));
/* protect mem stats from concurrent access */
MEM_STATS_INC_LOCKED(illegal);
return;
}
/* protect the heap from concurrent access */
LWIP_MEM_FREE_PROTECT();
/* mem has to be in a used state */
if (!mem->used) (4)
{
LWIP_MEM_ILLEGAL_FREE("mem_free: illegal \
memory: double free");
LWIP_MEM_FREE_UNPROTECT();
LWIP_DEBUGF(MEM_DEBUG | LWIP_DBG_LEVEL_SEVERE,
("mem_free: illegal memory: double free?\n"));
/* protect mem stats from concurrent access */
MEM_STATS_INC_LOCKED(illegal);
return;
}
if (!mem_link_valid(mem)) (5)
{
LWIP_MEM_ILLEGAL_FREE("mem_free: illegal memory:\
non-linked: double free");
LWIP_MEM_FREE_UNPROTECT();
LWIP_DEBUGF(MEM_DEBUG | LWIP_DBG_LEVEL_SEVERE,
("mem_free: illegal memory: non-linked: double free?\n"));
/* protect mem stats from concurrent access */
MEM_STATS_INC_LOCKED(illegal);
return;
}
/* mem is now unused. */
mem->used = 0; (6)
if (mem < lfree)
{
/*the newly freed struct is now the lowest */
lfree = mem; (7)
}
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); (8)
MEM_SANITY();
LWIP_MEM_FREE_UNPROTECT();
}
- (1):若释放的地址为空,直接返回。
- (2):对释放的地址进行偏移,得到真正内存块的起始地址。
- (3):判断内存块起始地址是否合法,不合法则返回。
- (4):若内存块未被使用,直接返回。
- (5):若内存块在链表中的连接不正常,返回。
- (6):将`used`置0,表示内存块已释放。
- (7):若刚释放的内存块地址比`lfree`指向的地址低,更新`lfree`指针。
- (8):调用`plug_holes()`函数尝试合并内存块,若新释放的内存块与上一个或下一个空闲内存块地址连续,则进行合并。
对内存释放函数的操作需格外小心,传递的参数必须是内存申请返回的地址,这样才能保证系统根据该地址找到内存块中的mem结构体,实现内存块释放及合并,否则可能打乱整个内存堆,产生内存碎片。
此外,用户申请内存后需及时释放,避免内存泄漏。内存泄漏指调用内存分配函数后,未及时或错误地释放内存。偶尔的操作影响不大,但周期性申请且不释放,会耗尽内存堆,导致系统死机等问题。
六、使用 C 库的 malloc 和 free 管理内存
LwIP 支持使用 C 标准库的malloc与free进行内存管理,当宏MEM_LIBC_MALLOC被定义时,编译器会编译相关代码,采用 C 标准库的函数。相关宏定义如下:
#if MEM_LIBC_MALLOC
void
mem_init(void)
{
}
void *
mem_trim(void *mem, mem_size_t size)
{
LWIP_UNUSED_ARG(size);
return mem;
}
#ifndef mem_clib_free
#define mem_clib_free free
#endif
#ifndef mem_clib_malloc
#define mem_clib_malloc malloc
#endif
#ifndef mem_clib_calloc
#define mem_clib_calloc calloc
#endif
#define MEM_LIBC_STATSHELPER_SIZE 0
#endif
若选择使用 C 标准库管理内存,void mem_init(void)和void* mem_trim(void *mem, mem_size_t size)函数无实际实现,ram_heap也不会被编译,用户申请的内存块在 C 标准库管理的系统堆中。这要求初始化整个 C 标准库并建立内存堆空间,因此不建议使用这种方式。
七、LwIP 中的配置
LwIP 中内存管理策略的选择由以下宏决定:
MEM_LIBC_MALLOC:决定是否使用 C 标准库自带的内存分配策略。默认值为 0,不使用,即使用 LwIP 提供的内存堆分配策略;定义为 1 时,使用 C 标准库自带策略。
当MEM_LIBC_MALLOC为 0 时,LwIP 的动态内存管理策略有两种实现形式:
-
通过内存堆 (HEAP) 管理策略(大数组)
-
通过内存池 (POOL) 管理策略(事先开辟的内存池)
-
MEMP_MEM_MALLOC:表示是否使用 LwIP 内存堆分配策略实现内存池分配(从内存池获取内存实际是从内存堆分配)。默认值为 0,内存池为独立内存实现,与MEM_USE_POOLS只能选其一。 -
MEM_USE_POOLS:表示是否使用 LwIP 内存池分配策略实现内存堆分配(从内存堆获取内存实际是从内存池分配)。默认值为 0,内存堆为独立内存实现,与MEMP_MEM_MALLOC只能选其一。
要使用内存池实现内存堆分配,需将MEM_USE_POOLS与MEMP_USE_CUSTOM_POOLS定义为 1,MEMP_MEM_MALLOC必须为 0,还需创建lwippools.h文件,添加初始化内存池的代码。
需注意内存池大小要依次增大,编译时编译器会将这些内存个数及大小添加到系统内存池。用户申请内存时,会选择最合适大小的内存块分配,若最匹配的内存池已用完,则选择更大的,可能浪费内存,但分配效率高,即 “以空间换时间”。
不同宏定义对应的内存分配策略如下表:
| MEMP_MEM_MALLOC | MEM_USE_POOLS | 内存分配策略 |
|---|---|---|
| 0 | 0 | 默认方式,内存池与内存堆独立实现 |
| 0 | 1 | 内存堆由内存池实现 |
| 1 | 0 | 内存池由内存堆实现 |
| 1 | 1 | 不允许 |

1501

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



