LwIP入门实战 — 5 LwIP 的内存管理
目录
5.1 LwIP 的内存管理策略
5.2 固定大小的内存块
5.2.1 内存池概念
5.2.2 memp_t枚举
5.2.3 memp结构体
5.2.4 内存池的配置
5.2.5 初始化流程
5.2.6 分配与释放操作
5.3 可变长度分配
5.3.1 内存池概念
5.3.2 First Fit内存管理算法
5.3.3 LwIP动态内存堆实现
5.1 LwIP 的内存管理策略
LwIP 采用两种内存分配策略并按需选用:一是固定大小内存块分配,通过预划分多个固定尺寸内存池,分配释放高效 且低碎片风险,适用于 TCP 控制块等大小固定的数据;二是动态内存堆分配,基于连续内存堆按需分配可变长度块,内存利用率高但效率受堆碎片化影响,适配变长应用数据的场景。
此外 LwIP 还支持使用 C 标准库中的 malloc和 free 进行内存分配,但是这种内存分配我们不建议使用,因为 C 标准库在嵌入式设备中使用会有很多问题,系统每次调用这些函数执行的时间可能都不一样。对于内存分配(malloc
),其本质是从堆中寻找一块大小合适的空闲内存块。由于堆中内存块的大小、分布和使用状态是动态变化的(随频繁的分配 / 释放操作变得碎片化),分配时需要遍历空闲块链表,根据特定策略(如首次适配、最佳适配等)查找满足需求的块。若当前堆中存在大片连续空闲内存,可能很快找到匹配块;若内存碎片化严重(存在大量零散小空闲块),则需要遍历更多节点才能找到合适的块,甚至可能需要对空闲块进行分割(将大空闲块拆分为所需大小和剩余部分),这会显著增加操作耗时。因此,分配时间会随堆的实时状态(空闲块分布、碎片化程度)而波动。
5.2 固定大小的内存块
5.2.1 内存池概念
使用固定大小的内存块分配策略,用户只能申请大小固定的内存块,在内存初始化的时候,系统会将所有可用的内存区域划分为 N 块固定大小的内存,然后将这些内存块通过单链表的方式连接起来,用户在申请内存块的时候就直接从链表的头部取出一个内存块进行分配,同理释放内存块的时候也是很简单,直接将内存块释放到链表的头部即可,这样子分配内存的时间就是固定的,非常高效。但是缺点也是很明显的,用户只能申请固定大小的内存块,如果内存块无法满足用户的需求,那么则无法申请成功,而如果将内存块大小变大,那么在用户需要极小的内存的时候就会造成内存的浪费。
可能会有人问了,那这种内存分配策略不好用,为什么 LwIP 作者会使用呢?其实不然, LwIP 中有很多固定的数据结构空间,如 TCP 首部、 UDP 首部, IP 首部,以太网首部等都是固定的数据结构,其大小就是一个固定的值,那么我们就能采用这种方式分配这些固定大小的内存空间,这样子的效率就会大大提高,并且无论怎么申请与释放,都不会产生内存碎片,这就让系统能很稳定地运行。这种分配策略在 LwIP 中被称之为动态内存池分配策略。内存池示意图具体见图。
内存池采用 “预分配固定大小内存块” 的策略。在协议栈初始化时,lwIP 会根据配置创建多个不同大小的内存池,每个池包含若干个相同大小的内存块(如用于存储 TCP 控制块的tcp_pcb
、UDP 控制块的udp_pcb
、网络数据包的pbuf
等)。这种设计的核心优势是分配 / 释放速度快(无需复杂计算,仅需操作链表标记空闲块)和无内存碎片(块大小固定,不会产生细碎空闲空间)。
内存池的管理通过memp.h
/memp.c
实现,关键函数包括memp_malloc(pool)
(从指定池分配一块内存)和memp_free(pool, ptr)
(释放内存块回原池)。用户可通过MEMP_NUM_*
系列宏(如MEMP_NUM_TCP_PCB
)配置各类型内存块的数量,平衡资源占用与功能需求。
5.2.2 memp_t枚举
memp_t枚举定义所有内存池的类型,每种类型对应一种特定对象,下面的写法值得我们学习。
typedef enum {
#define LWIP_MEMPOOL(name,num,size,desc) MEMP_##name,
#include "lwip/priv/memp_std.h"MEMP_MAX
} memp_t;
在枚举内部先定义一个宏,这个宏的作用是:将传入的name
参数拼接上MEMP_
前缀,再添加逗号(枚举值的语法要求)。例如,若调用LWIP_MEMPOOL(pbuf, ...)
,会被展开为MEMP_pbuf,
。
#define LWIP_MEMPOOL(name,num,size,desc) MEMP_##name,
#include "lwip/priv/memp_std.h"
会引入 LwIP 预定义的所有内存池配置。该文件的内容类似一系列LWIP_MEMPOOL
宏调用,例如:
// memp_std.h 中的部分内容
LWIP_MEMPOOL(PBUF, MEMP_NUM_PBUF, sizeof(struct pbuf), "PBUF")
LWIP_MEMPOOL(TCP_PCB, MEMP_NUM_TCP_PCB, sizeof(struct tcp_pcb), "TCP_PCB")
LWIP_MEMPOOL(UDP_PCB, MEMP_NUM_UDP_PCB, sizeof(struct udp_pcb), "UDP_PCB")
LWIP_MEMPOOL(ARP_TABLE, MEMP_NUM_ARP_TABLE, sizeof(struct etharp_entry),"ARP_TABLE")
// ... 其他内存池定义
当memp_std.h
被包含时,其中的每个LWIP_MEMPOOL
宏都会被前面定义的临时宏替换,最终生成枚举值列表:
typedef enum {MEMP_PBUF,MEMP_TCP_PCB,MEMP_UDP_PCB,MEMP_ARP_TABLE,// ... 其他内存池对应的枚举值MEMP_MAX
} memp_t;
5.2.3 memp结构体
struct memp内存池的元数据结构,记录每个内存池的配置(块大小、块数量、空闲块链表等)。c运行
struct memp {const char *name; // 内存池名称(调试用)u16_t size; // 每个块的大小(字节)u16_t num; // 块的总数量struct memp_block *free; // 空闲块链表头// ... 其他信息(如内存块起始地址)
};
5.2.4 内存池的配置
LwIP 的内存池大小和数量通过lwipopts.h
中的宏定义配置,可根据实际需求调整,例如:
// lwipopts.h
#define MEMP_NUM_PBUF 100 // 支持100个pbuf块
#define MEMP_NUM_TCP_PCB 20 // 支持20个TCP连接
#define MEMP_NUM_UDP_PCB 10 // 支持10个UDP套接字
#define MEMP_NUM_ARP_TABLE 10 // ARP缓存最多10个条目
5.2.5 初始化流程
memp_init
是 LwIP 中固定大小内存块分配策略的初始化入口,通过遍历所有内存池类型,调用memp_init_pool
为每个内存池建立空闲块链表,完成内存池的初始化;同时根据配置,可启用内存池统计功能和二级溢出检查
/*** 初始化lwIP内置的内存池。* 相关函数:memp_malloc(分配内存块)、memp_free(释放内存块)** 功能:将memp_memory内存区域分割为对应每种内存池类型的链表结构,* 使内存池可用于后续的块分配与释放操作。*/
void
memp_init(void)
{u16_t i; // 循环计数器/* 遍历所有内存池类型 */for (i = 0; i < LWIP_ARRAYSIZE(memp_pools); i++) {// 初始化第i个内存池,建立其内部的空闲块链表memp_init_pool(memp_pools[i]);#if LWIP_STATS && MEMP_STATS // 若开启内存池统计功能// 将第i个内存池的统计信息关联到全局统计结构lwip_stats.memp[i] = memp_pools[i]->stats;
#endif}#if MEMP_OVERFLOW_CHECK >= 2 // 若开启二级内存溢出检查/* 首次检查所有内存池,验证初始化是否成功 */memp_overflow_check_all();
#endif /* MEMP_OVERFLOW_CHECK >= 2 */
}
5.2.6 分配与释放操作
memp_malloc(memp_t type)
根据指定的内存池类型(memp_t
枚举值),从对应内存池的空闲链表中取出一个内存块,返回指向该块用户数据区的指针(跳过struct memp
头部);若池中空闲块耗尽则返回NULL
,分配过程仅涉及链表指针操作,速度快且无碎片。
/*** 从指定内存池分配一个内存块* @param type 内存池类型(memp_t枚举值)* @param file (条件参数)分配操作所在的源文件名(仅MEMP_OVERFLOW_CHECK开启时有效)* @param line (条件参数)分配操作所在的行号(仅MEMP_OVERFLOW_CHECK开启时有效)* @return 成功返回指向内存块数据区的指针,失败返回NULL*/
void *
#if !MEMP_OVERFLOW_CHECK // 未开启内存溢出检查时,函数无file和line参数
memp_malloc(memp_t type)
#else // 开启溢出检查时,需要传入文件名和行号用于调试
memp_malloc_fn(memp_t type, const char *file, const int line)
#endif
{void *memp; // 用于存储分配到的内存块指针// 断言检查:确保内存池类型合法(小于MEMP_MAX)LWIP_ERROR("memp_malloc: type < MEMP_MAX", (type < MEMP_MAX), return NULL;);#if MEMP_OVERFLOW_CHECK >= 2 // 若溢出检查等级为2及以上,先检查所有内存块是否溢出memp_overflow_check_all(); // 全局溢出检查(调试用,代价较高)
#endif /* MEMP_OVERFLOW_CHECK >= 2 */// 根据是否开启溢出检查,调用不同的内部分配函数
#if !MEMP_OVERFLOW_CHECK// 从指定内存池(memp_pools[type])分配内存块memp = do_memp_malloc_pool(memp_pools[type]);
#else// 带调试信息(file/line)的分配函数memp = do_memp_malloc_pool_fn(memp_pools[type], file, line);
#endifreturn memp; // 返回分配到的内存块(数据区指针)
}/*** 内部函数:实际执行内存池分配操作* @param desc 内存池描述符(包含空闲链表、块大小、统计信息等)* @param file (条件参数)源文件名(仅MEMP_OVERFLOW_CHECK开启时有效)* @param line (条件参数)行号(仅MEMP_OVERFLOW_CHECK开启时有效)* @return 成功返回数据区指针,失败返回NULL*/
static void *
#if !MEMP_OVERFLOW_CHECK
do_memp_malloc_pool(const struct memp_desc *desc)
#else
do_memp_malloc_pool_fn(const struct memp_desc *desc, const char *file, const int line)
#endif
{struct memp *memp; // 指向内存块头部(struct memp)SYS_ARCH_DECL_PROTECT(old_level); // 临界区保护变量#if MEMP_MEM_MALLOC // 若配置为从堆分配内存块(而非预分配池)// 通过mem_malloc(堆分配)获取内存,大小为:memp头部大小 + 对齐后的用户数据区大小memp = (struct memp *)mem_malloc(MEMP_SIZE + MEMP_ALIGN_SIZE(desc->size));SYS_ARCH_PROTECT(old_level); // 进入临界区(保护后续操作)
#else /* MEMP_MEM_MALLOC */ // 默认:从预分配的内存池链表中获取SYS_ARCH_PROTECT(old_level); // 进入临界区(防止多线程竞争空闲链表)// 从内存池的空闲链表头部取一个块(desc->tab是空闲链表头指针的地址)memp = *desc->tab;
#endif /* MEMP_MEM_MALLOC */// 若成功获取到内存块if (memp != NULL) {
#if !MEMP_MEM_MALLOC // 预分配池模式下的操作
#if MEMP_OVERFLOW_CHECK == 1 // 溢出检查等级1:检查当前块是否已溢出memp_overflow_check_element(memp, desc);
#endif /* MEMP_OVERFLOW_CHECK */// 更新空闲链表头:将下一个空闲块设为新的表头*desc->tab = memp->next;
#if MEMP_OVERFLOW_CHECK // 开启溢出检查时,清空当前块的next指针(避免野指针)memp->next = NULL;
#endif /* MEMP_OVERFLOW_CHECK */
#endif /* !MEMP_MEM_MALLOC */#if MEMP_OVERFLOW_CHECK // 记录调试信息memp->file = file; // 存储分配所在的文件名memp->line = line; // 存储分配所在的行号
#if MEMP_MEM_MALLOC // 堆分配模式下,初始化溢出检查标记(如尾部魔术值)memp_overflow_init_element(memp, desc);
#endif /* MEMP_MEM_MALLOC */
#endif /* MEMP_OVERFLOW_CHECK */// 断言:确保内存块地址按MEM_ALIGNMENT对齐(嵌入式系统通常要求内存对齐)LWIP_ASSERT("memp_malloc: memp properly aligned",((mem_ptr_t)memp % MEM_ALIGNMENT) == 0);#if MEMP_STATS // 若开启内存池统计desc->stats->used++; // 已使用块数量+1// 更新最大使用量记录if (desc->stats->used > desc->stats->max) {desc->stats->max = desc->stats->used;}
#endifSYS_ARCH_UNPROTECT(old_level); // 退出临界区// 计算并返回用户数据区指针:跳过memp头部(MEMP_SIZE是struct memp的大小)return ((u8_t *)memp + MEMP_SIZE);} else { // 内存块分配失败(池为空或堆分配失败)
#if MEMP_STATS // 统计分配失败次数desc->stats->err++;
#endifSYS_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; // 分配失败返回NULL
}
memp_free(memp_t type, void *mem)
则将指定内存块重新插入该池的空闲链表头部,通过指针偏移找到struct memp
头部后更新next
指针完成释放。
/*** 将内存块放回其所属的内存池* @param type 内存块所属的内存池类型(memp_t枚举值)* @param mem 待释放的内存块指针(用户数据区指针,由memp_malloc返回)*/
void
memp_free(memp_t type, void *mem)
{
#ifdef LWIP_HOOK_MEMP_AVAILABLEstruct memp *old_first; // 用于记录释放前的空闲链表头
#endif// 断言检查:确保内存池类型合法(小于MEMP_MAX)LWIP_ERROR("memp_free: type < MEMP_MAX", (type < MEMP_MAX), return;);// 若待释放指针为NULL,直接返回(避免空指针操作)if (mem == NULL) {return;}#if MEMP_OVERFLOW_CHECK >= 2 // 若溢出检查等级为2及以上,先全局检查内存溢出memp_overflow_check_all();
#endif /* MEMP_OVERFLOW_CHECK >= 2 */#ifdef LWIP_HOOK_MEMP_AVAILABLE// 记录释放前的空闲链表头(用于判断是否从空池变为非空)old_first = *memp_pools[type]->tab;
#endif// 调用内部函数执行实际释放操作do_memp_free_pool(memp_pools[type], mem);#ifdef LWIP_HOOK_MEMP_AVAILABLE// 若释放前池为空(old_first == NULL),释放后触发可用钩子函数// (可用于唤醒等待该内存池的任务)if (old_first == NULL) {LWIP_HOOK_MEMP_AVAILABLE(type);}
#endif
}/*** 内部函数:执行实际的内存块释放操作* @param desc 内存池描述符(包含空闲链表、统计信息等)* @param mem 待释放的用户数据区指针*/
static void
do_memp_free_pool(const struct memp_desc *desc, void *mem)
{struct memp *memp; // 指向内存块头部(struct memp)SYS_ARCH_DECL_PROTECT(old_level); // 临界区保护变量// 断言:确保待释放的内存块地址按MEM_ALIGNMENT对齐LWIP_ASSERT("memp_free: mem properly aligned",((mem_ptr_t)mem % MEM_ALIGNMENT) == 0);// 从用户数据区指针反向计算内存块头部指针:减去memp头部大小(MEMP_SIZE)/* 通过void*强制转换消除对齐警告 */memp = (struct memp *)(void *)((u8_t *)mem - MEMP_SIZE);SYS_ARCH_PROTECT(old_level); // 进入临界区(保护链表操作)#if MEMP_OVERFLOW_CHECK == 1 // 溢出检查等级1:检查当前块是否已溢出memp_overflow_check_element(memp, desc);
#endif /* MEMP_OVERFLOW_CHECK */#if MEMP_STATS // 若开启内存池统计,减少已使用块计数desc->stats->used--;
#endif#if MEMP_MEM_MALLOC // 若配置为从堆分配的模式(MEMP_MEM_MALLOC=1)LWIP_UNUSED_ARG(desc); // 标记desc未使用(消除编译警告)SYS_ARCH_UNPROTECT(old_level); // 退出临界区mem_free(memp); // 调用堆释放函数释放整个内存块(包括头部)
#else /* MEMP_MEM_MALLOC */ // 默认:放回预分配的内存池空闲链表// 将当前块插入空闲链表头部(头插法)memp->next = *desc->tab; // 当前块的next指向原链表头*desc->tab = memp; // 链表头更新为当前块#if MEMP_SANITY_CHECK // 若开启内存池完整性检查// 验证内存池链表是否合法(如无循环链表等错误)LWIP_ASSERT("memp sanity", memp_sanity(desc));
#endif /* MEMP_SANITY_CHECK */SYS_ARCH_UNPROTECT(old_level); // 退出临界区
#endif /* !MEMP_MEM_MALLOC */
}
5.3 可变长度分配
5.3.1 内存池概念
可变长度内存分配策略是一种广泛应用的内存管理方式,其核心特点是系统中空闲内存块的大小不固定,会随用户的内存申请与释放动态变化:系统初始化时通常仅存在一块连续的大内存堆,随着运行过程中用户不断申请内存(分割大内存块)、释放内存(回收小块内存),内存堆中的块数量、各块大小会持续改变。
可变长度分配策略在很多系统中都会被使用到,系统运行的时候,各个空闲内存块的大小是不固定的,它会随着用户的申请而改变,刚开始的时候,系统就是一块大的内存堆,随着系统的运行,用户会申请与释放内存块,所以系统的内存块的大小、数量都会随之改变,并且对于这种内存分配策略是有多种不同的算法的。LwIP 协议栈采用的 “动态内存堆与内存池互补,用于分配可变大小的内存块。
5.3.2 First Fit内存管理算法
LwIP 中也会使用这种内存分配策略,它采用 First Fit(首次拟合)内存管理算法, 申请内存时只要找到一个比所请求的内存大的空闲块,就从中切割出合适的块,并把剩余的部分返回到动态内存堆中, 这种分配策略分配的内存块大小有限制,要求请求的分配大小不能小于 MIN_SIZE,否则请求会被分配到MIN_SIZE 大小的内存空间, 一般 MIN_SIZE大小为 12 字节,在这 12 个字节中前几个字节会存放内存分配器管理用的私有数据,该数据区域不能被用户程序修改,否则导致致命问题。内存释放的过程是相反的过程,但分配器会查看该节点前后相邻的内存块是否空闲,如果空闲则合并成一个大的内存空闲块。
当然,采用这种内存堆的分配方式,在申请和释放的时候肯定需要消耗时间,可以类似地看做是以时间换空间的策略。 采用这种分配策略,其优点就是内存浪费小,比较简单,适合用于小内存的管理,其缺点就是如果频繁的动态分配和释放,可能会造成严重的内存碎片,如果在碎片情况严重的话,可能会导致内存分配不成功从而导致系统崩溃。
内存碎片导致系统崩溃的原因并不是因为系统没有可用内存了,而是内存块被分割成很多不连续的小内存块,当用户需要申请一个更大的内存块的时候,系统没办法提供这样子的内存块,就会导致申请失败。
5.3.3 LwIP动态内存堆实现
与内存池相同,系统启动时,通过 mem_init()
函数完成堆的初始化,将预定义的连续内存区域划分为初始空闲块。堆的总容量由 lwipopts.h
中的 MEM_SIZE
宏定义(例如 #define MEM_SIZE 16384
表示配置 16KB 堆空间),需根据嵌入式设备的实际内存资源(如 RAM 大小)灵活调整,避免过度占用内存或因容量不足导致分配失败。
堆中的每个内存块均由 “管理头部 + 用户数据区” 两部分组成,头部存储系统管理所需的元数据,数据区供用户程序使用:
struct mem {struct mem *next; // 链表指针,指向堆中相邻的下一个内存块(空闲块通过此指针串联)u16_t len; // 数据区大小(单位:字节),不包含头部自身的占用空间u8_t used; // 块状态标记:1 表示已分配,0 表示空闲
};
内存分配mem_malloc ()采用 First Fit(首次拟合)算法,优先选用低地址区域空闲块以平衡效率与利用率。分配时先将用户请求大小按 MEM_ALIGNMENT 对齐,且确保不小于默认 12 字节的 MIN_SIZE(不足则按 MIN_SIZE 分配);随后遍历空闲块链表,找到首个尺寸满足需求的空闲块,若块过大则分割为 “已分配块” 和 “剩余空闲块”(剩余部分重新入链表),最终返回跳过 struct mem 头部的用户数据区地址,未找到合适块时返回 NULL。
内存释放mem_free ()释放时先通过用户指针反向计算 struct mem 头部,校验块的合法性(是否属当前堆、是否已分配);接着将块的 used 标记设为 0,再分别检查前后相邻块,若为空闲则执行后向、前向合并以减少碎片;最后将合并后的空闲块(或原块)插入空闲链表对应位置,保持链表地址有序,为后续分配提供便利 。