【STM32】TCP/IP通信协议进阶(LwIP内存管理详解)

五、LWIP内存管理

1.什么是内存管理?

(1)内存管理,是指软件运行时对计算机内存资源的分配的使用的技术,其主要目的是如何高效、快速的分配,并且在适当的时候释放和回收内存资源(就比如C语言当中的malloc 、free分配和释放)

内存分配:大数组,完成之后返回内存地址

内存释放:传入内存地址让算法进行释放

(2)LWIP内存管理策略

1.内存堆:提供合适大小的内存,剩余内存返回堆中

2.内存池:只能申请固定大小的内存,能有效防止内存碎片

3.C库: C运行时库自带的内存分配策略(不建议使用)

lwip内存池和内存堆本质上直接操作数组实现

(3)lwip内存堆和内存池的应用

  • 接收数据:MAC内核数组【内存堆和内存池可适用,正点原子阿波罗版本的使用的是内存池】

  • 发送数据:用户调用lwip的API接口【lwip一般选用内存堆申请内存】

  • 用户调用:可调用lwip的内存池和内存堆API接口申请内存

  • 接口控制块:netconn、socket、raw接口

  • 构建消息:API消息、数据包消息

  • 2.lwip内存堆简介

    lwip内存堆是一种可变长的分配策略,可以随意申请任意大小的内存,lwip内存堆采用的是First Fit(首次拟合)内存算法

    (1)First Fit算法

    从低地址空间往高地址空间查找,从中切割成合适的块,并把剩余的部分返回到动态内存堆中。

    优点:

  • 内存浪费小、比较简单、适合小内存管理

  • 确保高地址空间具有足够的内存

  • 要求分配最小值以及相邻空间块合并,有效防止内存碎片

  • 缺点:

  • 分配与释放频繁,会造成内存碎片

  • 分配和释放时,从低地址开始寻找,会导致效率慢

  • 3.lwip内存堆原理解析

    1

    通过开辟一个内存堆,使用模拟IC运行时库的内存分配策略实现【大数组】

    (1)管理内存块的结构体:

    如下源码所示:

    struct mem {
      mem_size_t next;      //指向下一个节点索引
      mem_size_t prev;      //指向上一个节点索引
      u8_t used;            //描述内存块是否可用    0:未使用   1:已使用
    };
    (2)最小内存分配
    #ifndef MIN_SIZE
    #define MIN_SIZE             12//最小分配内存
    #endif /* MIN_SIZE */

    为了防止内存碎片,lwip内核定义了最小分配大小MIN_SIZE。当用户申请的内存小于最小分配内存时,系统将分配MIN_SIZE大小的内存,

    (3)对齐定义
    //对齐操作
    #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)//内存堆对齐

    SIZEOF_STRUCT_MEM宏定义用于确保内存大小进行4字节对齐,这不仅可以大大提升CPU的访问速度还因为某些硬件平台只能从特定地址处获取特定类型的数据。

    (4)定义内存堆的空间
    #ifndef LWIP_RAM_HEAP_POINTER 
    ​
    /*定义堆内存空间*/
    ​
    LWIP_DECLARE_MEMORY_ALIGNED(ram_heap, MEM_SIZE_ALIGNED + (2U*SIZEOF_STRUCT_MEM)); 
    ​
    #define LWIP_RAM_HEAP_POINTER ram_heap 
    ​
    #endif

    无论是内存池还是内存堆都是对一个大数组进行操作。这种大数组被称为ram_heap数组,其大小常被定义为 MEM_SIZE_ALIGNED + (2U*SIZEOF_STR UCT_MEM)。这个能让用户申请内存时在这个大数组中分配相应大小的内存块,减少内存碎片。

    (5)操作内存堆变量
    /* 指向对齐后的内存堆的地址*/
    static u8_t *ram;
    /* 指向对齐后的内存堆的最后一个内存块 */
    static struct mem *ram_end; 
    /* 指向已被释放的索引号最小的内存块(内存堆最前面的已被释放的)*/
    static struct mem * LWIP_MEM_LFREE_VOLATILE lfree;
    ​

    lwIP 内核使用三个指针:ram 指针、ram_end 指针和 lfree 指针。

    ram 指针指向ram_heap数组对齐后的内存堆总空间首地址

    ram_end 指针指向ram_heap数组内存堆总空间尾地址(接近总空间的尾 地址)

    lfree 指针指向ram_heap数组最低内存地址的空闲内存块。

    在内存分配当中lwip根据lfree指针指向的空闲内存进行分配,从而快速找到可用的内存块,并有效分配内存给需要的任务。ram_end指针用于检测总内存堆空间中是否存在空闲的内存,以便进行进一步的内存分配。

    (6)内存初始化mem_init():

    下图是对堆空间进行初始化,初始化后的lfree指针指向第一个内存块,该内存块由控制块可用内存所组成,ram_end指针指向堆空间的尾部,用于判断堆空间是否存在可用内存,若lfree指针指向ram_end指针,则表示该堆空间没有可用内存进行分配

    控制块:标记内存是否可用

    可用内存:实际可分配的内存区域

    void mem_init(void)
    {
      struct mem *mem;
    ​
      LWIP_ASSERT("Sanity check alignment",
        (SIZEOF_STRUCT_MEM & (MEM_ALIGNMENT-1)) == 0);
    ​
      /* 对内存堆的地址(全局变量的名)进行对齐指向 ram_heap*/
      ram = (u8_t *)LWIP_MEM_ALIGN(LWIP_RAM_HEAP_POINTER);
      /* 建立第一个内存块,内存块由内存块头+空间组成。 */
      mem = (struct mem *)(void *)ram;//附加在每个内存块前面的结构体
      /* 下一个内存块不存在,因此指向内存堆的结束 */
      mem->next = MEM_SIZE_ALIGNED;
      /* 前一个内存块就是它自己,因为这是第一个内存块 */ 
      mem->prev = 0;
      /* 第一个内存块没有被使用 */ 
      mem->used = 0;
      /* 初始化堆的末端(指向 MEM_SIZE_ALIGNED 底部位置)*/
      ram_end = (struct mem *)(void *)&ram[MEM_SIZE_ALIGNED];
      /* 最后一个内存块被使用。因为其后面没有可用空间,必须标记为已被使用 */
      ram_end->used = 1;
      /* 下一个不存在,因此指向内存堆的结束 */
      ram_end->next = MEM_SIZE_ALIGNED;
      /* 前一个不存在,因此指向内存堆的结束 */
      ram_end->prev = MEM_SIZE_ALIGNED;
    ​
      /* 已释放的索引最小的内存块就是上面建立的第一个内存块。*/
      lfree = (struct mem *)(void *)ram;
    ​
      MEM_STATS_AVAIL(avail, MEM_SIZE_ALIGNED);
     /* 这里建立一个互斥信号量,主要是用来进行内存的申请、释放的保护 */
      if(sys_mutex_new(&mem_mutex) != ERR_OK) {
        LWIP_ASSERT("failed to create mem_mutex", 0);
      }
    }

    在内存分配过程中,lfree指针从低地址开始不短查找和划分内存,直到ram_end指针所指向的地址,这意味着lfree指针从堆空间的起始位置开始,逐个遍历内存块,直到找到可用的内存或到达堆空间的末尾才能结束分配。

    (7)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;
        }
    }

    4.LWIP内存池简介

    (1)什么是内存池

    lwip内存池是把连续的内存池分成多个大小相同的内存空间,并通过单链表的方式连接起来。当用户申请内存时,系统会从单链表的头 部取出一个内存块进行分配,释放内存时只需将内存块放回链表的头部。

    lwip内存池优点:

    分配速度快,防止内存碎片,回收便捷

    lwip内存池缺点:

    资源浪费,申请大型内存时,可能申请失败

    lwip内存池应用场景:

    在 lwIP 中,存在多种固定大小的数据结构,这些数据结构的特点是预先知道其大小并且在整个生命周期中保持不变。比如说,在建立 TCP 连接时,需要使用 TCP 控制块这种数据结构,其大小是固定的。为了满足这些数据结构的内存分配需求,lwIP 在内存初始化时创建了动态内存池 POOL预先分配一定数量的内存块。这种内存管理方式有助于提高内存分配的效率和性能。

    (2)实现LWIP内存池的文件

    对于内存堆中的动态内存池,运用了很多复杂的宏定义的运用。所以我们将重点探究这四个关键文件:memp.c、memp.h、memp_std.h 和 memp_prive.h。

    i.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;
    };

    memp结构体和memp_desc结构体的关系:

    由图可知:每一个 memp_desc 结构体都是用于管理同一类型的内存池。 这些内存池,也就是内存块,是通过链表的形式相互连接起来的。

    ii.memp_std.h 文件

    该文件是 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 */

    通过上面代码,可发现:不同类型的内存池是通过相应的宏定义声明启用的。LWIP_MEMPOOL 这个宏定义用于初始化各种类型的内存池。

    iii. memp.h 文件

    文件主要定义了 memp_t 枚举类型,该类型用于获取各类内存池的数量声明了宏定义和函数以提供外部文件使用,这里定义里前面所提到了LWIP_MEMPOOL 宏定义,

    typedef enum {
    ​
    /* ##为 C 语言的连接符,例如 MEMP_##A,A = NAME ,所以等于 MEMP_NAME */
    #define LWIP_MEMPOOL(name,num,size,desc) MEMP_##name,
    ​
    #include "lwip/priv/memp_std.h"
     MEMP_MAX
     
    } memp_t;
    ​
    #include "lwip/priv/memp_priv.h" /* 该文件需要使用上面的枚举 */
    #include "lwip/stats.h"

    根据 memp_std.h 文件中启用的内存池类型来计算 MEMP_MAX,即各类内存池的最大数量。计算方法如下:

    1,LWIP_MEMPOOL 宏定义指向 MEMP_##name(##为 C 语言中的连接符)

    2,通过#include "lwip/priv/memp_std.h"文件来启用所需的内存池类型

    iv.memp.c 文件

    在memp.h当中,我们有提到:LWIP_MEMPOOL 指向 LWIP_MEMPOOL_DECLARE 宏定义,而在memp.c 文件当中的const memp_pools[MEMP _MAX]数组则用于管理各类内存池的描述符

    #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 类型的内存池时,展开后的枚举类型如下:

    u8_t memp_memory_RAW_PCB_base[((((((num) * (MEMP_SIZE +
    (((size) + MEM_ALIGNMENT - 1U) & ~(MEM_ALIGNMENT-1U))))) +
    MEM_ALIGNMENT - 1U)))];
    ​
    static struct memp *memp_tab_RAW_PCB;
    const struct memp_desc memp_RAW_PCB= { \
     LWIP_MEM_ALIGN_SIZE(size), \
     (num), \
     memp_memory_TCPIP_MSG_API_base, \
     &memp_tab_TCPIP_MSG_API \
     };
     
    u8_t memp_memory_UDP_PCB_base[((((((num) * (MEMP_SIZE +
    (((size) + MEM_ALIGNMENT - 1U) & ~(MEM_ALIGNMENT-1U))))) +
    MEM_ALIGNMENT - 1U)))];
    ​
    static struct memp *memp_tab_UDP_PCB;
     
    const struct memp_desc memp_UDP_PCB= { \
     LWIP_MEM_ALIGN_SIZE(size), \
     (num), \
     memp_memory_UDP_PCB_base, \
     &memp_tab_UDP_PCB \
     };

    这段代码通过使用 LWIP_MEMPOOL_DECLARE 宏定义,声明了各类内存池的描述和管理信息。具体来说,它定义了memp_desc 结构体,比如 :

    memp_RAW_PCB:用于描述该类型的内存池的数量、大小、分配内存地址以及指向空闲内存池的指针。

    const struct memp_desc* const memp_pools[MEMP_MAX] = {
     &memp_memp_RAW_PCB,
     &memp_memp_UDP_PCB,
    };

    memp_pools[MEMP_RAW_PCB],它取自 memp_RAW_PCB 变 量的地址。这是我们在前面展开LWIP_MEMPOOL_DECLARE 宏定义时定义的变量。

    v.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_pools[i]);
     }
    }
    ​
    void memp_init_pool(const struct memp_desc *desc)
    {
     int i;
     struct memp *memp;
     *desc->tab = NULL;
     /* 内存对齐 */
     memp = (struct memp*)LWIP_MEM_ALIGN(desc->base);
     /* 将内存块链接成链表形式 */
     for (i = 0; i < desc->num; ++i) {
     memp->next = *desc->tab;
     *desc->tab = memp;
     /* 地址偏移*/ 
     memp = (struct memp *)(void *)((u8_t *)memp +
    MEMP_SIZE + desc->size);
     }
    }
    ​

    每个类型的描述符都是用于管理和描述该类型的内存池。这些同一类型的内存池内部通过指向下一个节点的指针链接起来,形成一个链表。通过第二个 for 循 环语句,我们可以遍历这些同一类型的内存池,并将其以链表的形式进行链接。

    memp_pool 数组包含了不同类型的内存池描述符,每个描述符负责管理同类型的内存池。这些内存池通过指针链接成一个单向链表,方便管理和访问。同一类型的内存池都 在同一个数组中分配,通过 base 指针可以找到该数组的首地址。tab 指针指向第一个空闲的内 存池,当用户申请内存池时,将从 tab 指针指向的内存池进行分配。分配完成后,tab 指针将偏移到下一个空闲内存池的地址,以便下次分配。

    vi.memp_malloc 函数和 memp_malloc_pool 函数

    内存池有多种类型,因此用户在申请内存池时需要**明确申请的类型lwIP 内存池的申请函数是 memp_malloc**

    void *
    memp_malloc(memp_t type)
    {
     void *memp;
     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
        {
        }
        return NULL;
    }
    ​

    memp_malloc 函数根据用户传入的内存池类型,如 UDP_PCB 等,在 memp_pool 数组中查找对应的内存池描述符。一旦找到对应的描述符,该函数会根据描述符中的 tab 指针来分配内存给用户,并将 tab 指针偏移至下一个空闲内存池。

    vii.memp_free 函数与 memp_free_pool 函数

    内存池的释放函数相对简单,它需要传入两个参数:内存池的类型和要释放的内存池的地址。通过这两个参数,lwIP 内核可以确定该类型内存池描述符的位置,以及需要释放的内存池的具体位置。

    void memp_free(memp_t type, void *mem)
    {
        if (mem == NULL) /* 判断内存块的起始地址释放为空 */
        {
            return;
        }
        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;
    }

    释放函数很简单,只需要将内存池描述符的 tab 指针偏移至要释放的内存池。这样,释放的内存块就会被返回到相应的内存池中,以供后续使用。

    作者:菲子叭叭

    物联沃分享整理
    物联沃-IOTWORD物联网 » 【STM32】TCP/IP通信协议进阶(LwIP内存管理详解)

    发表回复