1.内存管理是什么
内存管理是指计算机操作系统中对内存资源的有效分配、利用和释放的过程。
一般来讲,通用操作系统上边的内存管理的功能有:
-
内存分配:将可用的内存空间分配给程序或进程,以便它们执行任务。这通常涉及到将内存空间划分为不同的区域,如代码段、数据段和堆栈等。
-
内存保护:确保不同的程序或进程之间不能互相访问彼此的内存空间,以防止数据泄露或程序错误导致的内存污染。
-
内存映射:将程序所需的虚拟内存地址映射到物理内存地址,使得程序在执行时能够正确地访问和操作内存中的数据。
-
内存回收:当一个程序或进程不再需要某块内存空间时,将其释放回系统,以便其他程序或进程可以重复利用这些空间,从而提高内存利用率。
-
内存优化:对内存资源进行合理的管理和优化,以提高系统的性能和效率,例如采用内存压缩、缓存和虚拟内存等技术来提高内存的利用率和访问速度
针对嵌入式系统无MMU的MCU来说,内存管理就是一套能够实现有效的管理分配和回收机制、能够减少内存碎片的产生;若是事实性要求较高的场景,还需要内存分配的时间是确定可控的。
1.1 内存分配和回收
1.1.1 内存的初次分配
内存的初次分配是指在程序的链接阶段就将elf文件的section排布好,这部分都是通过链接脚本控制。如下图所示,工具链通过编译汇编过程生成目标文件时,会同时将程序使用不同的数据指定好放在哪个section;随后链接器会将各个目标文件中的不同段分门别类的汇总到一个文件中,所有Code段放在一起、所有RO-data段放在一起、所有RW-data放在一起。
其实在程序中还有一段ZI-data段(未初始化的全局变量和静态变量、初始化为0的变量),这部分为什么没有在可执行文件中?因为没必要**,ZI的数据全部是0,没必要开始就包含,只要程序运行之前将ZI数据所在的区域(RAM里面)一律清 0,不占用Flash,运行时候占用RAM。**
以上是编译链接的过程,此过程中已经将内存排布了一部分。下面来看elf文件在静止态和执行态是什么样的,
在单片机上运行时,实际仅将RW-data加载到了内存中运行,并且还将一段内存划分给ZI-data使用了。除去这两块,剩余的就是程序的栈、堆了。
栈和堆在编译完成后,并不能知道运行时实际占用的内存大小,不过可以知道栈、堆的最大空间和起始地址。以stm32系列单片机为例,在启动文件中通常就定义了stack和heap的大小,而在编译出来的Map文件中可以找到stack和heap的起始地址。
栈空间的分配和回收是不需要我们操心的,我们真正关注的是堆空间的那部分,这部分内存需要我们好好规划,才能够高效的利用起来。
1.1.2 堆内存的分配和回收
堆内存是作为动态内存来使用的,使用的原则是需要多少申请多少,使用完以后需要释放相应的内存空间。
在单片机中,堆内存的分配和回收方式也是多种的,有c库函数的malloc、free也有rtos实现的内存申请和释放函数,还有内存池方式的分配和释放。
这几种方式其实也是lwIP的分配方式,后文详细说明。
1.2 内存碎片
1.2.1 内存碎片是如何产生的?
随着内存不断被分配和释放,整个内存区域会产生越来越多的碎片(因为在使用过程中,申请了一些内存,其中一些释放了,导致内存空间中存在一些小的内存块,它们地址不连续,不能够作为一整块的大内存分配出去),系统中还有足够的空闲内存,但因为它们地址并非连续,不能组成一块连续的完整内存块,会使得程序不能申请到大的内存。
内存碎片分为内部碎片和外部碎片,它们的产生原因不同,但是实际呈现的效果是相同的。
内部碎片的产生:源于处理器架构需要字节对齐,以stm32单片机为例通常是4字节对齐的,并且在链接脚本中也会在每个section起始和结束的地方进行一次对齐。如图中是4字节对齐,所以直接使用c库的malloc函数申请内存时也是按照4字节对齐的。
如下示意图所示,绿色表示实际malloc的内存大小,红色表示因为需要字节对齐而多出来的内存碎片,这里仅分配了3个字节,但是产生了6字节的碎片。
外部碎片的产生:接着上图分析,对内存进行多次malloc后,释放第一次malloc的内存,即下图中白色部分的内存已经空闲,如果此时程序再次申请20字节的空间,这时前面释放的4字节不够,就需要另找空间进行分配。如果多次出现这种情况,内存中就会存在多个4字节的小空间分散在堆内存的各个角落,在分配大一点的内存空间时,即使这些小内存空间加起来大于待分配的内存,但苦于不是连续的,也没有办法成功分配。这就是外部碎片。
1.2.2 如何避免内存碎片?
有两种办法避免内存碎片:1、重启设备;2、设计一套内存管理机制。
-
重启设备:内存实际就是随机访问存储器,掉电后里边的数据就自动清空了,也就不存在什么内存碎片了,当然一些重要的数据也会跟着消失。但是对于一些设备来说,关机重启是不可接受的。
-
内存管理机制:rtos中通常都会提供好几种管理方式,例如freertos中的heap_1、heap_2、heap_3、heap_4、heap_5;rt-thread的内存池、slab等等。
除此之外,良好的编码习惯也可以减少内存碎片的产生。
2. lwIP内存管理方式与使用
2.1 内存堆管理策略与内存池方式
在lwIP中动态内存有三种管理方式:
-
使用标准c库malloc函数(需要定义MEM_LIBC_MALLOC=1)
-
使用多个不同尺寸的内存池作为堆内存进行分配(需要定义MEM_USE_POOLS=1和MEMP_USE_CUSTOM_POOLS=1)
-
使用轻量级的堆内存分配方案
lwIP中内存分配策略:
-
MEM_LIBC_MALLOC:该宏定义是否使用C 标准库自带的内存分配策略。该值默认情况下为0,表示不使用C 标准库自带的内存分配策略。即默认使用LwIP提供的内存堆分配策略。如果要使用C标准库自带的分配策略,则需要把该值定义为 1。
-
MEMP_MEM_MALLOC:该宏定义表示是否使用LwIP内存堆分配策略实现内存池分配( 即:要从内存池中获取内存时,实际是从内存堆中分配)。默认情况下为 0,表示不从内存堆中分配,内存池为独立一块内存实现。与MEM_USE_POOLS只能选择其一。
-
MEM_USE_POOLS:该宏定义表示是否使用LwIP内存池分配策略实现内存堆的分配( 即:要从内存堆中获取内存时,实际是从内存池中分配)。默认情况下为 0,表示不使用从内存池中分配,内存堆为独立一块内存实现。与MEMP_MEM_MALLOC只能选择其一。
MEMP_MEM_MALLOC | MEM_USE_POOLS | 内存分配策略 |
---|---|---|
0 | 0 | LwIP中默认的宏定义,内存池与内存堆独 立实现,互不相干。 |
0 | 1 | 内存堆的实现由内存池实现。 |
1 | 0 | 内存池的实现由内存堆实现。 |
1 | 1 | 不允许的方式。 |
lwIP内存堆、内存池的使用:
2.2 pbuf 内存管理
pbuf 就是一个描述协议栈中数据包的数据结构。在各层协议的实现中都可以看到它的身影。使用的方法也很简单
在申请 pbuf 时还需要注意选择什么类型的内存,lwIP 总共提供了 4 种类型的 pbuf,一般使用 PBUF_RAM 就满足需求了。
类型 | 作用 |
---|---|
PBUF_RAM | 通过内存堆分配,包含数据区域 |
PBUF_ROM | 通过内存池分配,但是仅有 pbuf 结构体部分数据,真正的数据区域在 ROM 中 |
PBUF_REF | 通过内存池分配,但是仅有 pbuf 结构体部分数据,真正的数据区域在 RAM 中 |
PBUF_POOL | 通过内存池分配,包含数据区域 |
3. lwIP内存管理的原理
因为不推荐使用c库提供的内存分配和释放方式,所以此处仅介绍lwIP内存堆、内存池的方式以及 pbuf 的管理方式。
3.1 lwIP内存堆的实现原理
3.1.1 内存堆的数据结构
内存堆的数据结构定义如下。其中next、prev并不是链表中的指针,而是表示的是内存块的位置,used表示该内存块是否已被使用。
ram指针指向分配的heap起始地址,ram_end指向内存堆中最后一个内存块;lfree指向内存堆中第一个可用的空闲内存块。
/**
* 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 {
/** 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;
};
/** pointer to the heap (ram_heap): for alignment, ram is now a pointer instead of an array */
static u8_t *ram;
/** the last entry, always unused! */
static struct mem *ram_end;
/** pointer to the lowest free block, this is used for faster search */
static struct mem *lfree;
几个需要关注的宏定义:
-
MIN_SIZE:表示内存块最小管理的内存空间不能小于此值
-
MIN_SIZE_ALIGNED:表示MIN_SIZE四字节对齐的值
-
SIZEOF_STRUCT_MEM:表示结构体struct mem大小对齐后的值
-
MEM_SIZE:定义了内存堆最大可以分配的大小
-
MEM_SIZE_ALIGNED:表示MEM_SIZE四字节对齐后的值
-
LWIP_RAM_HEAP_POINTER:用户可以定义内存堆的首地址;如果未定义,是使用LwIP定义的ram_heap数组作为内存堆,数组大小为MEM_SIZE_ALIGNED + (2U*SIZEOF_STRUCT_MEM);
/** All allocated blocks will be MIN_SIZE bytes big, at least!
* MIN_SIZE can be overridden to suit your needs. Smaller values save space,
* larger values could prevent too small blocks to fragment the RAM too much. */
#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)
/** If you want to relocate the heap to external memory, simply define
* LWIP_RAM_HEAP_POINTER as a void-pointer to that location.
* If so, make sure the memory at that location is big enough (see below on
* how that space is calculated). */
#ifndef LWIP_RAM_HEAP_POINTER
/** the heap. we need one struct mem at the end and some room for alignment */
LWIP_DECLARE_MEMORY_ALIGNED(ram_heap, MEM_SIZE_ALIGNED + (2U*SIZEOF_STRUCT_MEM));
#define LWIP_RAM_HEAP_POINTER ram_heap
#endif /* LWIP_RAM_HEAP_POINTER */
在程序实际运行过程中,有各种原因会导致内存块中的数据被意外改写,在内存块中加入一小块区域作为魔术字,通过判断魔术字是否被改写来判断内存卡是否被破环
#if MEM_OVERFLOW_CHECK
#define MEM_SANITY_OFFSET MEM_SANITY_REGION_BEFORE_ALIGNED
#define MEM_SANITY_OVERHEAD (MEM_SANITY_REGION_BEFORE_ALIGNED + MEM_SANITY_REGION_AFTER_ALIGNED)
#else
#define MEM_SANITY_OFFSET 0
#define MEM_SANITY_OVERHEAD 0
#endif
3.1.2 内存堆初始化
内存堆初始化后,这片内存的状态如图所示。初始化后整个堆实际上被分为了两块内存,第一块内存大小可以看作就是整个堆的大小(实际减去24字节的控制块长度),第二块内存大小为0,并且标记为已使用,不能进行分配。这样做的好处就是提供一个内存结束的标志。
lfree在初始化时指向第一个内存块,随着内存的分配和释放,lfree指针会一直指向第一个可用于分配的内存块。
mem_init函数源码如下:
-
检查SIZEOF_STRUCT_MEM的值是否4字节对齐
-
将全局ram指针设置为对齐后的内存堆的内存首地址,注意这边通过宏LWIP_MEM_ALIGN对LWIP_RAM_HEAP_POINTER地址对齐,因为LWIP_RAM_HEAP_POINTER有可能是开发者定义的值,未必是对齐的地址值;
-
从内存堆的首地址处切割出一个新的内存块,将第一个内存块进行各个字段初始化,其next的值设置为MEM_SIZE_ALIGNED(就是设置为struct mem X在数组中的位置),prev设置为0,说明这个内存卡是第一个内存管理块,并且used设置为0,说明是未使用的内存块区域;也就是说该内存块管理的内存空间有MEM_SIZE_ALIGNED - SIZEOF_STRUCT_MEM字节大小。
-
将ram_end指向数组ram[MEM_SIZE_ALIGNED]的地址,也就是struct mem X的地址,并且其prev/next的值是自身在数组中的相对位置,表明其是内存堆中的最后一个位置(used设置为1)
-
lfee指向第一个内存块的位置
-
LwIP中对各个模块都有特定的debug字段,此处avail字段记录的内存堆中的可用内存
-
初始化互斥锁
/**
* Zero the heap and initialize start, end and lowest-free
*/
void
mem_init(void)
{
struct mem *mem;
LWIP_ASSERT("Sanity check alignment",
(SIZEOF_STRUCT_MEM & (MEM_ALIGNMENT-1)) == 0); //1
/* align the heap */
ram = (u8_t *)LWIP_MEM_ALIGN(LWIP_RAM_HEAP_POINTER); //2
/* initialize the start of the heap */
mem = (struct mem *)(void *)ram;
mem->next = MEM_SIZE_ALIGNED;
mem->prev = 0;
mem->used = 0; //3
/* initialize the end of the heap */
ram_end = (struct mem *)(void *)&ram[MEM_SIZE_ALIGNED];
ram_end->used = 1;
ram_end->next = MEM_SIZE_ALIGNED;
ram_end->prev = MEM_SIZE_ALIGNED; //4
/* initialize the lowest-free pointer to the start of the heap */
lfree = (struct mem *)(void *)ram; //5
MEM_STATS_AVAIL(avail, MEM_SIZE_ALIGNED); //6
if (sys_mutex_new(&mem_mutex) != ERR_OK) { //7
LWIP_ASSERT("failed to create mem_mutex", 0);
}
}
3.1.3 内存分配
在内存堆初始化完成后,第一个内存块占据了全部的堆大小,若是在初始化之后进行内存分配,那么此时内存的状态如下图所示
主要的代码流程如下:
- 首先对输入的size参数进行对齐并且对其大小进行限定,如果小于MIN_SIZE_ALIGNED,则设置为size为MIN_SIZE_ALIGNED(避免过多的小内存块)
if (size < MIN_SIZE_ALIGNED) {
/* every data block must be at least MIN_SIZE_ALIGNED long */
size = MIN_SIZE_ALIGNED;
}
-
如果需要分配的内存大于MEM_SIZE_ALIGNED(最大可分配的内存),则直接返回NULL
-
内存分配之前先上锁,防止多线程访问问题
-
从lfree位置开始搜索未使用的内存块
for (ptr = (mem_size_t)((u8_t *)lfree - ram); ptr < MEM_SIZE_ALIGNED - size;
ptr = ((struct mem *)(void *)&ram[ptr])->next) {
}
- 查找空闲的内存块并且其大小要满足:因为将一个内存块切割一个新的内存块出来,除了需要的内存size外,还要一个struct mem结构体用于管理剩余的块,所以这边判断条件中需要加入SIZEOF_STRUCT_MEM
if ((!mem->used) &&(mem->next - (ptr + SIZEOF_STRUCT_MEM)) >= size)
- 如果找到的空闲块足够大,分配需要的内存后还比最小内存块(SIZEOF_STRUCT_MEM + MIN_SIZE_ALIGNED)还要大,就需要进行分割;否则直接将找到的空闲块设置为使用的,并将这边内存空间返回
if (mem->next - (ptr + SIZEOF_STRUCT_MEM) >= (size + SIZEOF_STRUCT_MEM + MIN_SIZE_ALIGNED)) {
...
}
- 定义一个mem_size_t的指针,指向切割后剩余内存块的起始地址(实际为数组索引号)
ptr2 = ptr + SIZEOF_STRUCT_MEM + size;
/* create mem2 struct */
mem2 = (struct mem *)(void *)&ram[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) {
((struct mem *)(void *)&ram[mem2->next])->prev = ptr2;
-
更新lfree指针,指向mem2
-
返回分配的内存指针
3.1.4 内存释放
lwIP释放堆内存函数根据用户释放的内存块地址,通过偏移mem结构体大小得到正确的内存块起始地址,并且根据mem中保存的内存块信息进行释放、合并等操作,并将used字段清零,表示该内存块未被使用。
为了防止内存碎片的出现,通过算法将内存相邻的两个空闲内存块进行合并,在释放内存块的时候,如果内存块与上一个或者下一个空闲内存块在地址上是连续的,那么就将这两个内存块进行合并。
以上图为例,在释放完第二个内存块后,再次释放了第三个内存块。
此时lwIP发现,第三个内存块前后都是未使用的内存块,那么lwIP首先去校验第三个内存卡的下一块是否使用中,若是未使用,那么将会合并掉,随后对上一内存块也做相同的操作。
内存释放的流程如下:
- 根据rmem指针找到管理此内存块的struct mem的地址,并标记此内存块为未使用的。
/* Get the corresponding struct mem ... */
/* cast through void* to get rid of alignment warnings */
mem = (struct mem *)(void *)((u8_t *)rmem - SIZEOF_STRUCT_MEM);
- 如果当前未使用的内存块的地址比空闲块的指针还小,则将lfree指向这个新释放的内存块
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);
plug_holes内存合并函数流程如下:
-
检查当前块的后一个块是否是空闲块。
-
如果当前块的后一个块是空闲块则将当前的next设置为下一个块的next,下一个块的prev设置为当前块的索引值;
-
如果lfree指向当前块的下一个块,则lfree需要指向当前块
-
nmem = (struct mem *)(void *)&ram[mem->next]; //②-1
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) { //②-2
lfree = mem;
}
mem->next = nmem->next;
((struct mem *)(void *)&ram[nmem->next])->prev = (mem_size_t)((u8_t *)mem - ram);
-
检查当前块的上一个块
-
如果当前块的上一个块未使用,则进行拼接;
-
如果lfree指向当前块,则lfree需要指向当前块的上一个块
-
设置上一个块的next的值为当前块的next,当前块的下一个块的prev设置为当前块的上一个块的下标值
-
/* plug hole backward */
pmem = (struct mem *)(void *)&ram[mem->prev];
if (pmem != mem && pmem->used == 0) {
/* if mem->prev is unused, combine mem and mem->prev */
if (lfree == mem) { //③-3
lfree = pmem;
}
pmem->next = mem->next;
((struct mem *)(void *)&ram[mem->next])->prev = (mem_size_t)((u8_t *)pmem - ram);
}
3.2 lwIP内存池的实现原理
为什么lwIP有了内存堆还要有内存池?
因为不同的协议报文所占用的内存是不同的,所有针对不同协议定制不同尺寸的内存池在处理报文时不用去频繁申请释放内存,也可以减少内存碎片的产生。
3.2.1 内存池的数据结构
内存池的描述结构体如下:
-
desc:内存池的字符串描述(在调试、内存池溢出检查、数据统计时定义该字段)
-
stats:内存池统计信息,包括可用内存,最大内存、已经分配的内存信息(定义MEMP_STATS时定义该字段)
-
size:记录内存池中每个内存块的大小
-
num:记录该内存池中有几个内存块
-
base:内存池的首地址
-
tab:通过此字段将内存块构成链表进行管理(指向空闲的第一个内存块)
结构体stats_mem定义了内存的使用信息
-
各字段如下:
-
name:此结构体变量的描述性信息(用于指示属于某个内存结构)
-
err: 内存分配出错的次数
-
avail:可用内存
-
used:已使用内存
-
max:最大内存
结构体memp定义了内存池中一个内存块的信息:
-
next:下一个内存块的地址
-
file:调用内存池分配函数的文件
-
line:调用内存池分配函数的行数
/** Memory pool descriptor */
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 */
};
/** Memory stats */
struct stats_mem {
#if defined(LWIP_DEBUG) || LWIP_STATS_DISPLAY
const char *name;
#endif /* defined(LWIP_DEBUG) || LWIP_STATS_DISPLAY */
STAT_COUNTER err;
mem_size_t avail;
mem_size_t used;
mem_size_t max;
STAT_COUNTER illegal;
};
#if !MEMP_MEM_MALLOC || MEMP_OVERFLOW_CHECK
struct memp {
struct memp *next;
#if MEMP_OVERFLOW_CHECK
const char *file;
int line;
#endif /* MEMP_OVERFLOW_CHECK */
};
#endif /* !MEMP_MEM_MALLOC || MEMP_OVERFLOW_CHECK */
以上的内存池结构体其实就可以抽象出内存池大概长什么样了:
3.2.2 内存池初始化
使用memp_init后内存池的状态如下图。图中的1、2、n为内存池中的一个内存块。
base指向第一个内存块,tab指向最后一个内存块。
第一个内存块的next指针为NULL,第二个内存块的next指向第一个内存块,依次类推;最终内存块的描述结构desc->tab指向内存池最后一个内存块。
memp_init函数的初始化流程:
- 定义一个内存池
LWIP_MEMPOOL_DECLARE(pool, 4, 16, "pool")
// 宏展开
u8_t memp_memory_mypool_base[4 * (MEMP_SIZE + 16)];
static struct stats_mem memp_stats_pool;
static struct memp *memp_tab_pool;
const struct memp_desc memp_pool = {
"pool",
&memp_stats_pool,
16,
4,
memp_memory_pool_base,
&memp_tab_pool
};
- 对栈上变量memp进行赋值
struct memp *memp;
memp = (struct memp *)LWIP_MEM_ALIGN(desc->base);
- 对memp开始的地址进行清零操作
/* force memset on pool memory */
memset(memp, 0, (size_t)desc->num * (MEMP_SIZE + desc->size
- 对内存块管理结构进行赋值(将各个内存块通过next指针链接起来)
/* create a linked list of memp elements */
for (i = 0; i < desc->num; ++i) { //④
memp->next = *desc->tab;
*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 = (struct memp *)(void *)((u8_t *)memp + MEMP_SIZE + desc->size);
}
3.2.3 内存分配
内存分配接口:
memp_malloc(const struct memp_desc *desc)
向内存池申请一个内存块后,内存池的状态:
内存池分配内存块的流程如下:
- 获取内存池第一个空闲块的地址
memp = *desc->tab;
- 将内存池空闲块的指针指向当前块的下一个,并将当前块的next设置为NULL
*desc->tab = memp->next;
memp->next = NULL;
- 将获取的空闲内存块的地址返回(此处是memp + MEMP_SIZE)
/* cast through u8_t* to get rid of alignment warnings */
return ((u8_t *)memp + MEMP_SIZE);
在内存池已经分配了一个内存块的基础上再次分配一个内存块后的状态:
3.2.4 内存释放
内存释放接口:
memp_free_pool(const struct memp_desc *desc, void *mem)
在上图的基础上释放一个内存块后,内存池的状态:
释放内存块的流程:
- 获取内存块的首地址
memp = (struct memp *)(void *)((u8_t *)mem - MEMP_SIZE);
- 将要释放的内存块的next指向内存池中的空闲块的地址
memp->next = *desc->tab;
- 将内存池的空闲块指针指向当前块
*desc->tab = memp;
3.3 pbuf 内存管理
3.3.1 pbuf 的数据结构
pbuf 的数据结构如下。
-
next 指针:有些协议报文特别长,一个 pbuf 无法容纳的时候,就需要再次申请 pbuf 管理剩余的报文数据,使用 next 指针链接。单链表
-
payload 指针:指向协议报文的数据部分。
-
tot_len:当前 pbuf 加上后续 pbuf 所有数据的长度
-
len:当前 pbuf 中有效数据长度(payload)
/** Main packet buffer struct */
struct pbuf {
/** next pbuf in singly linked pbuf chain */
struct pbuf *next;
/** pointer to the actual data in the buffer */
void *payload;
/**
* total length of this buffer and all next buffers in chain
* belonging to the same packet.
*
* For non-queue packet chains this is the invariant:
* p->tot_len == p->len + (p->next? p->next->tot_len: 0)
*/
u16_t tot_len;
/** length of this buffer */
u16_t len;
/** a bit field indicating pbuf type and allocation sources
(see PBUF_TYPE_FLAG_*, PBUF_ALLOC_FLAG_* and PBUF_TYPE_ALLOC_SRC_MASK)
*/
u8_t type_internal;
/** misc flags */
u8_t flags;
/**
* the reference count always equals the number of pointers
* that refer to this pbuf. This can be pointers from an application,
* the stack itself, or pbuf->next pointers from a chain.
*/
LWIP_PBUF_REF_T ref;
/** For incoming packets, this contains the input netif's index */
u8_t if_idx;
};
以下图为一个 pbuf 结构的示意:
3.3.2 pbuf 内存分配
pbuf 内存分配函数主要是 pbuf_alloc。源码可见 lwIP 源码的 pbuf.c 文件。
pbuf 主要应用于各层协议中,各个协议的报文头部长度是不同的,lwip 在分配内存时,也会考虑到这一点,会为每一层协议预留出内存,这样对于发送协议报文来讲,上层协议填充好报头和 payload 以后,直接交给下层协议处理,而下层协议直接在 pbuf 预留的内存中填充本层的报文信息即可。
下图是 pbuf_alloc 函数的流程:
3.3.3 pbuf 内存释放
pbuf 内存分配函数主要是 pbuf_free。源码可见 lwIP 源码的 pbuf.c 文件。
释放 pbuf 是有一个条件的,即当 pbuf 中的 ref 为 1 时,表示没有其他程序引用该 pbuf,那么此时 pbuf 才可以被释放,并且如果 pbuf 是一个链表时,会同时去遍历该链表上所有的 pbuf,去查看 ref 是否还有为 1 的,就这样连续释放下去。
pbuf_free 的流程如下:
注意事项:传入的 pbuf 必须为 pbuf 链表的首节点;如果不是首节点的话,有部分 ref 为 1 的 pbuf 没有得到回收,或者是有部分 pbuf 还未处理就已经被回收掉了。
4. 使用过程中的问题
- 使用freertos提供的malloc函数分配内存后,再次使用c库的realloc函数进行内存的重分配导致设备死机。