协议栈内存管理
1.1 动态内存池管理
在LWIP中的内存分配策略与ucos中的两级内存管理类似(内存分区与内存块,每创建一个内存分区,都有特定数量大小的内存块构成链表供申请和释放),称之为动态内存池分配,不过内存池在初始化时会将可能用到的不同类型的内存分区全部初始化。由于每种类型的内存块大小相同,分配释放时不需要查找,直接取出或插入链表首部即可,所以前面说这种方式分配内存所需的时间很短,效率也很高。该内存分配方式可以称为固定长度内存分配,内存块的链接关系如下图示:
动态内存池POOL有多种类型,每种类型的单个POOL大小与POOL个数通常并不相同,因此要描述各种类型的POOL需要多个数据结构,在LWIP中与动态内存池管理相关的数据结构如下:
上表中的memp_t为系统定义一个枚举型数据类型,主要为系统将用到的各种类型的POOL取一个直观好记的名字,作为动态内存池函数申请空间的依据,下面先看下memp_t类型的定义:
// rt-thread\components\net\lwip-1.4.1\src\include\lwip\memp.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/memp_std.h" MEMP_MAX } memp_t; |
#define宏定义告诉编译器遇到LWIP_MEMPOOL(name,num,size,desc)这个宏就把它用MEMP_##name代替,其中“##”在C语言中是连接符,用于连接两个Token。#include包含头文件"lwip/memp_std.h",结合上面的宏定义,就是把该头文件里的宏LWIP_MEMPOOL(name,num,size,desc)替换为MEMP_##name。MEMP_MAX并不表示任何类型的POOL,在这里表示memp_t枚举类型中元素的总个数。先看一下"lwip/memp_std.h"头文件的部分代码:
// rt-thread\components\net\lwip-1.4.1\src\include\lwip\memp_std.h /* * A list of internal pools used by LWIP. * * LWIP_MEMPOOL(pool_name, number_elements, element_size, pool_description) * creates a pool name MEMP_pool_name. description is used in stats.c */ #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 */ #if IP_REASSEMBLY LWIP_MEMPOOL(REASSDATA, MEMP_NUM_REASSDATA, sizeof(struct ip_reassdata), "REASSDATA") #endif /* IP_REASSEMBLY */ #if IP_FRAG && !IP_FRAG_USES_STATIC_BUF && !LWIP_NETIF_TX_SINGLE_PBUF LWIP_MEMPOOL(FRAG_PBUF, MEMP_NUM_FRAG_PBUF, sizeof(struct pbuf_custom_ref),"FRAG_PBUF") #endif /* IP_FRAG && !IP_FRAG_USES_STATIC_BUF && !LWIP_NETIF_TX_SINGLE_PBUF */ #if LWIP_NETCONN LWIP_MEMPOOL(NETBUF, MEMP_NUM_NETBUF, sizeof(struct netbuf), "NETBUF") LWIP_MEMPOOL(NETCONN, MEMP_NUM_NETCONN, sizeof(struct netconn), "NETCONN") #endif /* LWIP_NETCONN */ ...... #undef LWIP_MEMPOOL ...... |
从上面的代码看,宏定义都是以条件编译方式定义的,也即编译时是否定义这个宏,看LwIP内核配置方式(在lwipopts.h与opt.h文件中)。下面按默认配置,假如系统配置了该条件编译选项,替换后的memp_t类型定义如下:
typedef enum { MEMP_RAW_PCB, MEMP_UDP_PCB, MEMP_TCP_PCB, MEMP_TCP_PCB_LISTEN, MEMP_TCP_SEG, ... MEMP_MAX } memp_t; |
其余的几个数据结构的定义跟memp_t有点类似,由于"lwip/memp_std.h"头文件最开头并没有#ifndef、#define、#endif这类条件编译语句,所以该头文件是可以多次编译的,该头文件最后还有#undef语句撤销宏定义,以便后续重新定义该宏的功能。
1.2 动态内存池操作
与动态内存池管理相关的系统函数有三个:(1)内存池初始化函数memp_init,在内核初始化时该函数必须被调用已完成内存池的建立;(2)内存池分配函数memp_malloc通常被内核调用,以实现内核中固定长度数据结构空间的申请;(3)内存池释放函数memp_free。
使用内存池分配内存的优点在于速度快、效率高,不会产生内存碎片。但其缺点在于只能分配某个固定大小的内存空间,系统必须事先知道用户的需求,即用户需要些什么类型的POOL以及每种类型POOL的个数,然后依照这个需求为用户在内存池中事先建立起内存池空间。这种分配方式只要在内核固定数据结构空间分配时被使用,如果用户对内核结构有足够的了解,就能灵活配置各个POOL的数量,达到优化系统性能的目的。
1.3 动态内存堆
在LWIP中实现了类似C语言库中malloc/free的内存分配策略,称之为动态内存堆分配。系统刚开始时,整个内存空间就是一个大的空闲块,随着内存分配和回收的进行,内存块的大小、数量也随着系统的运行而改变,某时刻内存中空闲块的链接关系如下:
类似于操作系统内存管理方法,若链表中存在多个空闲块,下次内存分配时选择哪个空闲块呢?常见的选择方式有如下三种:(1)首次拟合(从空闲链表头开始查找空闲块,将找到的第一个长度符合要求的空闲块分配给用户,并将该空闲块剩余空间重新组织为一个小的空闲块插入到链表中);(2)最佳拟合(从空闲链表中查找长度与需求最接近的空闲块分配给用户,为避免每次遍历整个链表,系统将各空闲块按从小到大顺序组织起来);(3)最差拟合(从最大空闲块中划分需求长度的内存空间给用户,为避免每次遍历整个链表,系统将各空闲块按从大到小顺序组织起来)。一般来说,最佳拟合适用于用户请求大小范围较广的系统;最差拟合适用于用户请求大小范围较窄的系统;首次拟合则介于两者之间。最佳拟合与最差拟合需要维护空闲块大小的有序性,时间效率也会差一些,所以LwIP中实现的动态内存堆分配采用的是首次拟合方式。
1.动态内存堆的描述
显然内存堆分配大小不能无限小,LWIP规定用户申请的内存块大小不能小于MIN_SIZE(通常为12字节),否则系统将自动将内存块大小设置为MIN_SIZE。动态内存堆分配的优点是内存浪费小、比较简单、适用于小内存管理,缺点是如果频繁的动态分配和释放可能造成严重的内存碎片。由于内存堆分配需要查找链表,所以时间效率比动态内存池分配要低一些。在LwIP中与动态内存堆管理相关的数据结构如下:
给出各数据类型实现代码:
u8_t ram_heap[MEM_SIZE_ALIGNED + (2*SIZEOF_STRUCT_MEM) + MEM_ALIGNMENT]; /** * 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 alignes. */ 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; }; /* MEM_SIZE would have to be aligned, but using 64000 here instead of * 65535 leaves some room for alignment... */ #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 */ /** 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; /** concurrent access protection */ #if !NO_SYS static sys_mutex_t mem_mutex; #endif |
其中全局数组ram_heap[]与内存池中的memp_memory[]类似,内存堆的内存空间也是通过定义全局数组分配的,而不是在堆Heap中分配的。其中MEM_SIZE_ALIGNED是内存堆大小MEM_SIZE(在lwipopts.h中定义)进行内存对齐后的值,SIZEOF_STRUCT_MEM是结构体mem进行内存对齐后的大小,MEM_ALIGNMENT是为了后续的内存对齐而附加进去的字节。内存堆管理模块靠每个内存块顶部放置的一个结构体mem来保存内存块分配信息,该结构体由两个指针(将各内存块组织成双向链表,指针实际保存的是目的地址相对内存堆空间起始地址的偏移量)和一个标志(该内存块是否已被分配)构成。内存堆的组织结构如下图示:
2.动态内存堆的操作
与内存堆管理相关的函数主要有三个:(1)内存堆初始化函数mem_init,在内核初始化时该函数必须被调用以完成内存堆的初始化;(2)内存堆分配函数mem_malloc;(3)内存堆释放函数mem_free。此外,还有个函数同内存堆的节点合并功能密切相关,即plug_holes。
内存堆管理还有两个操作函数,其功能描述如下:
操作函数 | 功能描述 |
void *mem_calloc(mem_size_t count, mem_size_t size) | 用于申请指定大小(count*size)且初始值全为0的内存空间 |
void *mem_trim(void *rmem, mem_size_t newsize) | 用于将一个内存区域重分配,该函数只能减少而不能增加已分配区域的空间大小 |
更多内容详见下一节:LWIP协议栈解析(三)——网络接口管理