目录
1. 几种内存分配策略
常见的内存分配策略有两种,一种是分配固定大小的内存块;另一种是利用内存堆进行动态分配,属于可变长度的内存块。
两种内存分配策略都会在LwIP 中被使用到,他们各有所长,LwIP 的作者根据不同的应用场景选择不同的内存分配策略,这样子使得系统的内存开销、分配效率等都得到很大的提高。
此外LwIP 还支持使用C 标准库中的malloc和free 进行内存分配,但是这种内存分配我们不建议使用,因为C 标准库在嵌入式设备中使用会有很多问题,系统每次调用这些函数执行的时间可能都不一样,这是致命的,因为内存分配中最重要的就是分配时间效率的问题。
内存分配的本质就是事先准备一大块内存堆(可以理解为一个巨大的数组),然后将该空间起始地址返回给申请者,这就需要内核必须采用自己独有的一套数据结构来描述、记录哪些内存空间已经分配,哪些内存空间是未使用的,根据使用的机制不同,延伸出多种类型的内存分配策略。
1.1 固定大小的内存块
固定大小的内存块分配策略,用户只能申请大小固定的内存块,在内存初始化的时候,系统会将所有可用的内存区域划分为N 块固定大小的内存,然后将这些内存块通过单链表的方式连接起来,用户在申请内存块的时候就直接从链表的头部取出一个内存块进行分配,同理释放内存块的时候也是很简单,直接将内存块释放到链表的头部即可。
优点: 分配时间固定,高效,回收完全
缺点:只能申请固定大小的内存块,若实际使用过大则无法申请成功,若很小则造成资源浪费。
LwIP 中有很多固定的数据结构空间,如TCP 首部、UDP 首部,IP 首部,以太网首部等都是固定的数据结构,其大小就是一个固定的值,那么我们就能采用这种方式分配这些固定大小的内存空间,这样子的效率就会大大提高,并且无论怎么申请与释放,都不会产生内存碎片,这就让系统能很稳定地运行。这种分配策略在LwIP 中被称之为动态内存池分配策略。
1.2 可变长度分配
这种内存分配策略在很多系统中都会被使用到,系统运行的时候,各个空闲内存块的大小是不固定的,它会随着用户的申请而改变,刚开始的时候,系统就是一块大的内存堆,随着系统的运行,用户会申请与释放内存块,所以系统的内存块的大小。数量都会随之改变,并且对于这种内存分配策略是有多种不同的算法的。
LwIP 中也会使用这种内存分配策略,它采用First Fit(首次拟合)内存管理算法,申请内存时只要找到一个比所请求的内存大的空闲块,就从中切割出合适的块,并把剩余的部分返回到动态内存堆中,这种分配策略分配的内存块大小有限制,要求请求的分配大小不能小于MIN_SIZE,否则请求会被分配到 MIN_SIZE 大小的内存空间,一般 MIN_SIZE大小为 12 字节,在这 12 个字节中前几个字节会存放内存分配器管理用的私有数据,该数据区域不能被用户程序修改,否则导致致命问题。内存释放的过程是相反的过程,但分配器会查看该节点前后相邻的内存块是否空闲,如果空闲则合并成一个大的内存空闲块。当然,采用这种内存堆的分配方式,在申请和释放的时候肯定需要消耗时间。
优点: 内存浪费小,比较简单,适合用于小内存的管理
缺点:频繁的动态分配和释放,可能会造成严重的内存碎片,甚至,可能会导致内存分配不成功从而导致系统崩溃。
2. 动态内存池(POOL)
1.1 描述了原理及在lwIP使用的原因
2.1 内存池的预处理
在内核初始化时,会事先在内存中初始化相应的内存池,内核会将所有可用的区域根据宏定义的配置以固定的大小为单位进行划分,然后用一个简单的链表将所有空闲块连接起来,这样子就组成一个个的内存池。由于链表中所有节点的大小相同,所以分配时不需要查找,直接取出第一个节点中的空间分配给用户即可。
内核在初始化内存池的时候,是根据用户配置的宏定义进行初始化的。
如,用户定义了LWIP_UDP 这个宏定义,在编译的时候,编译器就会将与UDP 协议控制块相关的数据构编译编译进去,这样子就将LWIP_MEMPOOL(UDP_PCB,MEMP_NUM_UDP_PCB, sizeof(struct udp_pcb),"UDP_PCB")包含进去,在初始化的时候,UDP 协议控制块需要的POOL 资源就会被初始化,其数量由MEMP_NUM_UDP_PCB 宏定义决定。
不同协议的POOL 内存块的大小是不一样的,这由协议的性质决定。
如UDP 协议控制块的内存块大小是sizeof(struct udp_pcb),而TCP 协议控制块的POOL 大小则为sizeof(struct tcp_pcb)。通过这种方式,就可以将一个个用户配置的宏定义功能需要的POOL 包含进去,就使得编程变得更加简便。
很有意思的文件,memp_std.h, include/lwip/priv目录下,它里面全是宏定义,为了实现方便,在不同的地方调用#include "lwip/priv/memp_std.h"就能产生不同的效果。
该文件中的宏值定义全部依赖于宏LWIP_MEMPOOL(name,num,size,desc),这样,只要外部提供的该宏值不同,则包含该文件的源文件在编译器的预处理后,就会产生不一样的结果。这样,就可以通过在不同的地方多次包含该文件,前面必定提供宏值MEMPOOL以产生不同结果。一脸懵逼,这就是优秀的代码,只能666。
部分代码:
代码清单 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;
搭眼一看,完全看不懂,怎么枚举类型中还包含了这些东西,lwIP源码作者真的厉害。
#define LWIP_MEMPOOL(name,num,size,desc) MEMP_##name,
宏定义,## 是 C 中连接符(详细见链接: https://blog.csdn.net/XieWinter/article/details/99672923)
定义的枚举类型,经编译器处理后,代码如下:
typedef enum {
MEMP_RAW_PCB,
MEMP_UDP_PCB,
MEMP_TCP_PCB,
/* ... 省略 */
MEMP_MAX
} memp_t;
memp_t 类型在整个内存池的管理中是最重要的存在,通过内存池申请函数申请内存的时候,唯一的参数就是memp_t 类型的。
需要注意,在memp_std.h文件的最后需要对LWIP_MEMPOOL 宏定义进行撤销,因为该文件很会被多个地方调用,在每个调用的地方会重新定义这个宏定义的功能,所以在文件的末尾添加这句#undef LWIP_MEMPOOL 代码是非常有必要的。
按照这种包含头文件的原理,只需要定义LWIP_MEMPOOL 宏的作用,就能产生很大与内存池相关的操作,如在memp.c 文件的开头就定义了如下代码,可以看出这里就完成了各类型内存池的开辟等操作,优秀啊
#define LWIP_MEMPOOL(name,num,size,desc) LWIP_MEMPOOL_DECLARE(name,num,size,desc)
#include "lwip/priv/memp_std.h"
#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 \
};
2.2 内存池初始化
在LwIP 协议栈初始化的时候