LwIP的内存管理(1)
主要内容参照5. LwIP的内存管理 — [野火]LwIP应用开发实战指南—基于野火STM32 文档,整理出来自用。
LwIP 本质上是对数据的处理,而网络中的数据量通常非常庞大,因此 LwIP 对这些数据的处理必然会消耗系统资源。一套优秀的内存管理策略就显得至关重要,内存分配策略、分配效率等都是衡量系统性能的重要因素。
1.1 几种内存分配策略
常见的内存分配策略主要有两种:一种是分配固定大小的内存块;另一种是利用内存堆进行动态分配,属于可变长度的内存块。这两种策略在 LwIP 中都会被用到,它们各有优劣,LwIP 的作者会根据不同的应用场景选择合适的策略,从而在系统内存开销、分配效率等方面取得较好的平衡。
此外,LwIP 还支持使用 C 标准库中的malloc
和free
进行内存分配,但这种方式并不推荐。因为在嵌入式设备中,C 标准库的这些函数存在诸多问题,例如每次调用的执行时间可能不一致,而内存分配中,分配时间的效率是至关重要的,这种不确定性可能会带来致命影响。
内存分配的本质是事先准备一大块内存堆(可以理解为一个巨大的数组),然后将该空间的起始地址返回给申请者。这就要求内核必须采用独有的数据结构来描述和记录哪些内存空间已被分配、哪些未被使用。基于不同的机制,衍生出了多种内存分配策略。
1.1.1 固定大小的内存块
采用固定大小的内存块分配策略时,用户只能申请大小固定的内存块。在内存初始化阶段,系统会将所有可用的内存区域划分为 N 块固定大小的内存,然后通过单链表将这些内存块连接起来。用户申请内存块时,直接从链表头部取出一个即可;释放时,也只需将内存块放回链表头部。这种方式的内存分配时间是固定的,效率非常高。
不过,其缺点也很明显:用户只能申请固定大小的内存块,如果该大小无法满足需求,申请就会失败;而如果将内存块调大,当用户需要极小内存时,又会造成浪费。
既然这种策略有明显缺陷,LwIP 作者为何还要使用?其实,LwIP 中存在很多固定大小的数据结构,如 TCP 首部、UDP 首部、IP 首部、以太网首部等,它们的大小是固定的。对于这些数据结构,采用固定大小内存块分配方式能极大提高效率,而且无论如何申请和释放,都不会产生内存碎片,保证系统稳定运行。这种分配策略在 LwIP 中被称为动态内存池分配策略,其示意图可参考图 5-1。
1.1.2 可变长度分配
可变长度分配策略在很多系统中都有应用。系统运行时,各个空闲内存块的大小不固定,会随着用户的申请和释放而变化。初始时,系统是一块大的内存堆,随着运行,内存块的大小和数量都会发生改变。这种策略有多种不同的算法,具体可参考操作系统相关书籍。
LwIP 中采用First Fit
(首次拟合)内存管理算法来实现可变长度分配。申请内存时,只要找到一个比请求内存大的空闲块,就从中切割出合适的部分,剩余部分返回给动态内存堆。这种策略对分配的内存块大小有要求,请求的分配大小不能小于MIN_SIZE
(通常为 12 字节),否则会分配MIN_SIZE
大小的空间。在这 12 字节中,前几个字节用于存放内存分配器管理的私有数据,用户程序不能修改,否则可能导致致命错误。
内存释放的过程则相反,分配器会检查该节点前后相邻的内存块是否空闲,若空闲则合并成一个大的空闲块。这种内存堆分配方式,申请和释放时会消耗一定时间,可看作是以时间换空间的策略。其优点是内存浪费小、实现简单,适合小内存管理;缺点是频繁的动态分配和释放可能导致严重的内存碎片。当碎片严重时,即使系统还有可用内存,但由于都是不连续的小内存块,无法满足用户对较大内存块的申请,可能导致系统崩溃。
当然,LwIP 也支持 C 标准库的malloc()
和free()
,但如前所述,不建议使用.
1.2 动态内存池(POOL)
动态内存池要求申请的内存大小必须是指定的固定字节值(如 4、8、16 等)。系统将所有可用区域按固定大小的字节单位划分,并用单链表连接所有空闲内存块。链表中所有节点大小相同,分配和释放操作都非常简单。
LwIP 源文件中的memp.c
和memp.h
实现了动态内存池分配策略。LwIP 需要内存池,是因为协议栈中有大量固定长度的协议首部,预先为它们分配固定内存,后续处理时直接使用,无需重新分配,能提高效率和便利性。
1.2.1 内存池的预处理
内核初始化时,会根据宏定义的配置,将所有可用区域按固定大小单位划分,并用单链表连接所有空闲块,组成一个个内存池。由于链表中所有节点大小相同,分配时无需查找,直接取出第一个节点的空间即可。
内核初始化内存池时,依赖用户配置的宏定义。例如,用户定义LWIP_UDP
宏后,编译器会编译与 UDP 协议控制块相关的数据结构,包含LWIP_MEMPOOL(UDP_PCB, MEMP_NUM_UDP_PCB, sizeof(struct udp_pcb),"UDP_PCB")
,初始化时就会初始化 UDP 协议控制块所需的 POOL 资源,其数量由MEMP_NUM_UDP_PCB
宏定义决定。不同协议的 POOL 内存块大小不同,由协议性质决定,如 UDP 协议控制块的内存块大小是sizeof(struct udp_pcb)
,TCP 协议控制块的则为sizeof(struct tcp_pcb)
。通过这种方式,能将用户配置的宏定义功能所需的 POOL 包含进来,简化编程。
memp_std.h
文件(位于include/lwip/priv
目录下)全是宏定义,其设计目的是为了方便使用。在不同地方调用#include "lwip/priv/memp_std.h"
,配合外部提供的不同LWIP_MEMPOOL
宏值,经过编译器预处理后会产生不同结果。
该文件中的宏定义全部依赖LWIP_MEMPOOL(name,num,size,desc)
宏,外部提供的宏值不同,包含该文件的源文件预处理结果就不同。通过在不同地方多次包含该文件,并提前提供MEMPOOL
宏值,可实现多样化的处理。
例如,代码清单展示了memp_std.h
的使用方式:
typedef enum
{
#define LWIP_MEMPOOL(name,num,size,desc) MEMP_##name,
#include "lwip/priv/memp_std.h"MEMP_MAX
} memp_t;
其中,#define LWIP_MEMPOOL(name,num,size,desc) MEMP_##name,
利用 C 语言的连接符##
(用于将两个 Token 连接为一个 Token),将LWIP_MEMPOOL(EmbedFire,num,size,desc)
替换为MEMP_EmbedFire,
。经过编译器处理后,上述枚举会变成如代码清单所示的内容(假设所有宏定义都使能):
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;
memp_t
类型在内存池管理中至关重要,通过内存池申请函数申请内存时,唯一的参数就是memp_t
类型,它指定从哪种类型的 POOL 中分配内存块,从而管理系统中所有类型的 POOL。
MEMP_MAX
不代表任何 POOL 类型,仅记录系统中 POOL 的数量。例如,上述例子中MEMP_RAW_PCB
值为 0,MEMP_MAX
值为 9,表示当前系统有 9 种 POOL。
需要注意的是,memp_std.h
文件最后需用#undef LWIP_MEMPOOL
撤销该宏定义,因为该文件可能被多个地方调用,每个调用处可能会重新定义该宏的功能。
此外,在查看 LwIP 源码时,若发现某个标识符找不到定义但编译无问题,很可能是通过##
连接符产生的宏定义,例如MEMP_RAW_PCB
的定义就在memp_t
类型中。
按照这种包含头文件的原理,定义LWIP_MEMPOOL
宏的作用,可产生与内存池相关的多种操作。例如,memp.c
文件开头定义:
#define LWIP_MEMPOOL(name,num,size,desc) LWIP_MEMPOOL_DECLARE(name,num,size,desc)
#include "lwip/priv/memp_std.h"
经过预处理后,会得到如代码清单中加粗部分所示的结果(具体细节无需深入理会,简单了解即可)。
以RAW_PCB
为例,经过转换后会得到相关的内存定义、结构体等,这些结构体(如memp_desc memp_RAW_PCB
)记录了该 POOL 的内存对齐后大小、内存块个数、内存池区域起始地址等重要信息。其中,真正的内存池区域由u8_t memp_memory_XXXX_base
定义,本质上是一个数组,知道其起始地址就能进行操作。
1.2.2 内存池的初始化
LwIP 协议栈初始化时,memp_init()
函数会对内存池进行初始化,真正执行初始化操作的是memp_init_pool()
函数,其源码如代码清单所示(已删减):
void memp_init(void)
{u16_t i;for (i = 0; i < LWIP_ARRAYSIZE(memp_pools); i++){memp_init_pool(memp_pools[i]);}
}void memp_init_pool(const struct memp_desc *desc)
{int i;struct memp *memp;*desc->tab = NULL;memp = (struct memp *)LWIP_MEM_ALIGN(desc->base);memset(memp, 0, (size_t)desc->num * (MEMP_SIZE + desc->size));for (i = 0; i < desc->num; ++i){memp->next = *desc->tab;*desc->tab = memp;memp = (struct memp *)(void *)((u8_t *)memp + MEMP_SIZE + desc->size);}
}
该函数的功能较为简单,根据每种 POOL 的memp_desc
描述进行初始化,将每种类型 POOL 中的空闲内存块连接成单链表,并使用memset()
函数将其内容清零。
1.2.3 内存分配
内存池初始化后,用户可通过memp_malloc
函数申请内存块。内存块的大小是固定的,申请时需指定内存池类型(因为不同类型 POOL 的内存块大小不同)。系统中所有内存池类型记录在memp_pools
数组(内存池描述表)中,通过它能快速找到对应的内存池进行分配。其源码如代码清单所示(已删减):
void *memp_malloc(memp_t type)
{void *memp;LWIP_ERROR("memp_malloc: type < MEMP_MAX", (type < MEMP_MAX), return NULL;);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;LWIP_ASSERT("memp_malloc: memp properly aligned", ((mem_ptr_t)memp % MEM_ALIGNMENT) == 0);SYS_ARCH_UNPROTECT(old_level);return ((u8_t *)memp + MEMP_SIZE);}else{SYS_ARCH_UNPROTECT(old_level);LWIP_DEBUGF(MEMP_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("memp_malloc: out of memory in pool %s\n", desc->desc));}return NULL;
}
内存池申请函数的核心是memp = *desc->tab;
,通过该语句直接获取对应内存块中的第一个空闲内存块,然后移动*desc->tab
指针指向下一个空闲内存块,最后返回((u8_t *)memp + MEMP_SIZE)
。其中,MEMP_SIZE
是内存块中用于存储管理信息的空间偏移(其值为(LWIP_MEM_ALIGN_SIZE(sizeof(struct memp)) + MEM_SANITY_REGION_BEFORE_ALIGNED)
),用户无需关注,只需知道返回的地址是可直接使用的即可,偏移部分为内存分配器管理空间,用户不可修改。
1.2.4 内存释放
内存释放函数同样简单,只需将使用完毕的内存块添加到对应内存池的空闲内存块链表中。释放时需要两个参数:POOL 类型和内存块起始地址,源码如代码清单 所示(已删减):
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);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);
}