目录
1.什么是内存管理
2.lwIP内存堆
3.lwIP内存堆程序代码解析
3.1 mem_init程序解析
3.2 mem_malloc程序解析
3.3 mem_free程序解析
4.lwIP内存池
5.lwIP内存池程序代码解析
5.1 实现lwIP内存池的文件
5.1.1 memp_priv.h
5.1.2 memp_std.h
5.1.3 memp.h
memp_t 枚举类型:
LWIP_MEMPOOL_DECLARE 宏定义:
5.1.4 memp.c
LWIP_MEMPOOL 指向 LWIP_MEMPOOL_DECLARE 宏定义:
const memp_pools[MEMP_MAX]数组 :
5.2 lwIP内存池函数
5.2.1 memp_init 函数和 memp_init_pool 函数
5.2.2 memp_malloc 函数和 memp_malloc_pool 函数
5.2.3 memp_free 函数与 memp_free_pool 函数
1.什么是内存管理
内存分配:申请一个大数组,完成后返回内存地址
内存释放:传入内存地址让算法进行释放
lwIP内存堆和内存池应用:
- 接收数据:MAC内核的数据(内存堆和内存池可适用)
- 发送数据:用户调用 lwIP 的 API 发起数据发送操作,需要存储要发送的数据以及相关的控制信息(lwIP一般选用内存堆申请内存)
- 用户调用:用户可主动调用lwIP的内存池和内存堆API接口申请内存
- 接口控制块:netconn、socket、raw接口,这些接口控制块通常具有固定的结构和大小(lwIP一般选用内存池申请内存)
- 构建消息:API消息、数据包消息,其大小通常相对固定或者在一定范围内(lwIP一般选用内存池申请内存)
2.lwIP内存堆
lwIP内存堆是一种可变长分配策略,可以随意申请任意大小的内存。
lwIP内存堆采用First Fit(首次拟合)内存算法
First Fit算法:从低地址空间往高地址空间查找,从中切割出合适的块,并把剩余的部分返回到动态内存堆中。
优点:
● 内存浪费小、比较简单,适合小内存管理
● 确保高地址空间具有足够的内存
● 要求分配最小值及相邻的空闲块合并(有效防止内存碎片)
缺点:
● 分配与释放频繁,会造成内存碎片
● 分配与释放时,从低地址开始查找,导致效率慢(以时间换空间原理)
3.lwIP内存堆程序代码解析
lwIP内存堆函数 | 描述 |
mem_init() | 内存堆初始化 |
mem_malloc() | 申请内存块 |
mem_free() | 释放内存块 |
…………………. | …………………. |
MEM_LIBC_MALLOC配置项必须设置为0 (这是C 标准库分配策略配置项,我们不用所以置0)
(1)内存堆控制块结构体:
struct mem { mem_size_t next; /* 保存下一个内存块的索引 */ mem_size_t prev; /* 保存前一个内存块的索引*/ u8_t used; /* 此内存快是否被用。1 使用、0 未使用 */
};
next、prev 变量用来保存下一个和前 一个内存块的索引,used 变量用来声明被管理的内存块是否可用
(2)内存堆的对齐及最小配置值
//② ********************** 最小分配内存 ******************
#ifndef MIN_SIZE
#define MIN_SIZE 12
#endif /* MIN_SIZE *///③ ********************** 对齐操作 ******************
#define MIN_SIZE_ALIGNED LWIP_MEM_ALIGN_SIZE(MIN_SIZE) // 最小分配内存大小对齐 --12字节
#define SIZEOF_STRUCT_MEM LWIP_MEM_ALIGN_SIZE(sizeof(struct mem)) // 内存控制块对齐---8字节
#define MEM_SIZE_ALIGNED LWIP_MEM_ALIGN_SIZE(MEM_SIZE) // 内存堆对齐--lwipopts.h文件下MEM_SIZE配置项
内存对齐的作用:
1,平台原因:不是全部的硬件平台都能访问随意地址上的随意类型数据的;某些硬件平台仅仅能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2,性能原因:经过内存对齐后,CPU 的内存访问速度大大提升。
(3)定义内存堆的空间
//********************** 内存堆定义--大数组 ******************
LWIP_DECLARE_MEMORY_ALIGNED(ram_heap, MEM_SIZE_ALIGNED + (2U * SIZEOF_STRUCT_MEM));#define LWIP_DECLARE_MEMORY_ALIGNED(variable_name, size)
u8_t variable_name[LWIP_MEM_ALIGN_BUFFER(size)]
无论是内存堆还是内存池,它们都是对一个大数组进行操作,上述的宏定义就是指向一个名为 ram_heap 数组,该数组的大小为 MEM_SIZE_ALIGNED + (2U*SIZEOF_STRUCT_MEM),lwIP 内存堆申请的内存就是从这个数组分配得来的。
(4)操作内存堆的变量
/* 指向对齐后的内存堆的地址*/
static u8_t *ram;
/* 指向对齐后的内存堆的最后一个内存块 */
static struct mem *ram_end; /* 指向已被释放的索引号最小的内存块(内存堆最前面的已被释放的)*/
static struct mem * LWIP_MEM_LFREE_VOLATILE lfree;
ram 指针:指向对齐后的内存堆总空间首地址
ram_end 指针:指向内存堆总空间尾地址(接近总空间的尾地址)
lfree 指针:指向最低内存地址的空闲内存块。
注:lwIP 内核就是根据 lfree 指针指向空闲内存块来分配内存,而 ram_end 指针用来检测该总 内存堆空间是否有空闲的内存。
3.1 mem_init程序解析
void mem_init(void)
{struct mem *mem;// 步骤1: 对内存堆的地址进行对齐操作,得到指向对齐后内存堆起始位置的指针ram = (u8_t *)LWIP_MEM_ALIGN(LWIP_RAM_HEAP_POINTER);// 步骤2: 建立第一个内存块mem = (struct mem *)(void *)ram;// 步骤3: 初始化第一个内存块的相关信息// 下一个内存块不存在,因此指向内存堆的结束mem->next = MEM_SIZE_ALIGNED;// 前一个内存块就是它自己,因为这是第一个内存块mem->prev = 0;// 第一个内存块没有被使用mem->used = 0;// 步骤4: 初始化堆的末端ram_end = ptr_to_mem(MEM_SIZE_ALIGNED);// 步骤5: 初始化堆末端内存块的相关信息// 最后一个内存块被使用,因为其后面没有可用空间,必须标记为已被使用ram_end->used = 1;// 下一个不存在,因此指向内存堆的结束ram_end->next = MEM_SIZE_ALIGNED;// 前一个不存在,因此指向内存堆的结束ram_end->prev = MEM_SIZE_ALIGNED;// 步骤6: 设置已释放的索引最小的内存块lfree = (struct mem *)(void *)ram;// 步骤7: 建立互斥信号量,用于内存的申请和释放保护if (sys_mutex_new(&mem_mutex) != ERR_OK) {LWIP_ASSERT("failed to create mem_mutex", 0);}
}
索引:struct mem结构体的 next 和 prev 变量并不是指针类型,它们保存的是内存块的索引, 例如定义一个 a[10]数组,next 和 prev 保存的是 0~9 的索引号,lwIP 内核根据索引号获取 a 数组的索引地址(&a[0~9])。
3.2 mem_malloc程序解析
void *
mem_malloc(mem_size_t size_in)
{ mem_size_t ptr, ptr2, size; struct mem *mem, *mem2; /*******第一:检测用户申请的内存块释放满足 LWIP 的规则*******/ /*******第二:从内存堆中划分用户的内存块******/ /* 寻找足够大的空闲块,从最低的空闲块开始.*/ for (ptr = mem_to_ptr(lfree); ptr < MEM_SIZE_ALIGNED - size; ptr = ((struct mem *)(void *)&ram[ptr])->next) { mem = ptr_to_mem(ptr); /* 取它的地址 */ /* 空间大小必须排除内存块头大小 */if ((!mem->used) && (mem->next - (ptr + SIZEOF_STRUCT_MEM)) >= size) { /* 这个地方需要判断 剩余的内存块是否可以申请 size 内存块 */ if (mem->next - (ptr + SIZEOF_STRUCT_MEM) >= (size + SIZEOF_STRUCT_MEM + MIN_SIZE_ALIGNED)) { /* 上面注释一大堆,主要就是说, 剩余内存可能连一个内存块的头都放不下了, 这个时候就没法新建空内存块。其索引也就不能移动 */ /* 指向申请后的位置,即: 建立下一个未使用的内存块的头部。 即:插入一个新空内存块 */ ptr2 = (mem_size_t)(ptr + SIZEOF_STRUCT_MEM + size); /*从 Ptr2 地址开始创建 mem2 的结构体 */ mem2 = ptr_to_mem(ptr2);/* 调用(struct mem *)(void *)&ram[ptr]; */ mem2->used = 0; /* 这个根据下面的 if(mem2->next != MEM_SIZE_ALIGNED)判定 */ mem2->next = mem->next; mem2->prev = ptr; /* 空闲内存块的前一个指向上面分配的内存块 */ /* 前一个内存块指向上面建立的空闲内存块 */ mem->next = ptr2; mem->used = 1;/* 将当前分配的内存块标记为 已使用 */ /* 如果 mem2 内存块的下一个内存块不是链表中最后一个内存块 (结束地址), 那就将它下一个的内存块的 prve 指向 mem2 */ if (mem2->next != MEM_SIZE_ALIGNED) { ((struct mem *)(void *)&ram[mem2->next])->prev = ptr2; }} else {/* 内存块太小了会产生的碎片 */ mem->used = 1; }/* 这里处理:当分配出去的内存正好是 lfree 时, 因为该内存块已经被分配出去了, 必须修改 lfree 的指向下一个最其前面的已释放的内存块*/ if (mem == lfree) { struct mem *cur = lfree;/* 只要内存块已使用且没到结尾,则继续往后找 */ while (cur->used && cur != ram_end) { cur = ptr_to_mem(cur->next);/* 下一个内存块 */ }/* 指向找到的 第一个已释放的内存块。如果上面没有找到,则 lfree = lfree 不变 */ lfree = cur; }/* 这里返回 内存块的空间的地址,排除内存块的头 */ return (u8_t *)mem + SIZEOF_STRUCT_MEM + MEM_SANITY_OFFSET; } } return NULL;
}
3.3 mem_free程序解析
void
mem_free(void *rmem)
{ struct mem *mem; /* 第一步:检查内存块的参数 */ /* 判断释放的内存块释放为空 */ if (rmem == NULL) { return;/* 为空则返回 */ }/* 除去指针就剩下内存块了,通过 mem_malloc 的到的地址是不含 struct mem 的 */ mem = (struct mem *)(void *)((u8_t *)rmem - (SIZEOF_STRUCT_MEM + MEM_SANITY_OFFSET)); /* 第二步:查找指定的内存块,标记为未使用 */ mem->used = 0; /* 第三步:需要移动全局的释放指针,因为 lfree 始终指向内存堆中最小索引的那个已经释放的内存块 */ if (mem < lfree) { /* 新释放的结构现在是最低的 */ lfree = mem; }
}
lwIP 内存堆释放内存是非常简单的,它一共分为三个步骤:
- 检测传入的地址是否正确
- 对这个地址进行偏移,偏移大小为 struct mem,这样可以得到释放内存的控制块首地址,并且设置该控制块为未使用标志,
- 判断该控制块的地址是否小于 lfree 指针指 向的地址,若小于,则证明 mem 的内存块在 lfree 指向的内存块之前即更接近堆空间首地址, 系统会把 lfree 指针指向这个释放的内存块(控制块 + 可用内存),以后申请内存时会在 lfree 指针的内存块开始查找合适的内存。
4.lwIP内存池
lwIP内存池是把连续的内存分成多个大小相同的内存空间,以链表的形式链接起来
以空间换时间原理
内存分配:无需切割,直接分配内存空间
内存释放:释放便捷,无需与相邻合并
优点:
● 分配速度快
● 防止内存碎片
● 回收便捷
缺点:
● 资源浪费
● 申请大型内存时,可能申请失败
5.lwIP内存池程序代码解析
5.1 实现lwIP内存池的文件
5.1.1 memp_priv.h
这个文件主要定义了两个结构体,分别为 memp 和 memp_desc 结构体:
memp 结构体:把同一类型的内存池以链表的形式链接起来
memp_desc 结构体:管理和描述各类型的内存池,如数量、大小、内存池的起始地址和指向空闲内存池的指针。
struct memp
{ struct memp *next; /*指向下一个控制块节点*//*其他的成员变量一般用不到*/
};
/* 内存池描述符 */
struct memp_desc
{
u16_t size; /* 每个内存块的大小 */
u16_t num; /* 内存块的数量 */
u8_t *base; /* 指向内存的基地址 */
struct memp **tab; /* 指向第一个空闲块 */
};
5.1.2 memp_std.h
该文件定义了 lwIP 内核所需的内存池,由于 lwIP 内核的固定数据结构多种多样,所以它 们使用宏定义声明是否使用该类型的内存池,如 TCP、UDP、DHCP、ICMP 等协议。
#if LWIP_RAW
LWIP_MEMPOOL(RAW_PCB, MEMP_NUM_RAW_PCB, sizeof(struct raw_pcb),"RAW_PCB")
#endif /* LWIP_RAW */ #if LWIP_UDP
LWIP_MEMPOOL(UDP_PCB, MEMP_NUM_UDP_PCB, sizeof(struct udp_pcb), "UDP_PCB")
#endif /* LWIP_UDP */ #if LWIP_TCP
LWIP_MEMPOOL(TCP_PCB, MEMP_NUM_TCP_PCB, sizeof(struct tcp_pcb), "TCP_PCB")
LWIP_MEMPOOL(TCP_PCB_LISTEN, MEMP_NUM_TCP_PCB_LISTEN,
sizeof(struct tcp_pcb_listen), "TCP_PCB_LISTEN")
LWIP_MEMPOOL(TCP_SEG, MEMP_NUM_TCP_SEG, sizeof(struct tcp_seg), "TCP_SEG")
#endif /* LWIP_TCP */
/* …………………………………………………………………………………忽略以下源码………………………………………………………………………………………
*/
注意:
1. 不同类型的内存池是由相应的宏定义声明启用
2. LWIP_MEMPOOL 宏定义用来初始化各类型的内存池
5.1.3 memp.h
主要是memp_t 枚举类型和 LWIP_MEMPOOL_DECLARE 宏定义
memp_t 枚举类型:
/* 获取描述符的数量 */
typedef enum
{
#define LWIP_MEMPOOL(name,num,size,desc) MEMP_##name,
#include "lwip/priv/memp_std.h"
MEMP_MAX
} memp_t;
##是连接符号,这里表示MEMP_后面接上 LWIP_MEMPOOL(name,num,size,desc) 中的参数 name :MEMP_name
举例:
如果 memp_std.h 文件只 启用了 LWIP_RAW 和 LWIP_UDP 类型的内存池,那么 MEMP_MAX 变量就等于 2
typedef enum {
MEMP_RAW_PCB,
MEMP_UDP_PCB, MEMP_MAX
} memp_t;
LWIP_MEMPOOL_DECLARE 宏定义:
此宏定义非常重要,各类型的内存池都使用这个宏定义声明,例如内存池的内存由来,各 类型内存池的数量、大小、内存由来的地址以及指向空闲的指针。
#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) \\// 声明一个静态的 memp 指针,用于指向内存池的表头static struct memp *memp_tab_ ## name; \\// 定义一个常量的 memp_desc 结构体,用于描述内存池的信息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 \};
这个宏定义展开后如下源码所示:
#define LWIP_MEMPOOL_DECLARE(name, num, size, desc) \/* 定义一个用于存储内存池数据的数组 */u8_t memp_memory_ ## name ## _base[((((((num) * (MEMP_SIZE + (((size) + \MEM_ALIGNMENT - 1U) & ~(MEM_ALIGNMENT - 1U))))) + MEM_ALIGNMENT - \1U)))]; \/* 声明一个静态指针,用于指向内存池的管理表 */static struct memp *memp_tab_ ## name; \/* 定义一个常量结构体,用于描述内存池的信息 */const struct memp_desc memp_ ## name = { \/* 每个内存块的对齐后的大小 */LWIP_MEM_ALIGN_SIZE(size), \/* 内存池中内存块的数量 */(num), \/* 内存池的起始地址 */memp_memory_ ## name ## _base, \/* 指向内存池管理表的指针 */&memp_tab_ ## name \};
展开之后可以看出,各类型的内存池的内存由来和 lwIP 内存堆一样,都是由数组分配的。
5.1.4 memp.c
LWIP_MEMPOOL 指向 LWIP_MEMPOOL_DECLARE 宏定义:
#define LWIP_MEMPOOL(name,num,size,desc) \ LWIP_MEMPOOL_DECLARE(name,num,size,desc)
#include "lwip/priv/memp_std.h"
举例:
memp_std.h 只启用 LWIP_RAW 和 LWIP_UDP 类型的内存池,展开之后如下所示:
// 定义 RAW_PCB 内存池相关
// 定义存储 RAW_PCB 内存池数据的数组
u8_t memp_memory_RAW_PCB_base[((((((num) * (MEMP_SIZE +
(((size) + MEM_ALIGNMENT - 1U) & ~(MEM_ALIGNMENT - 1U))))) +
MEM_ALIGNMENT - 1U)))];
// 声明指向 RAW_PCB 内存池管理表的静态指针
static struct memp *memp_tab_RAW_PCB;
// 定义描述 RAW_PCB 内存池信息的常量结构体
const struct memp_desc memp_RAW_PCB = {// 每个内存块的对齐后的大小LWIP_MEM_ALIGN_SIZE(size),// 内存池中内存块的数量(num),// RAW_PCB 内存池的起始地址memp_memory_RAW_PCB_base,// 指向 RAW_PCB 内存池管理表的指针&memp_tab_RAW_PCB
};// 定义 UDP_PCB 内存池相关
// 定义存储 UDP_PCB 内存池数据的数组
u8_t memp_memory_UDP_PCB_base[((((((num) * (MEMP_SIZE +
(((size) + MEM_ALIGNMENT - 1U) & ~(MEM_ALIGNMENT - 1U))))) +
MEM_ALIGNMENT - 1U)))];
// 声明指向 UDP_PCB 内存池管理表的静态指针
static struct memp *memp_tab_UDP_PCB;
// 定义描述 UDP_PCB 内存池信息的常量结构体
const struct memp_desc memp_UDP_PCB = {// 每个内存块的对齐后的大小LWIP_MEM_ALIGN_SIZE(size),// 内存池中内存块的数量(num),// UDP_PCB 内存池的起始地址memp_memory_UDP_PCB_base,// 指向 UDP_PCB 内存池管理表的指针&memp_tab_UDP_PCB
};
const memp_pools[MEMP_MAX]数组 :
const struct memp_desc* const memp_pools[MEMP_MAX] = {
#define LWIP_MEMPOOL(name,num,size,desc) &memp_ ## name,
#include "lwip/priv/memp_std.h"
};
举例:
若 memp_std.h 只启用 LWIP_RAW 和 LWIP_UDP 类型的内存池,这个数组展开之后如下所示:
const struct memp_desc* const memp_pools[MEMP_MAX] = { &memp_memp_RAW_PCB, &memp_memp_UDP_PCB,
};
5.2 lwIP内存池函数
lwIP内存池函数 | 描述 |
memp_init() | 内存池初始化 |
memp_malloc() | 申请内存池 |
memp_free() | 释放内存池 |
…………………. | …………………. |
5.2.1 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_init_pool(memp_pools[i]); }
} // 初始化单个内存池
void memp_init_pool(const struct memp_desc *desc)
{ int i; struct memp *memp; // 初始化内存池的表头指针为 NULL*desc->tab = NULL; /* 内存对齐 */ // 对内存池的起始地址进行对齐操作memp = (struct memp*)LWIP_MEM_ALIGN(desc->base); /* 将内存块链接成链表形式 */ for (i = 0; i < desc->num; ++i) { // 将当前内存块的 next 指针指向当前表头memp->next = *desc->tab; // 更新表头指针为当前内存块*desc->tab = memp; /* 地址偏移 */// 移动指针到下一个内存块的位置memp = (struct memp *)(void *)((u8_t *)memp + MEMP_SIZE + desc->size); }
}
5.2.2 memp_malloc 函数和 memp_malloc_pool 函数
memp_malloc 函数需要传入申请内存池的类型,如 UDP_PCB…,接着根据传入的类型来 查找对应的内存池描述符,查找完成之后根据该内存池描述符的 tab 指针指向内存池分配给用户,并且把 tab 指针偏移至下一个空闲内存池。
// 从指定类型的内存池中分配内存块
void *
memp_malloc(memp_t type)
{ void *memp; // 调用 do_memp_malloc_pool 函数从对应内存池分配内存memp = do_memp_malloc_pool(memp_pools[type]); return memp;
} // 实际执行内存池分配操作的函数
static void*
do_memp_malloc_pool(const struct memp_desc *desc)
{ struct memp *memp; // 获取内存池表头指向的内存块memp = *desc->tab; if (memp != NULL) { // 若内存块存在,更新表头指针指向下一个内存块*desc->tab = memp->next; // 返回排除管理信息部分的内存块地址return ((u8_t*)memp + MEMP_SIZE); } else {// 这里原代码没有处理逻辑,可根据实际情况添加错误处理等代码}// 若没有可用内存块,返回 NULLreturn NULL;
}
5.2.3 memp_free 函数与 memp_free_pool 函数
释放函数非常简单,只需对内存池描述符的 tab 指针偏移至释放的内存池。
// 释放指定类型内存池中的内存块
void memp_free(memp_t type, void *mem)
{ // 判断内存块的起始地址是否为空if (mem == NULL) { return; } // 调用 do_memp_free_pool 函数执行实际的释放操作do_memp_free_pool(memp_pools[type], mem);
} // 实际执行内存池内存块释放操作的函数
static void do_memp_free_pool(const struct memp_desc* desc, void *mem)
{ struct memp *memp; // 根据内存块的地址偏移得到内存块的起始地址memp = (struct memp *)(void *)((u8_t*)mem - MEMP_SIZE); // 内存块的下一个就是链表中的第一个空闲内存块memp->next = *desc->tab; // *desc->tab 指向 memp 内存块*desc->tab = memp;
}