5、 LwIP 的内存管理

1 、几种内存分配策略

  LwIP 本质就是对数据的处理,网络中的数据是非常多的,那么 LwIP 对这些数据的处理必然是需要消耗系统资源的,而有好的内存管理策略就显得非常必要了,内存分配策略、内存分配效率等都是衡量系统性能的重要因素。

  常见的内存分配策略有两种,一种是分配固定大小的内存块;另一种是利用内存堆进行动态分配,属于可变长度的内存块。

  这两种内存分配策略都会在 LwIP 中被使用到,他们各有所长, LwIP 的作者根据不同的应用场景选择不同的内存分配策略,这样子使得系统的内存开销、分配效率等都得到很大的提高。 此外 LwIP 还支持使用 C 标准库中的 malloc和 free 进行内存分配,但是这种内存分配我们不建议使用,因为 C 标准库在嵌入式设备中使用会有很多问题,系统每次调用这些函数执行的时间可能都不一样,这是致命的,因为内存分配中最重要的就是分配时间效率的问题。 内存分配的本质就是事先准备一大块内存堆(可以理解为一个巨大的数组),然后将该空间起始地址返回给申请者, 这就需要内核必须采用自己独有的一套数据结构来描述、记录哪些内存空间已经分配,哪些内存空间是未使用的,根据使用的机制不同,延伸出多种类型的内存分配策略。

  1.1、 固定大小的内存块

  使用固定大小的内存块分配策略,用户只能申请大小固定的内存块,在内存初始化的时候,系统会将所有可用的内存区域划分为 N 块固定大小的内存,然后将这些内存块通过单链表的方式连接起来,用户在申请内存块的时候就直接从链表的头部取出一个内存块进行分配,同理释放内存块的时候也是很简单,直接将内存块释放到链表的头部即可,这样子分配内存的时间就是固定的,非常高效。但是缺点也是很明显的,用户只能申请固定大小的内存块,如果内存块无法满足用户的需求,那么则无法申请成功,而如果将内存块大小变大,那么在用户需要极小的内存的时候就会造成内存的浪费,这也是不适合的。

  可能会有人问了,那这种内存分配策略不好用,为什么 LwIP 作者会使用呢?其实不然, LwIP 中有很多固定的数据结构空间,如 TCP 首部、 UDP 首部, IP 首部,以太网首部等都是固定的数据结构,其大小就是一个固定的值,那么我们就能采用这种方式分配这些固定大小的内存空间,这样子的效率就会大大提高,并且无论怎么申请与释放,都不会产生内存碎片,这就让系统能很稳定地运行。这种分配策略在 LwIP 中被称之为动态内存池分配策略,内存池示意图具体见下图:

  1.2、可变长度分配

  这种内存分配策略在很多系统中都会被使用到,系统运行的时候,各个空闲内存块的大小是不固定的,它会随着用户的申请而改变,刚开始的时候,系统就是一块大的内存堆,随着系统的运行,用户会申请与释放内存块,所以系统的内存块的大小。数量都会随之改变,并且对于这种内存分配策略是有多种不同的算法的。

  LwIP 中也会使用这种内存分配策略,它采用 First Fit(首次拟合)内存管理算法, 申请内存时只要找到一个比所请求的内存大的空闲块,就从中切割出合适的块,并把剩余的部分返回到动态内存堆中, 这种分配策略分配的内存块大小有限制,要求请求的分配大小不能小于 MIN_SIZE,否则请求会被分配到 MIN_SIZE 大小的内存空间, 一般 MIN_SIZE大小为 12 字节,在这 12 个字节中前几个字节会存放内存分配器管理用的私有数据,该数据区域不能被用户程序修改,否则导致致命问题。内存释放的过程是相反的过程,但分配器会查看该节点前后相邻的内存块是否空闲,如果空闲则合并成一个大的内存空闲块。 当然,采用这种内存堆的分配方式,在申请和释放的时候肯定需要消耗时间,可以类似地看做是以时间换空间的策略。 采用这种分配策略,其优点就是内存浪费小,比较简单,适合用于小内存的管理,其缺点就是如果频繁的动态分配和释放,可能会造成严重的内存碎片,如果在碎片情况严重的话,可能会导致内存分配不成功从而导致系统崩溃。

  补充:存碎片导致系统崩溃的原因并不是因为系统没有可用内存了,而是内存块被分割成很多不连续的小内存块,当用户需要申请一个更大的内存块的时候,系统没办法提供这样子的内存块,就会导致申请失败。

  当然 LwIP 也支持 C 标准库的 malloc()和 free(),因为不建议使用这种情况,所以此处我们就不做过多的讲解。

2、动态内存池(POOL)

  申请大小必须是指定固定大小字节的值(如 4、 8、 16 等等),系统将所有可用区域以固定大小的字节单位进行划分,然后用单链表将所有空闲内存块连接起来。链表中所有节点大小相同,分配,释放都非常简单。

  LwIP 源文件中 memp.c 和 memp.h 就是动态内存池分配策略,为什么 LWIP 需要有POOL?因为协议栈里面有大量的协议首部,这些协议首部长度都是固定不变的,所以我们可以首先分配固定内存,给这些固定长度的协议首部,以后每次需要处理协议首部的时候,都直接使用这些已经分配的内存,不需要重新分配内存区域,这样子就达到一个地方分配,多个地方使用的方便与效率。

  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目录下, 它里面全是宏定义, LwIP 为什么要这样子写呢, 其实很简单,当然是为了方便,在不同的地方调用#include "lwip/priv/memp_std.h"就能产生不同的效果。

  该文件中的宏值定义全部依赖于宏 LWIP_MEMPOOL(name,num,size,desc),这样,只要外部提供的该宏值不同,则包含该文件的源文件在编译器的预处理后,就会产生不一样的结果。这样,就可以通过在不同的地方多次包含该文件,前面必定提供宏值 MEMPOOL以产生不同结果。可能有些人看得一脸懵逼,其实我一开始也是这样子,不得不说 LwIP源码的作者还是很厉害的。

  简单来说,就是在外边提供 LWIP_MEMPOOL 宏定义, 然后在包含 memp_std.h 文件,编译器就会帮我们处理。

 

posted @ 2024-10-30 09:32  孤情剑客  阅读(321)  评论(0)    收藏  举报