工作笔记-----lwip的内存管理策略解析
工作笔记-----lwip的内存管理策略
@@ Author:明月清了个风
@@ Date:2025.9.21
@@ PS:在这之前已经写过几篇关于lwip的文章了,在其中都依赖于lwip核心的一个数据结构pbuf来传递消息,在前面的文章中并没有对该数据结构以及其所在文件“\pbuf.c”进行过深入讲解,但是其对于lwip协议栈的构建是十分重要的,因此想记录一下对于pbuf源码的学习。但在此之前,首先应对lwip的内存管理策略有所了解,因此这一篇先记录了对于lwip内存管理策略的学习。
@@ Version:First published date 2025.9.21
之前的文章:
FreeRTOS中的lwIP网络任务为什么会让出CPU
lwip网络任务初始化问题排查
基于FreeRTOS的lwIP网络任接收过程,从MAC至协议栈
lwip的内存管理
要存储和转运数据 ,就要有对应的内存管理和分配机制,这是绕不开的,因此在讲lwip是如何管理其数据结构pbuf之前,得先搞清楚他的内存管理机制。
对于lwip来说,一共有三种内存管理方法:内存池,C库的内存分配策略,lwip自己的动态内存堆分配策略。
其中C库的内存分配策略不必多说就是库函数malloc
和free
.
使用不同的内存管理策略着几个宏定义MEM_LIBC_MALLOC
,MEMP_MEM_MALLOC
,MEM_USE_POOLS
.
lwip内存堆管理策略
mem结构体
要看一个内存管理策略,首先看他的数据结构组织形式,下面是lwip内存堆管理策略使用的数据结构
/*** The heap is made up as a list of structs of this type.* This does not have to be aligned since for getting its size,* we only use the macro SIZEOF_STRUCT_MEM, which automatically aligns.*/
struct mem {mem_size_t next; mem_size_t prev;u8_t used;
#if MEM_OVERFLOW_CHECKmem_size_t user_size;
#endif
};
根据注释可知,分配给lwip的内存会被初始为一个mem
链表,里面的成员也很简单,两个用于构建链表的指针以及一个标志位,剩下的一个可选项用于检测内存的使用情况。
mem_init函数
然后来看他的内存堆初始化函数,比较短,直接把源码贴出来了
void mem_init(void)
{struct mem *mem;/* 内存对齐后的起始地址,并且留出了一个mem结构体大小,找到宏定义可以发现其包含了宏-MEM_SIZE-值 *//* 这里其实是初始化了一个u8数组,宏定义一层一层点进去就行 */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;/* 初始化最后一个管理数据结构,ram_end是全局变量 */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(); /* 内存合法性判断 *//* 使用一个mem类型全局变量lfree管理首块空闲内存,方便查找 */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_malloc函数
然后来看他是怎么分配的,也就是mem_malloc()
函数,这个函数比较长,因此先给出其大致流程,再讲解其中的细节处理(省略了部分可选宏定义的操作过程,不讨论全部的实现,仅给出核心操作思想)
mem_malloc(size_in)流程 参数size_in就是要分配的内存块大小--->第一步:对size_in字节对齐,判断合法性,得到实际要分配的大小size--->第二步:从lfree开始找第一个大小合适的内存块(First fit),将其分配--->第三步:更新lfree
-
1️⃣**第一步:**第一步没什么好说的。
-
2️⃣第二步:这是一个循环体,如果找到了合适大小的内存块就会在循环里直接return了,循环条件为
for (ptr = mem_to_ptr(lfree); ptr < MEM_SIZE_ALIGNED - size; ptr = ptr_to_mem(ptr)->next)
,这里有一组对应的函数mem_to_ptr()
和ptr_to_mem()
,前者是将mem
结构体指针转化为该mem
结构体的起始地址,后者是将一个mem
结构体起始地址转化为该结构体指针。进入循环体后,通过
mem = ptr_to_mem(ptr)
得到该结构体指针,进行以下两个判断:if ((!mem->used) && (mem->next - (ptr + SIZEOF_STRUCT_MEM)) >= size)
即通过
mem->used
查看该内存块是否被使用以及该内存块是否包含了能够满足需求内存大小size的最小内存空间。这里需要理解这个==最小==的含义,我们先来看这里判断正确后进一步的判断:if (mem->next - (ptr + SIZEOF_STRUCT_MEM) >= (size + SIZEOF_STRUCT_MEM + MIN_SIZE_ALIGNED))
即进一步对这个内存块的大小提出需求,除了要求的空间size,能不能再容纳一个
mem
结构体大小以及mem
结构体中包含的最小数据量MIN_SIZE_ALIGNED
(这里的最小数据量是指,如果要分配的内存大小小于这个值,那也会至少给你分配这么大的空间,这样做的目的是避免产生过小的内存碎片,因为假设有很多mem结构体只管理极小的空间却被分配了,那就会将这个内存分割成很多块,尽管可能有非常多的空闲内存,却可能无法满足分配请求,甚至如果一个mem结构体管理的内存空间是0,那么它就毫无意义了,因为他既不能分配,也没有管理。)。再来看上面的最小的含义,外层if
判断的最小是指这个mem
结构体管理的内存空间能够容纳需求的内存和自身所占空间(也就是SIZEOF_STRUCT_MEM
),内层判断的最小是这个mem
结构体管理的内存空间除了要容纳需求内存和自身所占空间,还要能够分配出一个管理了最小要求内存MIN_SIZE_ALIGNED
的mem
结构体,这个大小就是SIZEOF_STRUCT_MEM + MIN_SIZE_ALIGNED
如果正如我们所愿,当前这个
mem
结构体管理的空间非常大,那么我们就会分配需求的size大小内存,并紧接着该地址创建一个新的mem
结构体,将剩下的空间都交给他管理。上面的过程如图所示:
当然如果我们找到的空闲内存块仅仅够size大小,那么就不会创建这个新的
mem
结构体了,只是将当前mem
结构体标记为已使用。 -
3️⃣第三步:在得到了需求的内存后,就要更新当前的第一个空闲内存块了,也就是在
mem
链表中找到下一个没有被used的mem
.
mem_free函数
相比较分配的过程,释放会更加简单,主要的工作量在于两点,内存的合法性检查以及合并空闲内存块。
合法性检查就不说了,涉及了很多内存对齐和lwip内部的合法性检查相关内容,反正检查完后就会将mem->used = 0
,然后看看是否要更新lfree
,因为lfree
指向的永远是地址最小的那个空闲mem
结构体。
最后通过函数plug_holes(struct mem *mem)
合并空闲的内存块,源码不长,直接贴出来了,去掉了一些错误检查的代码,前一个和后一个合并的条件略有不同,因为后一个有可能是最后一个,还记得初始化mem
链表的时候么,最后一个是特殊的哨兵,不能被合并.
static void
plug_holes(struct mem *mem) /* 参数就是刚刚被释放的mem结构体 */
{struct mem *nmem; /* 下一个mem */struct mem *pmem; /* 上一个mem *//* 合并条件:1. 未被使用; 2.不是同一个;3.不是最后一个 */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;if (nmem->next != MEM_SIZE_ALIGNED) {ptr_to_mem(nmem->next)->prev = mem_to_ptr(mem);}}/* 合并条件:1.未被使用;2.不是同一个 */pmem = ptr_to_mem(mem->prev);if (pmem != mem && pmem->used == 0) {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内存堆管理的核心操作和内容就写完了.当然,其中省略了不少条件编译的内容,不过既然那些是作为条件编译的可选项,就说明并不是核心实现(起码不是基本功能的核心实现).除此之外,lwip内存管理也有一些别的功能函数,比如mem_trim()
用于尝试缩小一个已分配内存块的大小.剩下的就等碰到了再说了.
lwip内存池管理策略
lwip的内存池是可以自定义的,但是需要开启对应的宏定义MEMP_USE_CUSTOM_POOLS
,并且添加一个文件\lwippools.h
定义相关的内存池
如果用lwip自己的内存池,那么可以在文件\memp_std.h
中找到他们,都是通过一个宏LWIP_MEMPOOL
定义的,最后其实就是通过多层宏定义创建了一个这样的数组const struct memp_desc *const memp_pools[MEMP_MAX]
,并初始化其中的数组成员.
内存池其实就是使用空间换取时间,预先创建好非常多的各类的结构体成员以及内存,需要使用时直接拿来而非实时进行分配.
通过一个宏LWIP_MEMPOOL
定义的,最后其实就是通过多层宏定义创建了一个这样的数组const struct memp_desc *const memp_pools[MEMP_MAX]
,并初始化其中的数组成员.
内存池其实就是使用空间换取时间,预先创建好非常多的各类的结构体成员以及内存,需要使用时直接拿来而非实时进行分配.
这里就不对其相关函数展开细讲了,因为思路是差不多的,只是换了结构体,其实仍然是个链表结构体,如果后面有需求再补这部分的内容吧.