目录
一:概述
二:动态内存管理
2.1 内存管理的基本原理
2.2 内存管理使用的数据结构
2.3 内存管理的基本操作函数接口
三:常用结构体的内存分配与管理
3.1 基本原理
3.2 数据结构
3.3 函数接口
四:包缓冲管理
4.1 基本原理
4.2 数据结构
4.3 函数接口
Lwip中实现内存以及包缓冲管理的主要文件有如下三个:
mem.c:实现堆内存的管理
memp.c:实现对常用数据结构的管理
pbuf.c:实现对包缓冲的管理
所有这些内存在使用前都是分配好的,以数组的形式提供。这些全局变量的数组在程序的整个运行过程中都是不被释放的。对lwip来说,它们就像是已经存在的物理内存供自己使用。正如作者所说的,内存管理者使用系统所有内存的一个专有部分,这将保证网络系统不会使用所有其他可用内存,并且如果网络系统已经使用了它的所有内存,其他程序的操作也不会被干扰。
在系统内部,内存管理者通过在每一个已分配内存块上使用一个小的数据结构来跟踪已分配的内存。这个结构体含有两个指针用来指向其之前与之后已分配块的内存。它同样还有一个标识域用来指示分配块已经被分配了还是没有。
当在内存中查找到一个还没有被使用的分配块,并且对于分配请求而言其足够大,该内存块将被分配。这里,首次匹配原则被使用,也就是第一块满足条件的内存块将被首先使用。当一个分配块被释放时,标示域被置为0表示该内存块不再被使用,属于可分配块。为了避免碎片,之前与之后分配块的标示域将被检查,如果它们中的任何一个都没有被使用,这些块将被合并为一个更大的未使用块。
内存管理中使用结构体mem管理一块size大小的内存,该结构体的定义如下:
struct mem {
mem_size_t next, prev;
#if MEM_ALIGNMENT == 1
u8_t used;
#elif MEM_ALIGNMENT == 2
u16_t used;
#elif MEM_ALIGNMENT == 4
u32_t used;
#else
#error "unhandled MEM_ALIGNMENT size"
#endif /* MEM_ALIGNMENT */
};
在每一块已分配的内存块的顶端都有一个这样的结构体,所有内存块被链接为双向链表,全局变量struct mem*lfree指向最低的一个未使用的mem(lowest free)。该结构体中的next与prev都是内存在数组中的位置,因此,间接指向了某块内存。
函数接口:mem_init()
功能:内存初始化
操作:使用的内存为定义的ram数组,首先将其清零。初始化一个ram_end,表示内存尾部,将第一块的next指针指向该结构体。创建内存保护的信号量,最后将lfree指向第一块内存。初始化之后的内存区域如下图:
函数接口:mem_free(void *rmem)
功能:释放rmem指向的内存块
操作:首先进行参数检查,如果rmem为空,则直接返回;如果rmem指向的位置不在数组范围内,同样作为出错返回。否则,内部变量mem指向该块内存的起始位置,包括了管理这块内存的数据结构,并将表示该块内存是否使用的标识清零(表示这块内存已经被释放了)。调整lfree指针。最后调用函数plug_holes进行空闲块的整理工作。
函数接口:plug_holes(sturct mem *mem)
功能:空闲块整理
操作:首先创建两个本地局部变量 *nmem *pmem(n next p prev)。下面分别进行前向和回退整理:
前向整理:
nmem指向已释放的内存块的下一块。如果当前已释放的内存不是nmem指向的内存块,并且下一个的使用标识为零,并且下一个不在尾部,做如下两项操作:
1 如果lfree指向的空闲的内存块就是nmem指向的,则将其前移到当前已释放的内存块
2 将当前内存块的next值改为nmem的next值,nmem的下一个内存块的prev指向当前已释放的内存块。这样就完成了两个内存块的合并。
回退整理:
当前已释放的内存块的前向指针给pmem
如果它没有指向当前内存块,并且它的使用标识为零,同样做如下两项工作:
1 如果lfree指向当前内存块,则将其前移到当前内存块之前的内存块
2 pmem的next值改为当前内存块的next值,将当前内存块的下一个内存块的prev指针指向pmem。这样就完成了和前面内存块的合并工作。
通过以上,可以看出,对内存的释放只是将其使用标识清零,并且在每次释放后进行内存的整理,因为此时有可能产生了碎片(此操作只需查看临近的两块,不需要遍历操作)。另外,在整个的内存释放过程中对内存是进行保护的。
函数接口:mem_malloc(mem_size_t size)
功能:从ram中分配size大小的内存
操作:操作步骤如下:
如果参数size为零,则直接返回
如果参数大小不能够使内存对齐,则对其进行调整(通过扩大其值)
如果调整后的参数值大于我们内存空间,则返回
通过上述步骤的操作,形成了一个合法的参数,等待信号量,准备分配内存
整个内存分配工作可在一个for循环中完成,并且直接返回,如果执行到for循环外,则说明分配失败,可能是找不到合适的内存块,此时,释放信号量,返回
在for循环中完成如下的工作:
(循环条件:首先跳到第一个空闲内存块,每次移到next指向的位置,直到到达尾部为止)
获得第一个空闲内存块结构体的指针
如果当前内存块没有使用并且它的有效大小满足我们的分配请求,则进行内存分配工作(此时查看的大小至少要多出一个mem结构体的大小,以便于我们重新分配一个mem结构体进行内存的管理。所以每分配一次,就构建一个mem结构体,如果说当前内存块刚好够分配,最后的结果就是分配完后能够剩下一个mem结构体大小区域,即使它仅仅就是一个mem结构体)
首先根据上述条件,分配出一个新的mem结构体,对其的next 与prev指针进行设置,然后将剩余未使用块的used标识设为零,表示没有使用,而将刚才分配的内存块标识为使用
如果当前分配的内存块就是第一个空闲内存块,也就是说我们第一次就找到了匹配的内存块,此时,我们需要调整lfree指针到新的位置,也就是当前已分配内存块之后的第一个空闲内存块。否则,lfree目前仍是lowest free内存块,不需要改动(当前要分配的内存块可能在后面找到了,但此时当前最低的未分配的内存块的位置仍然没有变)。
分配完成,释放信号量并返回
如果当前内存块不满足我们的需求,则查看下一个空闲块,直到匹配成功为止,或者直到满足循环条件,跳出循环
函数接口:mem_realloc(void *rmem, mem_size_t newsize)
功能:在原有基础上调整已分配内存的大小
操作:
首先,类似于内存分配函数,对newsize参数进行调整,并判断参数范围的合法性信号量保护
判断参数rmem指向范围的合法性,是否在系统内存范围内
重新进行分配的前提是当前节点的大小大于新的size加上两个结构体本身的大小,这里的第二个结构体用于管理分配后剩余的内存,方便操作。
如果符合上述条件,则进行内存的调整,该操作主要是在原有的内存的基础上重新调整内存的大小,相比于原始的内存分配函数,它少了查找空闲块的操作,但同时多了合并相邻空闲块的操作。因为要缩减(至少是不扩大)原来的内存,肯定会产生一个尾巴,也就是碎片。
释放信号量,并返回内存指针
个人感觉该函数成功的可能性有两种:当前节点是最后一个节点或者新的大小小于原来的大小,因为分配方法是每次分的大小刚好就是请求的大小,并非按照某个固定大小进行分配,所以如果当前节点是分配好的中间节点,则不可能拿出其后的节点(假设其后是空节点)进行更大内存的分配。
函数名:mem_reallocm(void *rmem, mem_size_t newsize)
功能:使用新的参数重新进行内存分配
操作:
首先使用newsize作为参数直接调用mem_malloc进行内存的分配
如果成功了,拷贝newsize大小的内容到新的内存,释放原有的内存块,并返回新的内存指针
如果分配失败了,则直接调用mem_realloc操作,并返回。
同样,此处只可能是新的大小小于原来的大小或者当前节点是最后一个节点时才有可能成功。
memp.c文件实现的功能类似于Linux中的slab分配器。它为常用的数据结构预先准备固定数量的内存块,如果需要的话直接按类型进行索取就可以了。
程序中使用一个枚举类型的数据结构描述所有的要管理的数据结构体,如下:
typedef enum {
MEMP_PBUF,
MEMP_RAW_PCB,
MEMP_UDP_PCB,
MEMP_TCP_PCB,
MEMP_TCP_PCB_LISTEN,
MEMP_TCP_SEG,
MEMP_NETBUF,
MEMP_NETCONN,
MEMP_API_MSG,
MEMP_TCPIP_MSG,
MEMP_SYS_TIMEOUT,
MEMP_MAX
} memp_t;
从上面的定义中可以看出目前管理的结构体共有12个,可分为3类:第一类是与socket链接有关的pcb结构体和内存buf;第二类是协议栈与上下层进行信息交换的消息结构体;第三种就是管理各个定时事件所用的结构体。
Memp结构体用于将上述各个结构体成链,便于管理,如下:
struct memp {
struct memp *next;
};
同样,为了方便管理,还定义了如下几个数组:
memp_tab 该数组用来指向每一类结构体
memp_sizes 该数组的每一个元素给出了每一类结构体类型本身占用的内存大小
memp_num 该数组的每一个元素则给出了每一类型的结构体有几个
memp_memory 该数组为所有要管理的结构体提供存储空间。
函数名:memp_init(void)
功能:memp的初始化
操作:
初始化工作在两个for循环中完成,外层循环用来在各个类型间跳转,内存循环用来在某一类型的各个元素之间建立链。函数建链过程是倒的过程。
最后创建保护信号量,返回
最终初始化完成后的结果如下图所示:
函数名:memp_malloc(memp_t type)
功能:分配某一类型结构体
操作:
首先等待信号量,进行代码的保护
通过memp_tab获得这一类型结构体的当前首地址
如果为空,则说明已没有可分配或者没有创建,释放信号量,返回空
否则,memp_tab[i]指向下一个该类型结构体,将已经获得的该类型结构体的next指针置空
释放信号量,返回结构体的地址(指针)
函数名:memp_free(memp_t type, void *mem)
功能:释放某一类型结构体,上面函数的逆过程
操作:
如果mem为空,则为非法参数,返回
否则,等待信号量,进行代码保护
该结构体的next指针指向memp_tab[i],然后将memp_tab[i]指向该结构体(通过指针操作将其添加进去)
释放信号量
包缓冲的管理在pbuf.c文件中实现。实现思想在翻译中有详细描述。
包缓冲采用pbuf结构体进行管理,该结构体的定义如下:
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;
/** flags telling the type of pbuf, see PBUF_FLAG_ */
u16_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.
*/
u16_t ref;
};
该结构体的图示
首先定义一个pbuf缓冲池内存块,以后所有的pbuf的分配都从该缓冲池中提取。缓冲池大小的设置主要由PBUF_POOL_SIZE与PBUF_POOL_BUFSIZE决定,其中pbuf_pool_size设置了缓冲池中pbuf的数目,pbuf_pool_bufsize决定了缓冲池中每一个buf的大小。
函数名:pbuf_init(void)
功能:缓冲池的初始化
操作:
令pbuf_pool结构体指针指向缓冲池对齐的首地址,在为缓冲池分配内存时,已经考虑到对齐多分配了相应的字节,此时只是让pbuf_pool指向最开始的对齐地址。(这里的对齐地址就是地址的最低n位是零,也就是说pbuf_pool_memory的最开始几个字节可能不用)
对缓冲池中的数据结构进行初始化设置,初始化完成后的pbuf_pool_memory如下图
同时列出之前的进行比较
创建用于资源保护的信号量
函数名:pbuf_pool_alloc(void)
功能:在pbuf_pool缓冲池中取出一个pool类型的pbuf
操作:
等待信号量
从pbuf 缓冲池中取一个pbuf(如果为空,则表示已经分配完了)
Pbuf_pool指向当前缓冲池的头,也就是后移一个位置
释放信号量
返回取得的指针
函数名:pbuf_alloc(pbuf_layer l, u16_t length, pbuf_flag flag)
功能:分配一个给定类型的pbuf给调用者
操作:
该函数首先确定头的偏移量
如果为传输层,则添加传输层头空间,这里的传输层通常为TCP,大小通常为20字节
如果为IP层,添加IP层头空间,20字节
如果为链路层,添加链路层头空间,为14字节。为了对齐,我们实际使用的值是16
如果为raw类型,则不添加
其次,根据类型进行相应的分配工作
如果为pbuf_pool类型,调用pbuf_pool_alloc取得一个pbuf。设置payload指针,如果一个pbuf不能满足分配大小请求,则继续分配。如果在此过程中pbuf池中没有资源可分配了,则释放所有已分配的资源,返回。在每次分配成功时,设置相应结构体的值,并将它们串起来。此处需要注意的是,只有第一次设置payload指针时才会考虑头空间,因为一次的pbuf分配是针对一个包而言,所以后续的pbuf则只是数据而已,不再包含包头相关内容。
如果为pbuf_ram类型,则从内存中为其分配资源,调用函数mem_malloc进行分配。设置结构体,关键是其中的payload指针的定位。由于这时是在mem中进行分配,不像pbuf_pool是从缓冲池中取,所以一次就可分配完成(不管成功与否),不会进行串链操作。因为pbuf_pool类型的pbuf主要用于网卡部分的数据包的保存,从池中取可以加快分配速度,提高性能。
如果为pbuf_rom或者pbuf_ref类型的,只是从内存中为它们分配pbuf结构体,其payload的指向需要调用者来正确的设置。
PBUF_POOL_FAST_FREE(p)
对Pbuf pool的快速释放的宏
这里的快速释放只是修改指针,重新将pbuf串在pbuf池中。对应的,pbuf pool的释放PBUF_POOL_FREE(p)调用快速释放函数完成操作,只不过要添加信号量进行保护。
函数名:pbuf_realloc(struct pbuf *p, u16_t new_len)
功能:缩减一个pbuf链到需要的大小
操作:
如果新的大小大于当前的大小,则直接返回,当前还不支持扩展操作
跳过所有仍然需要保留的pbuf
如果为ram类型的pbuf,则调用mem_realloc重新进行内存大小的调整
如果为rom或者ref类型的pbuf,则只需要重新设置其length域
如果链中还有剩余的pbuf,则释放它们。
简而言之,该函数就是用来为pbuf瘦身的。
函数名:pbuf_header(struct pbuf *p, s16_t header_size_increment)
功能:调节pbuf中的payload指针和长度域。调整payload指针来隐藏或者显现负载中的头数据,也就是说让外部感知到或者感知不到负载中多携带的头部数据(比如tcp/udp/ip头等)
操作:
如果调整的大小为零或者pbuf不存在,则直接返回
保存当前的payload指针
判断pbuf的类型是否为pool或者ram类型的
如果是,则该pbuf中携带了负载数据,将payload指针调整到需要的位置。如果越界了,这里只判断了前向越界,也就是说payload指针可能指向到了pbuf结构体本身的数据,则将payload指针恢复到原来位置,返回
否则,判断如果是ref类型或者是rom类型,判断移动方向:
如果向前移动,也就是说扩大payload,此时有可能指向到pbuf结构体本身的内容,而这两种类型的pbuf本身是不携带payload的,所以不合逻辑,出错返回。
如果是向后移动,也就是让payload指针向前走,不支持,返回1
pbuf_hader()函数用来调节改变payload指针和长度域,以使得一个数据头能够按照计划加入到pbuf中
函数名:
功能:
操作:
以下图片来自网络: