LWIP内存管理
介绍
LWIP使用内存池和动态内存堆管理动态内存。LWIP 提供了丰富的配置选项,允许用户根据系统的资源和性能要求调整内存管理策略。例如,可以在lwipopts.h文件中设置内存池的大小和数量,调整动态堆的大小,以及设置 PBUF 的分配策略等。可以根据系统的内存资源,权衡内存池和动态堆的使用比例,以达到性能和内存使用的平衡。例如,如果系统有较多的内存,可以适当增大动态堆的大小,以方便处理较大的数据;如果内存紧张,增大内存池的数量,提高内存分配效率。
内存池(Memory Pools)
LWIP 将内存划分为多个固定大小的内存池,每个内存池用于存储特定类型的数据结构。例如,有用于存储 TCP 控制块、UDP 控制块、PBUF(数据包缓冲区)等不同数据结构的内存池。内存池中的每个块大小是固定的,这样可以避免内存碎片的产生。当需要分配内存时,从相应的内存池中获取一个块;当使用完毕后,将该块归还给内存池。这种方式提高了内存分配和回收的速度,因为不需要复杂的内存管理算法,只需要简单的链表操作。不同类型的内存池大小和数量可以根据具体需求在 LWIP 的配置文件中进行配置。例如,可以调整 TCP 控制块内存池的大小和数量,以满足不同应用对 TCP 连接数量的需求。
动态堆内存分配(Dynamic Heap)
动态堆内存分配通常用于不适合使用内存池的情况,因为内存池只能分配固定大小的块,对于大小变化的数据,内存池无法满足需求。因此,LWIP 提供了动态堆内存分配。例如,对于大小不固定的数据,如应用层的数据缓冲区,使用动态堆内存分配。
LWIP 可以使用标准 C 库的malloc和free函数进行动态堆内存分配,但为了更好地适应嵌入式环境,通常会重写这两个函数。例如,使用mem_malloc和mem_free函数,它们可以根据具体的嵌入式系统进行优化,比如使用静态数组作为堆内存,避免动态分配的不确定性和复杂性。
相关宏
默认配置MEMP_MEM_MALLOC和MEM_USE_POOLS均为0,也就是内存堆和内存池有各自的实现方式。
MEM_LIBC_MALLOC
当 MEM_LIBC_MALLOC定义为1时,LWIP会使用 C 库提供的 malloc、free 和 realloc 函数来替代 LWIP 内部的内存分配器。如果项目中已经在使用 C 库的这些内存管理函数,那么在 LWIP 中启用 MEM_LIBC_MALLOC 可以避免重复实现内存分配功能,从而节省代码的大小。例如,在一个资源受限的嵌入式系统中,代码空间非常宝贵,使用 C 库已有的内存管理函数,就无需再集成 LWIP 自定义的内存分配器代码,减少了最终可执行文件的体积。
MEMP_MEM_MALLOC
如果MEMP_MEM_MALLOC定义为1,使用内存堆的实现方式替代内存池的实现方式。不能和MEM_USE_POOLS同时置1。在使用MEM_LIBC_MALLOC时特别有用,但在使用时需要注意以下两点:
1.执行速度:堆内存分配(heap alloc)可能比池内存分配(pool alloc)慢很多。
2.中断中的使用:尤其是当网络接口驱动(netif driver)在中断中为接收的帧分配PBUF_POOL类型的pbuf时,需要特别小心。
目前,这种方式会将所有的内存池(不仅包括memp_std.h中定义的内部池,还包括私有池)都使用堆内存来分配。简单来说,启用这个配置选项意味着改变LWIP 的内存分配方式,这种改变可能带来性能和中断处理方面的问题,并且会影响所有类型的内存池。
MEMP_MEM_INIT
当 MEMP_MEM_INIT定义为1时,它强制在初始化内存池时使用 memset 函数。这意味着在内存池被创建时,池中每一块内存都会被 memset 函数按照特定方式初始化。如果内存池被放置在内存的未初始化区域,这个设置就非常有用。在嵌入式系统开发中,为了节省内存,有时会将内存池放置在未初始化的数据段(如 .bss 段),这些区域在程序启动时默认内容是不确定的。通过启用 MEMP_MEM_INIT,可以确保内存池中的数据结构(如协议控制块 pcbs 结构体)在各种情况下都能被正确初始化。
MEM_ALIGNMENT
字节对齐个数,比如STM32是32位单片机,为4字节对齐。
MEM_SIZE
内存堆大小,使用默认内存堆实现方式时,这个宏定义了内存堆数组的长度。(不是最终长度,实际还要增加用于管理内存堆结构体,对齐字节等)
MEMP_OVERFLOW_CHECK和MEM_OVERFLOW_CHECK
LWIP 中使用 MEMP_OVERFLOW_CHECK 宏来控制是否启用内存池或内存堆溢出检查,在 LWIP 中,内存池或内存堆溢出是指在分配或使用时,超出了内存池或内存堆所定义的范围。例如,内存池被分配了一定数量的内存块,当试图分配超过这个数量的内存块,或者在使用内存块时超出了每个内存块的大小,就会发生溢出。这个宏可以设置不同的级别:
- MEMP_OVERFLOW_CHECK/MEM_OVERFLOW_CHECK == 0:不进行内存池溢出检查
- MEMP_OVERFLOW_CHECK/MEM_OVERFLOW_CHECK == 1:在内存池中的内存块被释放时检查
- MEMP_OVERFLOW_CHECK/MEM_OVERFLOW_CHECK >= 2:每次使用memp_malloc()或者memp_free()时都对每个内存池中的内存块进行检查(有用但效率低)。
默认MEMP_OVERFLOW_CHECK和MEM_OVERFLOW_CHECK 为0。
MEMP_SANITY_CHECK和MEM_SANITY_CHECK
当 MEMP_SANITY_CHECK或MEM_SANITY_CHECK定义为1时,每次调用释放内存函数之后,LWIP 会执行一个完整性检查。这个完整性检查的主要目的是确保内存池或内存堆中链表的正确性。
MEM_USE_POOLS
如果MEM_USE_POOLS定义为1,使用内存池的实现方式替代内存堆的实现方式。要使用这种基于内存池的内存分配方式,除了设置MEM_USE_POOLS为1之外,还必须启用MEMP_USE_CUSTOM_POOLS。不能和MEMP_MEM_MALLOC同时置1。
MEM_USE_POOLS_TRY_BIGGER_POOL
如果MEM_USE_POOLS_TRY_BIGGER_POOL定义为1,当一个内存分配池(malloc-pool)为空,无法满足当前的内存分配请求时,系统不会立即宣告内存分配失败,而是尝试使用下一个更大的内存池来分配内存。这种策略可能会导致内存浪费,在资源紧张的系统中,这种内存浪费可能会影响系统的整体性能和资源利用率;对于那些对可靠性要求较高,并且对内存资源不是极度敏感的系统,可以考虑启用此宏。
MEMP_USE_CUSTOM_POOLS
如果MEMP_USE_CUSTOM_POOLS定义为1,需要包含一个用户自定义的文件 lwippools.h。这个 lwippools.h 文件的主要作用是定义额外的内存池。
内存池(Memory Pools)
源文件memp.c和memp.h。
描述内存池的结构体:
struct memp_desc {
#if defined(LWIP_DEBUG) || MEMP_OVERFLOW_CHECK || LWIP_STATS_DISPLAY
/** Textual description 是一个描述性的字符串,用于描述该内存池的用途,方便调试和查看内存池的信息。*/
const char *desc;
#endif /* LWIP_DEBUG || MEMP_OVERFLOW_CHECK || LWIP_STATS_DISPLAY */
#if MEMP_STATS
/** Statistics 指向内存池统计信息的指针,用于记录内存池的使用情况,如已使用的内存块数量、空闲的内存块数量等。*/
struct stats_mem *stats;
#endif
/** Element size 每个内存池中的内存块的大小,这个大小是根据存储的数据结构和对齐要求确定的。*/
u16_t size;
#if !MEMP_MEM_MALLOC
/** Number of elements 该内存池包含的内存块的数量。*/
u16_t num;
/** Base address 指向内存池的起始地址,即内存池的内存区域的开始位置。*/
u8_t *base;
/** First free element of each pool. Elements form a linked list. 每个内存池的第一个空闲元素。所有元素组成一个链表。*/
struct memp **tab;
#endif /* MEMP_MEM_MALLOC */
};
内存池的存储区域是一块连续的内存空间,也就是数组,其大小为 num * (MEMP_SIZE + MEMP_ALIGN_SIZE(size))。
内存池定义:
#define LWIP_DECLARE_MEMORY_ALIGNED(variable_name, size) u8_t variable_name[LWIP_MEM_ALIGN_BUFFER(size)]
#define LWIP_MEMPOOL_DECLARE(name,num,size,desc) \
LWIP_DECLARE_MEMORY_ALIGNED(memp_memory_ ## name ## _base, ((num) * (MEMP_SIZE + MEMP_ALIGN_SIZE(size)))); \
\
LWIP_MEMPOOL_DECLARE_STATS_INSTANCE(memp_stats_ ## name) \
\
static struct memp *memp_tab_ ## name; \
\
const struct memp_desc memp_ ## name = { \
DECLARE_LWIP_MEMPOOL_DESC(desc) \
LWIP_MEMPOOL_DECLARE_STATS_REFERENCE(memp_stats_ ## name) \
LWIP_MEM_ALIGN_SIZE(size), \
(num), \
memp_memory_ ## name ## _base, \
&memp_tab_ ## name \
};
#endif /* MEMP_MEM_MALLOC */
const struct memp_desc *const memp_pools[MEMP_MAX] = {
#define LWIP_MEMPOOL(name,num,size,desc) &memp_ ## name,
#include "lwip/priv/memp_std.h"
};
LWIP_DECLARE_MEMORY_ALIGNED 用于声明一个内存区域,确保内存区域是对齐的。
LWIP_MEMPOOL_DECLARE_STATS_INSTANCE 用于创建一个内存池的统计实例。
memp_tab_ ## name 是一个静态指针数组,用于管理内存池中的内存块。
所有需要定义的内存池都在lwip/priv/memp_std.h文件中根据需要的功能定义,基于 LWIP_MEMPOOL (name,num,size,desc) 宏,在被包含时,编译器会根据提供的具体参数展开该宏。同时,在文件的末尾有 #undef LWIP_MEMPOOL 语句,这是因为该文件可能会被多个地方包含和重复使用,如果不撤销宏定义,可能会导致宏定义的重复定义和冲突,影响程序的正确性和可维护性。
例如,若有 #define LWIP_MEMPOOL (UDP_PCB, 4, sizeof (struct udp_pcb),“UDP_PCB”),则会生成如下结构体:
const struct memp_desc memp_UDP_PCB = {
DECLARE_LWIP_MEMPOOL_DESC("UDP_PCB")
LWIP_MEMPOOL_DECLARE_STATS_REFERENCE(memp_stats_UDP_PCB)
LWIP_MEM_ALIGN_SIZE(sizeof(struct udp_pcb)),
4,
memp_memory_UDP_PCB_base,
&memp_tab_UDP_PCB
};
通过在memp.h重复定义 LWIP_MEMPOOL (name,num,size,desc) 宏,在 typedef enum 语句中,会生成一个枚举类型 memp_t,用于唯一标识每种类型的内存池。例如:
/* run once with empty definition to handle all custom includes in lwippools.h */
#define LWIP_MEMPOOL(name,num,size,desc)
#include "lwip/priv/memp_std.h"
/** Create the list of all memory pools managed by memp. MEMP_MAX represents a NULL pool at the end */
typedef enum {
#define LWIP_MEMPOOL(name,num,size,desc) MEMP_##name,
#include "lwip/priv/memp_std.h"
MEMP_MAX
} memp_t;
typedef enum {
#define LWIP_MEMPOOL(name,num,size,desc) MEMP_##name,
#include "lwip/priv/memp_std.h"
MEMP_MAX
} memp_t;
这样,在整个 LWIP 的内存池管理中,就可以使用 memp_t 类型的变量作为参数来指定要操作的内存池类型,方便在内存分配和释放等操作中进行统一管理和索引。
内存初始化
内存池初始化时函数调用关系:MX_LWIP_Init()→tcpip_init()→lwip_init()→memp_init()→memp_init_pool()。
void
memp_init_pool(const struct memp_desc *desc)
{
#if MEMP_MEM_MALLOC
// 如果使用 MEMP_MEM_MALLOC 宏定义,说明使用内存池采用动态内存分配,这里的参数未使用
LWIP_UNUSED_ARG(desc);
#else
int i;
struct memp *memp;
// 将内存池的 tab 指针初始化为 NULL,表示开始时没有分配的内存块
*desc->tab = NULL;
// 将 memp 指针指向内存池的起始地址,并进行内存对齐操作
memp = (struct memp *)LWIP_MEM_ALIGN(desc->base);
#if MEMP_MEM_INIT
/* force memset on pool memory // 如果启用 MEMP_MEM_INIT,使用 memset 函数将内存池中的内存块初始化为 0*/
memset(memp, 0, (size_t)desc->num * (MEMP_SIZE + desc->size
#if MEMP_OVERFLOW_CHECK
+ MEM_SANITY_REGION_AFTER_ALIGNED
#endif
));
#endif
/* create a linked list of memp elements 创建一个内存池元素的链表*/
for (i = 0; i < desc->num; ++i) {
memp->next = *desc->tab; // 将当前内存块的next 指针指向前一个已添加到链表中的内存块
*desc->tab = memp; // 将当前内存块添加到链表头部
#if MEMP_OVERFLOW_CHECK
memp_overflow_init_element(memp, desc);// 如果启用了溢出检查,初始化内存块的溢出检查信息
#endif /* MEMP_OVERFLOW_CHECK */
/* cast through void* to get rid of alignment warnings 移动到下一个内存块的位置,考虑内存块大小、MEMP_SIZE等。*/
memp = (struct memp *)(void *)((u8_t *)memp + MEMP_SIZE + desc->size
#if MEMP_OVERFLOW_CHECK
+ MEM_SANITY_REGION_AFTER_ALIGNED
#endif
);
}
#if MEMP_STATS
desc->stats->avail = desc->num;// 如果启用 MEMP_STATS,将内存池的可用内存块数量初始化为内存池的总内存块数量
#endif /* MEMP_STATS */
#endif /* !MEMP_MEM_MALLOC */
#if MEMP_STATS && (defined(LWIP_DEBUG) || LWIP_STATS_DISPLAY)
desc->stats->name = desc->desc;// 如果启用了统计信息且处于调试模式或显示统计信息,将内存池的名称存储在统计信息中
#endif /* MEMP_STATS && (defined(LWIP_DEBUG) || LWIP_STATS_DISPLAY) */
}
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_init_pool 函数初始化每个内存池
#if LWIP_STATS && MEMP_STATS
lwip_stats.memp[i] = memp_pools[i]->stats;// 如果启用了统计功能,将当前内存池的统计信息存储到 lwip_stats.memp[i] 中
#endif
}
#if MEMP_OVERFLOW_CHECK >= 2
/* check everything a first time to see if it worked */
memp_overflow_check_all();
#endif /* MEMP_OVERFLOW_CHECK >= 2 */
}
同样以上面的UDP_PCB为例,#define LWIP_MEMPOOL (UDP_PCB, 4, sizeof (struct udp_pcb),“UDP_PCB”),初始化后UDP内存池示意图:
keil中debug信息如下:
size大小为0x20,num个数为4,base指向UDP内存池首地址0x200093E3,是一个u8_t类型的数组,stm32内存对齐MEM_ALIGNMENT为4字节,因此数组大小为size*num + 4 - 1,即131。
LWIP_DECLARE_MEMORY_ALIGNED(variable_name, size) u8_t variable_name[LWIP_MEM_ALIGN_BUFFER(size)]
#define LWIP_MEM_ALIGN_BUFFER(size) (((size) + MEM_ALIGNMENT - 1U))
UDP内存池链表的第一个内存块起始地址为0x20009444,最后一个内存块起始地址是0x200093E4。
tab为struct memp *类型的结构体指针,该指针的存储地址0x200000D4。
内存申请
void *
#if !MEMP_OVERFLOW_CHECK// 根据是否启用 MEMP_OVERFLOW_CHECK 宏,定义不同的 memp_malloc 函数名
memp_malloc(memp_t type)//参数类型为memp_t 枚举
#else
memp_malloc_fn(memp_t type, const char *file, const int line)
#endif
{
void *memp;
// 检查传入的内存池类型是否有效,若类型不小于 MEMP_MAX 则返回 NULL
LWIP_ERROR("memp_malloc: type < MEMP_MAX", (type < MEMP_MAX), return NULL;);
#if MEMP_OVERFLOW_CHECK >= 2
// 如果启用了 MEMP_OVERFLOW_CHECK 且级别大于等于 2,则进行内存池溢出的全面检查
memp_overflow_check_all();
#endif /* MEMP_OVERFLOW_CHECK >= 2 */
#if !MEMP_OVERFLOW_CHECK
memp = do_memp_malloc_pool(memp_pools[type]);
#else
memp = do_memp_malloc_pool_fn(memp_pools[type], file, line);
#endif
return memp;
}
void *
#if !MEMP_OVERFLOW_CHECK
memp_malloc_pool(const struct memp_desc *desc)//参数类型为指定的内存池
#else
memp_malloc_pool_fn(const struct memp_desc *desc, const char *file, const int line)
#endif
{
LWIP_ASSERT("invalid pool desc", desc != NULL);
if (desc == NULL) {
return NULL;
}
#if !MEMP_OVERFLOW_CHECK
return do_memp_malloc_pool(desc);
#else
return do_memp_malloc_pool_fn(desc, file, line);
#endif
}
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;
SYS_ARCH_DECL_PROTECT(old_level);//int lev;
#if MEMP_MEM_MALLOC// 如果使能了 MEMP_MEM_MALLOC,使用 mem_malloc 函数分配内存,大小为 MEMP_SIZE 加上对齐后的 desc->size
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);//进入临界区保护
memp = *desc->tab;// 从内存池的 tab 指针获取第一个可用的内存块
#endif /* MEMP_MEM_MALLOC */
if (memp != NULL) { // 内存池的 tab 指针不为NULL
#if !MEMP_MEM_MALLOC
#if MEMP_OVERFLOW_CHECK == 1
memp_overflow_check_element(memp, desc);// 如果仅启用了一级溢出检查,检查该内存块是否溢出
#endif /* MEMP_OVERFLOW_CHECK */
*desc->tab = memp->next;// 将内存池的 tab 指针指向下一个可用内存块
#if MEMP_OVERFLOW_CHECK
memp->next = NULL;// 清空已分配内存块的 next 指针,避免意外的链表操作
#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 */
LWIP_ASSERT("memp_malloc: memp properly aligned",
((mem_ptr_t)memp % MEM_ALIGNMENT) == 0);// 断言分配的内存块地址是否正确对齐
#if MEMP_STATS
desc->stats->used++; // 更新内存池的使用统计信息
if (desc->stats->used > desc->stats->max) {// 如果当前使用量超过最大值,更新最大值
desc->stats->max = desc->stats->used;
}
#endif
SYS_ARCH_UNPROTECT(old_level);// 离开临界区保护
/* cast through u8_t* to get rid of alignment warnings */
return ((u8_t *)memp + MEMP_SIZE);// 返回内存块的用户数据部分(跳过 memp 结构头部)
} else {
#if MEMP_STATS
desc->stats->err++;// 记录内存分配错误次数
#endif
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;
}
SYS_ARCH_DECL_PROTECT(old_level)
用于声明一个变量 old_level,该变量通常用于存储当前系统的保护级别。在多任务或多线程环境中,保护级别可以用来表示当前的临界区保护状态,防止多个任务同时访问共享资源,避免竞态条件和数据不一致的问题。
SYS_ARCH_PROTECT(old_level)
用于进入临界区保护,通常以下方式实现:
- 关闭中断:在某些嵌入式系统中,通过关闭中断来防止其他任务或中断服务程序干扰当前操作,确保代码的原子性。
- 获取锁:在多线程系统中,可能是获取一个互斥锁或信号量,以确保同一时间只有一个线程可以访问临界区。
SYS_ARCH_UNPROTECT(old_level)
用于退出临界区保护,通常以下方式实现:
- 开启中断:在之前关闭中断的情况下,重新开启中断,恢复系统的正常中断处理。
- 释放锁:在多线程系统中,释放之前获取的互斥锁或信号量,允许其他线程访问临界区。
内存释放
void
memp_free(memp_t type, void *mem)
{
#ifdef LWIP_HOOK_MEMP_AVAILABLE//内存池钩子函数使能
struct memp *old_first;
#endif
// 检查要释放的内存池类型是否有效
LWIP_ERROR("memp_free: type < MEMP_MAX", (type < MEMP_MAX), return;);
if (mem == NULL) {
return;
}
#if MEMP_OVERFLOW_CHECK >= 2
memp_overflow_check_all(); // 若启用了 MEMP_OVERFLOW_CHECK 且级别大于等于 2,则进行内存池的溢出检查
#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);// 调用 do_memp_free_pool 函数将内存块释放回内存池
#ifdef LWIP_HOOK_MEMP_AVAILABLE
if (old_first == NULL) {// 如果释放内存之前没有可用内存,那么现在有了,因为刚释放了一个内存块,调用钩子函数
LWIP_HOOK_MEMP_AVAILABLE(type);
}
#endif
}
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);// 将 mem 指针转换为 memp 指针,通过减去 MEMP_SIZE 定位到内存池元素的起始位置
SYS_ARCH_PROTECT(old_level);//进入临界区保护
#if MEMP_OVERFLOW_CHECK == 1
memp_overflow_check_element(memp, desc); // 若启用了一级的溢出检查,对该内存块进行溢出检查
#endif /* MEMP_OVERFLOW_CHECK */
#if MEMP_STATS
desc->stats->used--; // 若启用了内存池统计,将已使用的内存块数量减一
#endif
#if MEMP_MEM_MALLOC//如果内存池中内存块使用动态申请
LWIP_UNUSED_ARG(desc);
SYS_ARCH_UNPROTECT(old_level);// 离开临界区保护
mem_free(memp);
#else /* MEMP_MEM_MALLOC */
memp->next = *desc->tab;// 将释放的内存块添加到内存池的可用内存块链表头部
*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 */
}
LWIP_HOOK_MEMP_AVAILABLE 宏通常在内存池操作的上下文中使用,特别是在内存释放操作中,用于在内存池中有新的可用内存块时执行额外的操作。
当 LWIP_HOOK_MEMP_AVAILABLE 被定义时,在释放内存块后,如果释放该内存块使得内存池的第一个可用内存块指针 old_first 从 NULL 变为非 NULL(即内存池从满到有可用内存块,在调用 do_memp_free_pool 之前,old_first = *memp_pools[type]->tab; 存储了当前的 tab 指针;如果 old_first 是 NULL,表示内存池在释放之前是满的;释放内存块后,*memp_pools[type]->tab 不再是 NULL,因为刚释放的内存块成为了链表的头部。),会调用 LWIP_HOOK_MEMP_AVAILABLE(type)。可以用来监控内存池的状态,当内存池从满状态变为有可用内存时,执行相应的监控操作,例如更新监控信息、记录日志或通知其他模块;当内存池可用时,允许新的网络连接或任务,或者通知其他模块可以进行资源分配操作。
动态堆内存分配(Dynamic Heap)
源文件mem.c和mem.h。
描述内存池的结构体:
#if MEM_SIZE > 64000L
typedef u32_t mem_size_t;
#define MEM_SIZE_F U32_F
#else
typedef u16_t mem_size_t;
#define MEM_SIZE_F U16_F
#endif /* MEM_SIZE > 64000 */
#endif
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;
#if MEM_OVERFLOW_CHECK
/** this keeps track of the user allocation size for guard checks */
mem_size_t user_size;
#endif
};
next和prev类似于双向链表中的指针,用于管理整个内存堆,在这里根据MEM_SIZE的大小定义为u32_t或者u16_t类型的变量,用来指示目的地址相对于整个内存堆的起始地址的偏移量。
used用来表示该内存块是否被使用。
当 MEM_OVERFLOW_CHECK 宏被定义时,struct mem 结构体包含 user_size 成员,目的是进行溢出检查,确保用户不会超出分配的内存大小,避免内存溢出。
//MIN_SIZE 定义了分配的内存块的最小值,较小的值可节省空间,较大的值可以防止过小的内存块导致内存碎片化。
#ifndef MIN_SIZE
#define MIN_SIZE 12
#endif /* MIN_SIZE */
/* some alignment macros: we define them here for better source code layout */
#define MIN_SIZE_ALIGNED LWIP_MEM_ALIGN_SIZE(MIN_SIZE)
#define SIZEOF_STRUCT_MEM LWIP_MEM_ALIGN_SIZE(sizeof(struct mem))
#define MEM_SIZE_ALIGNED LWIP_MEM_ALIGN_SIZE(MEM_SIZE)
/** 如果希望将堆重定位到外部内存,只需将LWIP_RAM_HEAP_POINTER定义为指向该位置的void指针。 */
#ifndef LWIP_RAM_HEAP_POINTER
/** 堆 ,需要在末尾创建一个struct mem,并为对齐留出一些空间 */
LWIP_DECLARE_MEMORY_ALIGNED(ram_heap, MEM_SIZE_ALIGNED + (2U * SIZEOF_STRUCT_MEM));
#define LWIP_RAM_HEAP_POINTER ram_heap
static u8_t *ram;//u8_t 类型的静态指针,用于指向内存堆。
static struct mem *ram_end;//struct mem 类型的静态指针,标记内存堆的最后一个条目。
static struct mem * LWIP_MEM_LFREE_VOLATILE lfree;//struct mem 类型的静态指针,指向最低位置的空闲内存块。可以从该位置开始查找,而不是每次都从内存堆的开头开始查找,提高查找效率。
#endif /* LWIP_RAM_HEAP_POINTER */
内存初始化
内存堆初始化时函数调用关系:MX_LWIP_Init()→tcpip_init()→lwip_init()→mem_init()。
void
mem_init(void)
{
struct mem *mem;
LWIP_ASSERT("Sanity check alignment",
(SIZEOF_STRUCT_MEM & (MEM_ALIGNMENT - 1)) == 0);//确保struct mem结构体内存对齐
/* align the heap */
ram = (u8_t *)LWIP_MEM_ALIGN(LWIP_RAM_HEAP_POINTER);//内存堆空间对齐,使ram指向内存堆
/* initialize the start of the heap */
mem = (struct mem *)(void *)ram;// 初始化堆内存的起始部分,内存堆数组起始位置
mem->next = MEM_SIZE_ALIGNED;// 设置起始内存块的下一个块的索引为 MEM_SIZE_ALIGNED,内存堆结束位置
mem->prev = 0;// 起始内存块的上一个块的索引为 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; // next 与 prev 字段都指向自身,表示到了内存堆结束位置,后边没有内存可以分配
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);
}
}
内存堆本质上是一个数组,数组大小由MEM_SIZE决定。在使用 mem_init 函数初始化内存管理系统后,可以使用 mem_malloc 和 mem_free 函数进行内存的分配和释放操作,同时通过 lfree 指针、ram_end 指针和内存块的 used 状态等信息,维护内存的使用状态和链表结构,确保内存的正确分配和释放。运行过程中lfree始终指向最低位置的空闲内存块;ram_end不会改变,指向系统中的最后一个内存块,且一直是已使用状态。通过 mem_mutex 互斥锁,保护内存操作在多任务或多线程环境下的安全性。
内存堆初始化后如下:
默认内存堆大小为1600字节,为方便演示改为400字节,则改动后内存堆数组大小为419(MEM_SIZE_ALIGNED + (2U * SIZEOF_STRUCT_MEM) + MEM_ALIGNMENT - 1),即400 + 2 * 8 + 4 -1 = 419。
那么MEM_SIZE_ALIGNED值为400(0x190)。
内存申请
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) { // 如果malloc的内存大小为 0,则直接返回 NULL
return NULL;
}
/* // 扩展要分配的内存区域大小,以调整对齐 */
size = (mem_size_t)LWIP_MEM_ALIGN_SIZE(size_in);
if (size < MIN_SIZE_ALIGNED) {// 确保每个数据块至少为 MIN_SIZE_ALIGNED 长度
/* every data block must be at least MIN_SIZE_ALIGNED long */
size = MIN_SIZE_ALIGNED;
}
#if MEM_OVERFLOW_CHECK// 如果进行内存溢出检查,为前后的内存完整性区域添加额外的空间
size += MEM_SANITY_REGION_BEFORE_ALIGNED + MEM_SANITY_REGION_AFTER_ALIGNED;
#endif
if ((size > MEM_SIZE_ALIGNED) || (size < size_in)) {// 如果扩展后的大小超出了最大内存大小或小于原始请求大小,返回 NULL
return NULL;
}
/* protect the heap from concurrent access */
sys_mutex_lock(&mem_mutex);// 加锁以保护堆免受并发访问
LWIP_MEM_ALLOC_PROTECT();
/* // 从最低的空闲块开始遍历堆,寻找足够大的空闲块*/
for (ptr = mem_to_ptr(lfree); ptr < MEM_SIZE_ALIGNED - size;
ptr = ptr_to_mem(ptr)->next) {
mem = ptr_to_mem(ptr);
if ((!mem->used) &&
(mem->next - (ptr + SIZEOF_STRUCT_MEM)) >= size) { // 找到一个未使用且足够大的内存块
/* 这个内存块的大小需要满足用户需要的大小加上SIZEOF_STRUCT_MEM结构体的大小,因为需要使用SIZEOF_STRUCT_MEM
结构体管理这块内存*/
if (mem->next - (ptr + SIZEOF_STRUCT_MEM) >= (size + SIZEOF_STRUCT_MEM + MIN_SIZE_ALIGNED)) {
/*除满足上述条件外,这个内存块还可以容纳MIN_SIZE_ALIGNED加上SIZEOF_STRUCT_MEM 大小的空间,那么说明这个内存块太大了
不能直接分配给用户,需要先进行分割,先拆分大的内存块,拆分后剩余内存空间必须大到足以容纳 MIN_SIZE_ALIGNED 数据:
因为如果mem->next - (ptr + (2*SIZEOF_STRUCT_MEM)) == size,用于管理拆分后空间的结构体 mem 可以放入,
但在 mem2 和 mem2->next 之间将没有空间可以被分配,分割就没有意义。
我们可以不考虑 MIN_SIZE_ALIGNED。这样我们会创建一个无法容纳数据的空区域,但是当 mem->next 被释放时,这两个区域将被
合并,从而产生更多的可用内存。
*/
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;//指向开始分配内存块的起始地址
/* // 将 mem2 插入到 mem 和 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));
}
if (mem == lfree) {//lfree指向最低位置的空闲内存块,分配后需要更新lfree位置
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);
}
lfree = cur;
LWIP_ASSERT("mem_malloc: !lfree->used", ((lfree == ram_end) || (!lfree->used)));
}
LWIP_MEM_ALLOC_UNPROTECT();
sys_mutex_unlock(&mem_mutex);// 解锁
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);
#if MEM_OVERFLOW_CHECK
mem_overflow_init_element(mem, size_in);
#endif
MEM_SANITY();
return (u8_t *)mem + SIZEOF_STRUCT_MEM + MEM_SANITY_OFFSET; // 返回分配的内存地址,跳过内存结构体和可能的内存完整性偏移,即用户可以操作的空间
}
}
MEM_STATS_INC(err);
LWIP_MEM_ALLOC_UNPROTECT();
sys_mutex_unlock(&mem_mutex);// 解锁
LWIP_DEBUGF(MEM_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("mem_malloc: could not allocate %"S16_F" bytes\n", (s16_t)size));
return NULL;//不能分配指定大小的空间,返回 NULL
}
上面代码不包含LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT宏相关的代码,如果想要从中断上下文(或另一个不允许等待信号量的上下文)中释放 PBUF_RAM pbufs(或调用 mem_free ()),将这个宏设置为 1。如果设置为 1,mem_malloc 将受到信号量和 SYS_ARCH_PROTECT 的保护,而 mem_free 将仅使用 SYS_ARCH_PROTECT。mem_malloc 会在每次循环时 SYS_ARCH_UNPROTECT,以便 mem_free 可以运行。这会导致频繁地启用 / 禁用中断,降低运行速度,并且,在内存不足时,mem_malloc 可能需要更长的时间。如果不希望这样,至少对于 NO_SYS = 0 (有RTOS)的情况,你仍然可以使用以下函数将一个释放调用加入队列,该调用随后会在 tcpip_thread 上下文中运行:pbuf_free_callback§;或者mem_free_callback(m);
作用是把要释放的内存和对应的内存释放函数等封装成struct tcpip_msg类型的消息,通过邮箱(Freertos使用消息队列实现)发送给tcpip_thread 线程,而不是立即释放该内存。这样可以确保内存释放操作在 tcpip_thread 中安全地执行,避免了一些上下文的问题,比如在中断上下文中释放内存时可能会出现的同步问题,因为它将释放操作推迟到 tcpip_thread 来处理。
内存堆初始化后连续申请两个20字节的空间,SIZEOF_STRUCT_MEM此时的值为8:
示意图:
分别向申请的两个内存块中写入"hello"和"world"字符串,可以看到用户申请的空间起始地址相对于内存块的起始地址偏移量为8,也就是SIZEOF_STRUCT_MEM的大小。
内存释放
void
mem_free(void *rmem)
{
struct mem *mem;
LWIP_MEM_FREE_DECL_PROTECT();
if (rmem == NULL) { // 如果要释放的内存指针为 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"));
/* // 保护内存统计信息免受并发访问 */
MEM_STATS_INC_LOCKED(illegal);
return;
}
/* 获取相应的mem 内存结构指针,通过减去内存结构的大小和可能的内存完整性偏移量 */
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;
}
#if MEM_OVERFLOW_CHECK
mem_overflow_check_element(mem);
#endif
/*// 保护堆免受并发访问*/
LWIP_MEM_FREE_PROTECT();
/*// 要释放的内存应该处于使用状态,如果未使用则为非法释放 */
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->used = 0;
if (mem < lfree) {
/* // 如果释放的内存结构是当前最低的,更新最低空闲内存指针 */
lfree = mem;
}
// 减少使用的内存统计信息,计算释放的内存大小
MEM_STATS_DEC_USED(used, mem->next - (mem_size_t)(((u8_t *)mem - ram)));
/* // 最终,查看前后的内存块是否也为空闲,如果有相邻的空闲内存块进行合并操作 */
plug_holes(mem);
MEM_SANITY();
LWIP_MEM_FREE_UNPROTECT();
}
static int
mem_link_valid(struct mem *mem)//检查要释放的内存块是不是被正确链接,如果不是,可能是因为重复释放
{
struct mem *nmem, *pmem;
mem_size_t rmem_idx;
rmem_idx = mem_to_ptr(mem);//获取到要释放内存块的起始地址
nmem = ptr_to_mem(mem->next);//要释放内存的下一个内存块
pmem = ptr_to_mem(mem->prev);//要释放内存的前一个内存块
if ((mem->next > MEM_SIZE_ALIGNED) || (mem->prev > MEM_SIZE_ALIGNED) ||//要释放内存块的前或者后一个内存块的起始地址大于内存堆的大小
//要释放内存块的前一个内存块的next没有指向本内存块并且前一个内存块的next数值(数组偏移)不等于本内存块的起始地址
((mem->prev != rmem_idx) && (pmem->next != rmem_idx)) ||
//要释放内存块的后一个内存块不是最后一个内存块并且后一个内存块的prev数值(数组偏移)不等于本内存块的起始地址
((nmem != ram_end) && (nmem->prev != rmem_idx))) {
return 0;
}
return 1;
}
//合并相邻的空闲内存块,在这个函数执行完之后,不应该存在一个空的内存结构体指向另一个空的内存结构体。
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);
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 (lfree == nmem) {//更新空闲指针
lfree = mem;
}
mem->next = nmem->next;//更新当前内存块next指针
if (nmem->next != MEM_SIZE_ALIGNED) { //如果下一个内存块不是最后一个内存块
// 更新下一个内存块的前一个指针指向当前内存块
ptr_to_mem(nmem->next)->prev = mem_to_ptr(mem);
}
}
/*向前合并 */
pmem = ptr_to_mem(mem->prev);
if (pmem != mem && pmem->used == 0) {
/* 如果前一个内存块未使用,则合并当前内存块和前一个内存块 */
if (lfree == mem) {//更新空闲指针
lfree = pmem;
}
pmem->next = mem->next;//更新前一个内存块next指针
if (mem->next != MEM_SIZE_ALIGNED) {
// 更新下一个内存块的前一个指针指向合并后内存块
ptr_to_mem(mem->next)->prev = mem_to_ptr(pmem);
}
}
}
mem_free()函数用来释放内存,释放前会通过mem_link_valid()函数检查要释放的内存块是否被正确链接,防止重复释放,通过plug_holes()函数检查要释放内存块前后相邻的内存块是否空闲,将相邻的空闲内存块进行合并,以优化内存使用,避免内存碎片的产生,提高内存管理的效率和性能。
如果先释放申请的第一个内存块后:
示意图:
由于释放第一个内存块的前后没有空闲内存块,因此无法合并,此时lfree指向第一个刚释放的内存块,下次申请时从这个刚释放的内存块位置开始遍历。
如果先释放申请的第二个内存块后:
示意图:
如果先释放申请的第二个内存块,它的下一个内存块为空闲内存块,因此进行了合并,此时lfree指向合并后的内存块。