【LwIP】02——内存管理
LwIP主要使用动态内存池(pool)和动态内存堆(heap)来进行内存管理。
谈及内存管理,常见的分配策略有两种,一种使用固定大小的内存分配策略,也就是说,用户每次申请都只能申请固定长度的内存;另外一种则是使用堆进行动态分配,这种方式可以分配出不同长度的内存。在LwIP中,两种方式都在特定的场景被使用,除此之外,还支持使用C库的malloc/free进行内存分配,不过这种方式并不建议使用,因为每次分配的时间开销并不确定,这会引发很多问题。
一、动态内存池(Pool)
一种申请内存大小必须是固定大小字节的分配策略,系统将可用区域以固定大小进行分割,可用的内存使用一个简单的链表连接起来,由于每个内存块大小都一样,需要分配时直接取出第一个交给用户就行了。
在LwIP中,为什么要使用动态内存池?因为协议栈里面有许多协议,每个协议的首部长度往往是固定的,使用pool,可以预先为这些固定长度的协议首部分配内存,以后每次需要处理协议首部的时候,都可以直接使用这段内存。
1、动态内存池的预处理
LwIP定义Pool的方式非常的有意思,首先我们关注一个文件,memp_std.h,位于include/lwip/priv这个目录下,这里我们关注如下这些宏定义
memp_std.h代码(部分)
#if LWIP_RAW
LWIP_MEMPOOL(RAW_PCB, MEMP_NUM_RAW_PCB, sizeof(struct raw_pcb), "RAW_PCB")
#endif /* LWIP_RAW */
#if LWIP_UDP
LWIP_MEMPOOL(UDP_PCB, MEMP_NUM_UDP_PCB, sizeof(struct udp_pcb), "UDP_PCB")
#endif /* LWIP_UDP */
#if LWIP_TCP
LWIP_MEMPOOL(TCP_PCB, MEMP_NUM_TCP_PCB, sizeof(struct tcp_pcb), "TCP_PCB")
LWIP_MEMPOOL(TCP_PCB_LISTEN, MEMP_NUM_TCP_PCB_LISTEN, sizeof(struct tcp_pcb_listen), "TCP_PCB_LISTEN")
LWIP_MEMPOOL(TCP_SEG, MEMP_NUM_TCP_SEG, sizeof(struct tcp_seg), "TCP_SEG")
#endif /* LWIP_TCP */
#if LWIP_ALTCP && LWIP_TCP
LWIP_MEMPOOL(ALTCP_PCB, MEMP_NUM_ALTCP_PCB, sizeof(struct altcp_pcb), "ALTCP_PCB")
#endif /* LWIP_ALTCP && LWIP_TCP */
#if LWIP_IPV4 && IP_REASSEMBLY
LWIP_MEMPOOL(REASSDATA, MEMP_NUM_REASSDATA, sizeof(struct ip_reassdata), "REASSDATA")
#endif /* LWIP_IPV4 && IP_REASSEMBLY */
#if (IP_FRAG && !LWIP_NETIF_TX_SINGLE_PBUF) || (LWIP_IPV6 && LWIP_IPV6_FRAG)
LWIP_MEMPOOL(FRAG_PBUF, MEMP_NUM_FRAG_PBUF, sizeof(struct pbuf_custom_ref),"FRAG_PBUF")
#endif /* IP_FRAG && !LWIP_NETIF_TX_SINGLE_PBUF || (LWIP_IPV6 && LWIP_IPV6_FRAG) */
#if LWIP_NETCONN || LWIP_SOCKET
LWIP_MEMPOOL(NETBUF, MEMP_NUM_NETBUF, sizeof(struct netbuf), "NETBUF")
LWIP_MEMPOOL(NETCONN, MEMP_NUM_NETCONN, sizeof(struct netconn), "NETCONN")
#endif /* LWIP_NETCONN || LWIP_SOCKET */
观察上面的代码会发现,根据使能的协议、配置项不同,使用LWIP_MEMPOOL()这个宏的数量也不同,通过名称可以得出,这个宏似乎和内存池有着很强的联系,我们接着看以下的代码。
memp.h代码(部分)
typedef enum
{
#define LWIP_MEMPOOL(name,num,size,desc) MEMP_##name,
#include "lwip/priv/memp_std.h"
MEMP_MAX
} memp_t;
结合这两段代码,我们可以发现,LWIP_MEMPOOL这个宏似乎用于定义这个枚举,当将所有编译选项/协议宏使能,预编译完成后,这个枚举类型应该是下面这样
memp.h代码(预编译完成后)
typedef enum
{
MEMP_RAW_PCB,
MEMP_UDP_PCB,
MEMP_TCP_PCB,
MEMP_TCP_PCB_LISTEN,
MEMP_TCP_SEG,
MEMP_ALTCP_PCB,
MEMP_REASSDATA,
MEMP_NETBUF,
MEMP_NETCONN,
MEMP_MAX
} memp_t;
当然,事情似乎并没有如此简单,在协议栈中,我们会发现,在多处都会定义一次LWIP_MEMPOOL,并且每次在定义的地方,都会使用 #include "lwip/priv/memp_std.h",例如
文件memp.c(开头部分)
#define LWIP_MEMPOOL(name,num,size,desc) LWIP_MEMPOOL_DECLARE(name,num,size,desc)
#include "lwip/priv/memp_std.h"
如果将宏全部展开,和我们前面发现的枚举似乎时两码事了。这便是LwIP设计的高明之处,memp_std.h这个文件像是一个模板,上面列举了我们目前需要准备的东西,例如UDP的pcb内存池里一个单元要多大,要几个,名称是啥。当包含这个头文件时,根据这些信息,制定不同的功能。例如memp.h用它来定义枚举类型,memp.c用它来制定不同协议的pcb内存,这样做虽然阅读起来可能会有点费劲。但是可以保证配置的简洁性,提示编译的效率。
例如前面的memp.c展开后,例如
LWIP_MEMPOOL(RAW_PCB, MEMP_NUM_RAW_PCB, sizeof(struct raw_pcb), "RAW_PCB")
可能就会变成(上述配置不同,结构体的参数也会不同,整个内存的长度也是不同的)
u8_t memp_memory_RAW_PCB_base[((4 * 24) + 4 - 1U)];
static struct memp *memp_tab_RAW_PCB;
const struct memp_desc memp_RAW_PCB ={
((24) + 4 - 1U) & ~(4-1U)),
(24),
4
memp_memory_RAW_PCB_base,
&memp_tab_RAW_PCB
};
其中struct memp_desc的成员如下
/** Memory pool descriptor */
struct memp_desc {
/* 内存池中每个块的大小 */
u16_t size;
/* 内存池中有多少个块 */
u16_t num;
/** 内存的起始地址 */
u8_t *base;
/**第一个空闲的块 */
struct memp **tab;
};
内存池单元块的描述结构体如下:
struct memp {
struct memp *next;
};
2、动态内存池的初始化
memp_init(),实际上初始化的函数是memp_init_pool(),精简代码如下
void
memp_init(void)
{
u16_t i;
/* for every pool: */
for (i = 0; i < LWIP_ARRAYSIZE(memp_pools); i++) {
memp_init_pool(memp_pools[i]);
}
这里的memp_pools也是用上一章节提到的方式添加的,它是一个由struct *memp_desc组成的数组,前面提到的 const struct memp_desc memp_RAW_PCB 的地址就是其中一员 。
memp_init_pool的作用就是将整个内存分成desc->num个,每个大小是desc->size,并将每个块都有一个next指针指向下一个块,第一个可用的块memp_desc->tab放在内存的末尾。

3、动态内存池的使用
[1] 动态内存池的分配
memp.c中,删减掉不必要的代码后
void *
memp_malloc(memp_t type)
{
void *memp;
memp = do_memp_malloc_pool(memp_pools[type]);
return memp;
}
static void *
do_memp_malloc_pool(const struct memp_desc *desc)
{
struct memp *memp;
SYS_ARCH_DECL_PROTECT(old_level);
memp = *desc->tab;
if (memp != NULL) {
*desc->tab = memp->next;
SYS_ARCH_UNPROTECT(old_level);
return ((u8_t *)memp + MEMP_SIZE);
} else {
SYS_ARCH_UNPROTECT(old_level);
}
return NULL;
}
可以发现,分配过程非常的简单直接,从desc中取出当前可用的内存块(desc->tab),然后移动desc->tab到memp的next,返回memp给用户即可
[2] 动态内存池的释放
memp.c中,删减掉不必要的代码后
void
memp_free(memp_t type, void *mem)
{
LWIP_ERROR("memp_free: type < MEMP_MAX",
(type < MEMP_MAX), return;);
if (mem == NULL)
{
return;
}
do_memp_free_pool(memp_pools[type], mem);
}
static void
do_memp_free_pool(const struct memp_desc *desc, void *mem)
{
struct memp *memp;
SYS_ARCH_DECL_PROTECT(old_level);
LWIP_ASSERT("memp_free: mem properly aligned",
((mem_ptr_t)mem % MEM_ALIGNMENT) == 0);
/* cast through void* to get rid of alignment warnings */
memp = (struct memp *)(void *)((u8_t *)mem - MEMP_SIZE); (1)
SYS_ARCH_PROTECT(old_level);
memp->next = *desc->tab; (2)
*desc->tab = memp; (3)
SYS_ARCH_UNPROTECT(old_level);
}
可以看出,释放也是很高效的,直接将内存找到,重新建立链表(修改当前的等待释放的memp的next为tab),然后让这段memp变成tab。意思就是将这段内存释放后,下次分配时,这段内存会是第一个被分配的。
二、内存堆(heap)
动态内存堆,可以申请任意大小的内存。有两种方式,一种是使用C库提供的Malloc/Free;另外一种是LwIP自身的内存堆管理策略。使用宏MEM_LIBC_MALLOC进行选择,宏值为1时,使用C库提供的内存分配接口。
1、动态内存堆的组织部分
struct mem {
/*下一个内存块在内存堆中的索引*/
mem_size_t next;
/*上一个内存块在内存堆中的索引*/
mem_size_t prev;
/* 当前内存块是否被使用 */
u8_t used;
};
/*申请的最小内存(需要大于struct mem这个结构)*/
#define MIN_SIZE 12
/*内存堆的定义 (用户可分配的(MEM_SIZE_ALIGNED) + 2个struct mem(最开始和结尾)的大小)*/
/*堆的名字叫heap*/
LWIP_DECLARE_MEMORY_ALIGNED(ram_heap, MEM_SIZE_ALIGNED+(2U*SIZEOF_STRUCT_MEM));
/*使用宏进行重命名*/
#define LWIP_RAM_HEAP_POINTER ram_heap
/*堆的起始地址*/
static u8_t *ram;
/*指向最后一个内存块的mem结构*/
static struct mem *ram_end;
/*内存保护锁*/
static sys_mutex_t mem_mutex;
/*mem类型指针,指向内存堆中低地址的空闲内存块*/
static struct mem * LWIP_MEM_LFREE_VOLATILE lfree;
2、动态内存堆的初始化
直接看代码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);
/* 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 = ptr_to_mem(MEM_SIZE_ALIGNED);
ram_end->used = 1;
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;
MEM_STATS_AVAIL(avail, MEM_SIZE_ALIGNED);
if (sys_mutex_new(&mem_mutex) != ERR_OK) {
LWIP_ASSERT("failed to create mem_mutex", 0);
}
}
可以看出,上述代码就是在对上一节说的全局变量进行初始化。初始化完成后,内存堆就变成下面这个样子了

3、动态内存堆使用
[1]内存堆的内存分配
分配过程中,首先将用户申请的大小(size_in)进行对齐。对齐后,如果小于MIN_SIZE_ALIGNED,则设为最小的默认值,如果超出堆的大小,直接返回错误。
if (size_in == 0) {
return NULL;
}
size = (mem_size_t)LWIP_MEM_ALIGN_SIZE(size_in);
if (size < MIN_SIZE_ALIGNED) {
size = MIN_SIZE_ALIGNED;
}
if ((size > MEM_SIZE_ALIGNED) || (size < size_in)) {
return NULL;
}
接着是遍历整个堆,通过mem结构进行查询,当发现mem没被使用(mem->used == 0)且下一个mem块和当前mem块的间隔满足用户需求的内存块大小+mem结构大小,开始尝试从mem开始分配内存。
此时会有两种情况,一种是当前mem开始到mem->next之间的内存还有很大,此时需要分割,分割的方式就是在内存分配给用户后,在mem到mem->next中间再插入一个mem2,相当于mem2->next = mem->next; mem->next = mem2,当然,如果mem2的next此时并不是堆的末尾,那么说明那里有一个有效的mem,那么那个mem的prev应该是mem2。
当然,也有可能这段内存没有多余的部分,那么把mem标记成已经使用(mem->used = 1)就可以了。
if (mem->next - (ptr + SIZEOF_STRUCT_MEM) >=
(size + SIZEOF_STRUCT_MEM + MIN_SIZE_ALIGNED))
{
ptr2 = (mem_size_t)(ptr + SIZEOF_STRUCT_MEM + size);
LWIP_ASSERT("invalid next ptr",ptr2 != MEM_SIZE_ALIGNED);
/* create mem2 struct */
mem2 = ptr_to_mem(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)
{
ptr_to_mem(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_to_ptr(mem));
}
在分配完成后,如果我们分配出去的内存堆是lfree,我们也应该更新lfree,方便下一次来分配内存。更新lfree的方法就是,直接从低地址遍历整个堆,找到第一个空闲的块。
例如分配一个24字节后的堆如下图所示:

[2]内存堆的内存释放
内存释放的操作也是比较简单的,根据用户需要释放的内存地址找到mem结构,将mem保存的内存块信息进行释放、合并,并将used字段清0,表示该段内存没被使用。
具体是这样做的,首先也会对传入的待释放内存进行检查,需要检查的点是内存是否为空、内存是否未对齐、内存块有没有被使用、内存块的prev和next正常不。如果上述有一个不正常,函数都直接返回。
检查完成后,将mem->used清0,调用plug_holes进行内存合并。
具体代码如下:
void
mem_free(void *rmem)
{
struct mem *mem;
LWIP_MEM_FREE_DECL_PROTECT();
if (rmem == NULL) {
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;
}
/* Get the corresponding struct mem: */
/* cast through void* to get rid of alignment warnings */
mem = (struct mem *)(void *)((u8_t *)rmem - (SIZEOF_STRUCT_MEM + MEM_SANITY_OFFSET));
if ((u8_t *)mem < ram || (u8_t *)rmem + MIN_SIZE_ALIGNED > (u8_t *)ram_end) {
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) {
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)) {
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;
if (mem < lfree) {
/* the newly freed struct is now the lowest */
lfree = mem;
}
/* finally, see if prev or next are free also */
plug_holes(mem);
LWIP_MEM_FREE_UNPROTECT();
}
static void
plug_holes(struct mem *mem)
{
struct mem *nmem;
struct mem *pmem;
LWIP_ASSERT("plug_holes: mem >= ram", (u8_t *)mem >= ram);
LWIP_ASSERT("plug_holes: mem < ram_end", (u8_t *)mem < (u8_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 && (u8_t *)nmem != (u8_t *)ram_end) {
/* if mem->next is unused and not end of ram, combine mem and mem->next */
if (lfree == nmem) {
lfree = mem;
}
mem->next = nmem->next;
if (nmem->next != MEM_SIZE_ALIGNED) {
ptr_to_mem(nmem->next)->prev = mem_to_ptr(mem);
}
}
/* plug hole backward */
pmem = ptr_to_mem(mem->prev);
if (pmem != mem && pmem->used == 0) {
/* if mem->prev is unused, combine mem and mem->prev */
if (lfree == mem) {
lfree = pmem;
}
pmem->next = mem->next;
if (mem->next != MEM_SIZE_ALIGNED) {
ptr_to_mem(mem->next)->prev = mem_to_ptr(pmem);
}
}
}
三、LwIP中的配置
1、是否使用LibC提供的Malloc实现堆,通过下面这个宏来决定,该值默认情况下为0,表示不使用C 标准库自带的内存分配策略。即默认使用LwIP提供的内存堆分配策略。如果要使用C标准库自带的分配策略,则需要把该值定义为 1
MEM_LIBC_MALLOC
2、使用内存堆来实现内存池分配(要从内存池分配时,其实是从内存堆进行分配),使用下面这个宏来决定,默认为0
MEMP_MEM_MALLOC
3、使用内存池来实现内存堆(要从内存堆中获取内存时,实际是从内存池中分配),使用下面这个宏来决定,默认为0
MEM_USE_POOLS
要使用这个功能,除了使能这个宏,还需要将MEMP_USE_CUSTOM_POOLS定义为1,并且在lwippools.h文件中定义一些内存池,例如
LWIP_MALLOC_MEMPOOL_START
LWIP_MALLOC_MEMPOOL(20, 256) //定义一个内存池,里面有20个块,每个块256个字节
LWIP_MALLOC_MEMPOOL(10, 512)
LWIP_MALLOC_MEMPOOL(5, 1512)
LWIP_MALLOC_MEMPOOL_END
上述提到的 MEMP_MEM_MALLOC 和 MEM_USE_POOLS 不可以同时被置1。