lwIP TCP/IP 协议栈笔记之六: 内存管理

目录

1. 几种内存分配策略

1.1 固定大小的内存块

1.2 可变长度分配

2. 动态内存池(POOL)

2.1 内存池的预处理

2.2 内存池初始化

2.2 内存池分配

2.3 内存释放

3. 动态内存堆

3.1 内存堆的组织结构

3.2 内存堆的初始化

3.3 内存分配

3.4 内存释放

4. 使用C 库的malloc 和free 来管理内存

5. LwIP 中的配置


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 协议栈初始化的时候, memp_init()会对内存池进行初始化,真正的内存初始化函数是memp_init_pool()函数,该函数源码具体如下:

/* 代码有删减 */
void
memp_init(void)
{
  u16_t i;

  /* for every pool: */
  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);
  /* force memset on pool memory */
  memset(memp, 0, (size_t)desc->num * (MEMP_SIZE + desc->size));

  /* create a linked list of memp elements */
  /* 将内存块链接成链表形式 */
  for (i = 0; i < desc->num; ++i) {
    memp->next = *desc->tab;
    *desc->tab = memp;

    /* cast through void* to get rid of alignment warnings */
    /* 地址偏移 */
    memp = (struct memp *)(void *)((u8_t *)memp + MEMP_SIZE + desc->size);
  }
}
  • 1. 每种POOL 的memp_desc 描述进行初始化,
  • 2. memset()函数将其内容清零
  • 2. 每种类型的POOL 中将空闲内存块连接成单链表

2.2 内存池分配

内存池的初始化之后,这些内存池中的内存块就可以使用了,这就需要用户通过memp_malloc 函数进行申请内存块,而内存块的大小就是指定的大小,根据内存池的类型去选择从哪个内存池进行分配。

系统中所有的内存池类型都会被记录在memp_pools 数组中,我们可以将该数组称之为内存池描述表,它负责将系统所有的内存池信息描述并且记录下来。

/* 代码有删减  */
void *
memp_malloc(memp_t type)
{
  void *memp;
  LWIP_ERROR("memp_malloc: type < MEMP_MAX", (type < MEMP_MAX), return NULL;);

  memp = do_memp_malloc_pool(memp_pools[type]);

  return memp;
}

/* 代码有删减 */
static void *
do_memp_malloc_pool(const struct memp_desc *desc)

{
  struct memp *memp;
  SYS_ARCH_DECL_PROTECT(old_level);

  SYS_ARCH_PROTECT(old_level);

  memp = *desc->tab;


  if (memp != NULL) {

    *desc->tab = memp->next;

    LWIP_ASSERT("memp_malloc: memp properly aligned",
                ((mem_ptr_t)memp % MEM_ALIGNMENT) == 0);

    SYS_ARCH_UNPROTECT(old_level);
    /* cast through u8_t* to get rid of alignment warnings */
    return ((u8_t *)memp + MEMP_SIZE);
  } else {

    SYS_ARCH_UNPROTECT(old_level);
  }

  return NULL;
}

内存池申请函数的核心代码就一句,那就是memp = *desc->tab;,通过这句代码,能直接得到对应内存块中的第一个空闲内存块,并将其取出,并且移动*desc->tab 指针,指向下一个空闲内存块,然后将((u8_t *)memp + MEMP_SIZE)返回,MEMP_SIZE 偏移的空间大小,因为内存块需要一些空间存储内存块相关的信息。(使用的内存从链表删除)

2.3 内存释放

内存释放函数也非常简单的,只需要把使用完毕的内存添加到对应内存池中的空闲内存块链表即可,只不过释放内存有两个参数,一个是POOL 的类型,还有就是内存块的起始地址。(链表增加节点)

/* 代码有删减 */
void
memp_free(memp_t type, void *mem)
{

  LWIP_ERROR("memp_free: type < MEMP_MAX", (type < MEMP_MAX), return;);

  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;
  SYS_ARCH_DECL_PROTECT(old_level);

  LWIP_ASSERT("memp_free: mem properly aligned",
              ((mem_ptr_t)mem % MEM_ALIGNMENT) == 0);

  /* cast through void* to get rid of alignment warnings */
  memp = (struct memp *)(void *)((u8_t *)mem - MEMP_SIZE);

  SYS_ARCH_PROTECT(old_level);

  memp->next = *desc->tab;
  *desc->tab = memp;

  SYS_ARCH_UNPROTECT(old_level);
}

1. 根据内存块的地址偏移得到内存块的起始地址

2. 内存块的下一个就是链表中的第一个空闲内存块

3. 将内存块插入到对应内存池的*desc->tab 中

LwIP 对内存池的设计,采用了很多巧妙的地方,特别是对编译全局变量的过程,根据用户配置的宏定义决定是否编译进去。而且,内存池还采用内存池描述表进行管理系统中所有的内存池,在用户需要某种类型的内存块时候,就直接将其需要的类型传递进去就能得到对应大小的内存块,分配的方式是非常方便并且高效的。

3. 动态内存堆

在嵌入式开发中,内存管理以及使用是至关重要的,内存使用的多少、内存泄漏等时刻需要注意。合理的内存管理策略将从根本上决定内存分配和回收效率,最终决定系统的整体性能。LwIP 为了能够灵活的使用内存,为使用者提供两种简单却又高效的动态内存管理策略:动态内存堆管理(heap)、动态内存池管理(pool),

动态内存堆管理(heap)又可以分为两种:一种是 C 标准库自带的内存管理策略,另一种是LwIP 自身实现的内存堆管理策略。这两者的选择需要通过宏值MEM_LIBC_MALLOC 来选择,且二者只能选择其一。一般不使用C库的。

其次,LwIP 在自身内存堆和内存池的实现上设计得非常灵活。内存池可由内存堆实现,反之,内存堆也可以由内存池实现。通过MEM_USE_POOLS MEMP_MEM_MALLOC这两个宏定义来选择,且二者只能选择其一。

3.1 内存堆的组织结构

内存堆的的组织结构,它包括了内存数据结构与某些重要的全局变量。

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;                          // 标记内存是否被使用
#if MEM_OVERFLOW_CHECK
  /** this keeps track of the user allocation size for guard checks */
  mem_size_t user_size;
#endif
};
#ifndef MIN_SIZE
#define MIN_SIZE             12                                    
#endif /* MIN_SIZE */
/* some alignment macros: we define them here for better source code layout */
#define MIN_SIZE_ALIGNED     LWIP_MEM_ALIGN_SIZE(MIN_SIZE)
#define SIZEOF_STRUCT_MEM    LWIP_MEM_ALIGN_SIZE(sizeof(struct mem))
#define MEM_SIZE_ALIGNED     LWIP_MEM_ALIGN_SIZE(MEM_SIZE)

/** If you want to relocate the heap to external memory, simply define
 * LWIP_RAM_HEAP_POINTER as a void-pointer to that location.
 * If so, make sure the memory at that location is big enough (see below on
 * how that space is calculated). */
#ifndef LWIP_RAM_HEAP_POINTER
/** the heap. we need one struct mem at the end and some room for alignment */
LWIP_DECLARE_MEMORY_ALIGNED(ram_heap, MEM_SIZE_ALIGNED + (2U * SIZEOF_STRUCT_MEM)); 
#define LWIP_RAM_HEAP_POINTER ram_heap
#endif /* LWIP_RAM_HEAP_POINTER */

/** 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 * LWIP_MEM_LFREE_VOLATILE lfree;

MIN_SIZE:

申请的内存最小为12 字节,因为一个内存块最起码需要保持mem结构体的信息,以便于对内存块进行操作,而该结构体在对齐后的内存大小就是12 字节.

LWIP_DECLARE_MEMORY_ALIGNED(ram_heap, MEM_SIZE_ALIGNED + (2U * SIZEOF_STRUCT_MEM));

内存堆的大小是由这个宏定义的,该语句在编译器处理之后就是u8_t ram_heap[(((MEM_SIZE_ALIGNED + (2U * SIZEOF_STRUCT_MEM)) +MEM_ALIGNMENT - 1U))];,其中MEM_SIZE_ALIGNED 宏是内存堆大小MEM_SIZE 经过内存对齐后的大小;而SIZEOF_STRUCT_MEM则是结构体mem 经过内存对其后的大小,MEM_ALIGNMENT 则是CPU 按多少字节对其的宏定义,一般为4。MEM_SIZE 需要在lwipopts.h中定义,图片例中为16000。

ram_heap:

ram_heap[]就是内核的内存堆空间,LWIP_RAM_HEAP_POINTER这个宏定义相对于重新命名ram_heap。

ram:

全局指针变量,指向内存堆对齐后的起始地址,因为真正的内存堆起始地址不一定是按照CPU 的对齐方式对齐的,而此处就要确保内存堆的起始地址是对齐的。

ram_end:

mem 类型指针,指向内存堆中最后一个内存块

lfree:

mem 类型指针,指向内存堆中低地址的空闲内存块,简单来说就是空闲内存块链表指针。用于更快的搜索。

3.2 内存堆的初始化

在内核初始化的时候,会调用mem_init()函数进行内存堆的初始化,内存堆初始化主要的过程就是对上述所属的内存堆组织结构进行初始化,主要设置内存堆的起始地址,以及初始化空闲列表。根据用户配置的宏定义进行相关初始化,配置不同其实现也不同。

void
mem_init(void)
{
  struct mem *mem;

  LWIP_ASSERT("Sanity check alignment",
              (SIZEOF_STRUCT_MEM & (MEM_ALIGNMENT - 1)) == 0);

  /* align the heap */
  /* 内存堆空间对齐,关联到ram */
  ram = (u8_t *)LWIP_MEM_ALIGN(LWIP_RAM_HEAP_POINTER);
  /* initialize the start of the heap */
  /* 在内存堆起始位置放置一个mem 类型的结构体,因为初始化后的
     内存堆就是一个大的空闲内存块,每个空闲内存块的前面都需要放置一个mem 结构体 */
  mem = (struct mem *)(void *)ram;

  /* 下一个内存块的偏移量为MEM_SIZE_ALIGNED */
  mem->next = MEM_SIZE_ALIGNED;
  mem->prev = 0;                            // 上一个内存块为空
  mem->used = 0;                            // 未使用
  /* initialize the end of the heap */
  /* 指针移动到内存堆末尾的位置,并且在那里放置一个mem 类型的
     结构体,并初始化表示内存堆结束的内存块 */
  ram_end = ptr_to_mem(MEM_SIZE_ALIGNED);
  ram_end->used = 1;                        // 标记已经使用了该内存块

  /* 同时mem 结构体的next 与prev 字段都指向自身,此
     处仅表示已经到了内存堆的结束的地方,并无内存可以分配 */
  ram_end->next = MEM_SIZE_ALIGNED;         
  ram_end->prev = MEM_SIZE_ALIGNED;
  MEM_SANITY();

  /* initialize the lowest-free pointer to the start of the heap */
  /* 空闲内存块链表指针指向内存堆的起始地址,因为当前只有一个内存块。 */
  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);
  }
}

经过mem_init()函数后,内存堆会被初始化为两个内存块,第一个内存块的大小就是整个内存堆的大小,而第二个内存块就是介绍内存块,其大小为0,并且被标记为已使用状态,无法进行分配。值得注意的是,系统在运行的时候,随着内存的分配与释放,lfree指针的指向地址不断改变,都指向内存堆中低地址空闲内存块,而ram_end 则不会改变,它指向系统中最后一个内存块,也就是内存堆的结束地址。初始化完成的示意图

3.3 内存分配

内存分配函数根据用户指定申请大小的内存空间进行分配内存,其大小要大于MIN_SIZE。

LwIP 中使用内存分配算法是首次拟合方法,其分配原理就是在空闲内存块链表中遍历寻找,直到找到第一个合适用户需求大小的内存块进行分配,如果该内存块能进行分割,则将用户需要大小的内存块分割出来,剩下的空闲内存块则重新插入空闲内存块链表中。经过多次的分配与释放,很可能会出现内存碎片,当然,LwIP 也有解决的方法

mem_malloc()函数是LwIP 中内存分配函数,其参数是用户指定大小的内存字节数,如果申请成功则返回内存块的地址,如果内存没有分配成功,则返回NULL,分配的内存空间会受到内存对其的影响,可能会比申请的内存略大,比如用户需要申请22 个字节的内存,而CPU 是按照4 字节内存对齐的,那么分配的时候就会申请24 个字节的内存块。

内存块在申请成功后返回的是内存块的起始地址,但是该内存并未进行初始化,可能包含任意的随机数据,用户可以立即对其进行初始化或者写入有效数据以防止数据错误。此外内存堆是一个全局变量,在操作系统的环境中进行申请内存块是不安全的,所以LwIP使用互斥量实现了对临界资源的保护,在多个线程同时申请或者释放的时候,会因为互斥量的保护而产生延迟。

void *
mem_malloc(mem_size_t size_in)
{
  mem_size_t ptr, ptr2, size;
  struct mem *mem, *mem2;
#if LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT
  u8_t local_mem_free_count = 0;
#endif /* LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT */
  LWIP_MEM_ALLOC_DECL_PROTECT();

  if (size_in == 0) {
    return NULL;
  }

  /* Expand the size of the allocated memory region so that we can
     adjust for alignment. */
  /* 将用户申请的内存大小进行对齐操作 */
  size = (mem_size_t)LWIP_MEM_ALIGN_SIZE(size_in);
  if (size < MIN_SIZE_ALIGNED) {
    /* every data block must be at least MIN_SIZE_ALIGNED long */
    /* 申请的内存大小小于最小的内存对齐大小 */
    size = MIN_SIZE_ALIGNED;
  }

#if MEM_OVERFLOW_CHECK
  size += MEM_SANITY_REGION_BEFORE_ALIGNED + MEM_SANITY_REGION_AFTER_ALIGNED;
#endif
  /* 申请的内存大小大于整个内存堆对齐后的大小,则返回NULL */
  if ((size > MEM_SIZE_ALIGNED) || (size < size_in)) {
    return NULL;
  }

  /* protect the heap from concurrent access */
  /* 获得互斥量,这一句代码在操作系统环境才起作用 */
  sys_mutex_lock(&mem_mutex);
  LWIP_MEM_ALLOC_PROTECT();
#if LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT
  /* run as long as a mem_free disturbed mem_malloc or mem_trim */
  do {
    local_mem_free_count = 0;
#endif /* LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT */

    /* Scan through the heap searching for a free block that is big enough,
     * beginning with the lowest free block.
     * 遍历空闲内存块链表,直到找到第一个适合用户需求的内存块大小
     */
    for (ptr = mem_to_ptr(lfree); ptr < MEM_SIZE_ALIGNED - size;
         ptr = ptr_to_mem(ptr)->next) {
      /* 得到这个内存块起始地址 */
      mem = ptr_to_mem(ptr);
#if LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT
      mem_free_count = 0;
      LWIP_MEM_ALLOC_UNPROTECT();
      /* allow mem_free or mem_trim to run */
      LWIP_MEM_ALLOC_PROTECT();
      if (mem_free_count != 0) {
        /* If mem_free or mem_trim have run, we have to restart since they
           could have altered our current struct mem. */
        local_mem_free_count = 1;
        break;
      }
#endif /* LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT */

      /* 如果该内存块是未使用的,并且它的大小不小于用户需要的大小加上mem 结构体的大小,
         那么就满足用户的需求 */
      if ((!mem->used) &&
          (mem->next - (ptr + SIZEOF_STRUCT_MEM)) >= size) {
        /* mem is not used and at least perfect fit is possible:
         * mem->next - (ptr + SIZEOF_STRUCT_MEM) gives us the 'user data size' of mem */

        if (mem->next - (ptr + SIZEOF_STRUCT_MEM) >= (size + SIZEOF_STRUCT_MEM + MIN_SIZE_ALIGNED)) {
          /* (in addition to the above, we test if another struct mem (SIZEOF_STRUCT_MEM) containing
           * at least MIN_SIZE_ALIGNED of data also fits in the 'user data space' of 'mem')
           * -> split large block, create empty remainder,
           * remainder must be large enough to contain MIN_SIZE_ALIGNED data: if
           * mem->next - (ptr + (2*SIZEOF_STRUCT_MEM)) == size,
           * struct mem would fit in but no data between mem2 and mem2->next
           * @todo we could leave out MIN_SIZE_ALIGNED. We would create an empty
           *       region that couldn't hold data, but when mem->next gets freed,
           *       the 2 regions would be combined, resulting in more free memory
           */

          /* 既然满足用户需求,那么内存块可能很大,不直接分配给用户,否则太浪费了,
             那就看看这个内存块能不能切开,如果能就将一部分分配给用户即可,
             程序能执行到这里,说明内存块能进行分割,通过内存块的起始地址与需求大小进行偏移,
             得到剩下的的内存起始块地址ptr2。*/
          ptr2 = (mem_size_t)(ptr + SIZEOF_STRUCT_MEM + size);
          LWIP_ASSERT("invalid next ptr",ptr2 != MEM_SIZE_ALIGNED);
          /* create mem2 struct */
          /* 将该地址后的内存空间作为分割之后新内存块mem2,
            将起始地址转换为mem 结构体用于记录内存块的信息 
            注意:实际链表插入
          */
          mem2 = ptr_to_mem(ptr2);

          /* 标记为未使用的内存块,并且将其插入空闲内存块链表中 */
          mem2->used = 0;
          mem2->next = mem->next;
          mem2->prev = ptr;
          /* and insert it between mem and mem->next */
          mem->next = ptr2;
          mem->used = 1;    // 被分配出去的内存块mem 标记为已使用状态

          /* 如果mem2 内存块的下一个内存块不是链表中最后一个内存块(结束地址),
            那就将它下一个的内存块的prve 指向mem2 
            简而言之,未到最后一个块,则下一个块的prve重新关联到新的节点*/
          if (mem2->next != MEM_SIZE_ALIGNED) {
            ptr_to_mem(mem2->next)->prev = ptr2;
          }
          MEM_STATS_INC_USED(used, (size + SIZEOF_STRUCT_MEM));
        } else {
          /* (a mem2 struct does no fit into the user data space of mem and mem->next will always
           * be used at this point: if not we have 2 unused structs in a row, plug_holes should have
           * take care of this).
           * -> near fit or exact fit: do not split, no mem2 creation
           * also can't move mem->next directly behind mem, since mem->next
           * will always be used at this point!
           */
          /* 如果不能分割,直接将分配的内存块标记为已使用即可 */
          mem->used = 1;
          MEM_STATS_INC_USED(used, mem->next - mem_to_ptr(mem));
        }
#if LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT
mem_malloc_adjust_lfree:
#endif /* LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT */
        /* 如果被分配出去的内存块是lfree 指向的内存块,那么就需要重新给lfree 赋值 */
        if (mem == lfree) {
          struct mem *cur = lfree;
          /* Find next free block after mem and update lowest free pointer */
          while (cur->used && cur != ram_end) {
#if LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT
            mem_free_count = 0;
            LWIP_MEM_ALLOC_UNPROTECT();
            /* prevent high interrupt latency... */
            LWIP_MEM_ALLOC_PROTECT();
            if (mem_free_count != 0) {
              /* If mem_free or mem_trim have run, we have to restart since they
                 could have altered our current struct mem or lfree. */
              goto mem_malloc_adjust_lfree;
            }
#endif /* LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT */

            /* 找到第一个低地址的空闲内存块 */
            cur = ptr_to_mem(cur->next);
          }
          lfree = cur;                    // 将lfree 指向该内存块
          LWIP_ASSERT("mem_malloc: !lfree->used", ((lfree == ram_end) || (!lfree->used)));
        }
        LWIP_MEM_ALLOC_UNPROTECT();
        sys_mutex_unlock(&mem_mutex);     // 释放互斥量
        LWIP_ASSERT("mem_malloc: allocated memory not above ram_end.",
                    (mem_ptr_t)mem + SIZEOF_STRUCT_MEM + size <= (mem_ptr_t)ram_end);
        LWIP_ASSERT("mem_malloc: allocated memory properly aligned.",
                    ((mem_ptr_t)mem + SIZEOF_STRUCT_MEM) % MEM_ALIGNMENT == 0);
        LWIP_ASSERT("mem_malloc: sanity check alignment",
                    (((mem_ptr_t)mem) & (MEM_ALIGNMENT - 1)) == 0);

#if MEM_OVERFLOW_CHECK
        mem_overflow_init_element(mem, size_in);
#endif
        MEM_SANITY();
        /* 返回内存块可用的起始地址,因为内存块的块头需要使用mem结构体保存内存块的基本信息。 */
        return (u8_t *)mem + SIZEOF_STRUCT_MEM + MEM_SANITY_OFFSET;
      }
    }
#if LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT
    /* if we got interrupted by a mem_free, try again */
  } while (local_mem_free_count != 0);
#endif /* LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT */
  MEM_STATS_INC(err);
  LWIP_MEM_ALLOC_UNPROTECT();
  sys_mutex_unlock(&mem_mutex);    // :如果没法分配成功,就释放互斥量并且退出
  LWIP_DEBUGF(MEM_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("mem_malloc: could not allocate %"S16_F" bytes\n", (s16_t)size));
  return NULL;
}

 如果在初始化后的内存堆中分配了一个大小为24 字节的内存块出去,则分配完成的示意图

经过上面的分析,可知,申请内存的过程,其实本质就是标识使用的内存块链表,新建空闲内存块链表,并插入

3.4 内存释放

内存释放的操作也是比较简单的,LwIP 做法:它根据用户释放的内存块地址,通过偏移mem 结构体大小得到正确的内存块起始地址,并且根据mem 中保存的内存块信息进行释放、合并等操作,并将used 字段清零,表示该内存块未被使用。

LwIP 为了防止内存碎片的出现,通过算法将内存相邻的两个空闲内存块进行合并,在释放内存块的时候,如果内存块与上一个或者下一个空闲内存块在地址上是连续的,那么就将这两个内存块进行合并。

void
mem_free(void *rmem)
{
  struct mem *mem;
  LWIP_MEM_FREE_DECL_PROTECT();

    // 如果释放的地址为空,则直接返回
  if (rmem == NULL) {
    LWIP_DEBUGF(MEM_DEBUG | LWIP_DBG_TRACE | LWIP_DBG_LEVEL_SERIOUS, ("mem_free(p == NULL) was called.\n"));
    return;
  }
  if ((((mem_ptr_t)rmem) & (MEM_ALIGNMENT - 1)) != 0) {
    LWIP_MEM_ILLEGAL_FREE("mem_free: sanity check alignment");
    LWIP_DEBUGF(MEM_DEBUG | LWIP_DBG_LEVEL_SEVERE, ("mem_free: sanity check alignment\n"));
    /* protect mem stats from concurrent access */
    MEM_STATS_INC_LOCKED(illegal);
    return;
  }

  /* Get the corresponding struct mem: */
  /* cast through void* to get rid of alignment warnings */
  // 对释放的地址进行偏移,得到真正内存块的起始地址
  mem = (struct mem *)(void *)((u8_t *)rmem - (SIZEOF_STRUCT_MEM + MEM_SANITY_OFFSET));

  // 内存块的起始地址是否合法检查
  if ((u8_t *)mem < ram || (u8_t *)rmem + MIN_SIZE_ALIGNED > (u8_t *)ram_end) {
    LWIP_MEM_ILLEGAL_FREE("mem_free: illegal memory");
    LWIP_DEBUGF(MEM_DEBUG | LWIP_DBG_LEVEL_SEVERE, ("mem_free: illegal memory\n"));
    /* protect mem stats from concurrent access */
    MEM_STATS_INC_LOCKED(illegal);
    return;
  }
#if MEM_OVERFLOW_CHECK
  mem_overflow_check_element(mem);
#endif
  /* protect the heap from concurrent access */
  LWIP_MEM_FREE_PROTECT();
  /* mem has to be in a used state */
  // 判断一下内存块是否被使用,如果是未使用的也直接返回
  if (!mem->used) {
    LWIP_MEM_ILLEGAL_FREE("mem_free: illegal memory: double free");
    LWIP_MEM_FREE_UNPROTECT();
    LWIP_DEBUGF(MEM_DEBUG | LWIP_DBG_LEVEL_SEVERE, ("mem_free: illegal memory: double free?\n"));
    /* protect mem stats from concurrent access */
    MEM_STATS_INC_LOCKED(illegal);
    return;
  }

  // 判断一下内存块在链表中的连接是否正常
  if (!mem_link_valid(mem)) {
    LWIP_MEM_ILLEGAL_FREE("mem_free: illegal memory: non-linked: double free");
    LWIP_MEM_FREE_UNPROTECT();
    LWIP_DEBUGF(MEM_DEBUG | LWIP_DBG_LEVEL_SEVERE, ("mem_free: illegal memory: non-linked: double free?\n"));
    /* protect mem stats from concurrent access */
    MEM_STATS_INC_LOCKED(illegal);
    return;
  }

  /* mem is now unused. */
  mem->used = 0;    //内存块能正常释放,就将used 置0 表示已经释放了内存块。

  if (mem < lfree) {
    /* the newly freed struct is now the lowest */
    // 释放的内存块地址比lfree 指向的内存块地址低,则更新lfree 指针。
    lfree = mem;
  }

  MEM_STATS_DEC_USED(used, mem->next - (mem_size_t)(((u8_t *)mem - ram)));

  /* finally, see if prev or next are free also */
  // 调用plug_holes()函数尝试进行内存块合并
  plug_holes(mem);
  MEM_SANITY();
#if LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT
  mem_free_count = 1;
#endif /* LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT */
  LWIP_MEM_FREE_UNPROTECT();
}
static void
plug_holes(struct mem *mem);

plug_holes()函数尝试进行内存块合并,如果能合并则合并,该函数就是我们说的内存块合并算法,只要新释放的内存块与上一个或者下一个空闲内存块在地址上是连续的,则进行合并。

对内存释放函数的操作要非常小心,用户在申请内存的时候要注意及时释放内存块,否则就会造成内存泄漏!

4. 使用C 库的malloc 和free 来管理内存

LwIP 支持使用C 标准库的malloc 与free 进行内存的管理,当宏定义MEM_LIBC_MALLOC 被定义的时候,编译器就会把以下代码编译进去,就会采用C 标准库的malloc 与free 函数。

5. LwIP 中的配置

LwIP 中,内存的选择是通过以下这几个宏值来决定的,根据用户对宏值的定义值来判断使用那种内存管理策略,具体如下:

  • MEM_LIBC_MALLOC:

该宏定义是否使用C 标准库自带的内存分配策略。

该值默认情况下为0,表示不使用C 标准库自带的内存分配策略。即默认使用LwIP提供的内存堆分配策略。

该值定义为 1,使用C 标准库自带的分配策略。

  • MEMP_MEM_MALLOC:

该宏定义表示是否使用LwIP 内存堆分配策略实现内存池分配(即:要从内存池中获取内存时,实际是从内存堆中分配)。

默认情况下为 0,表示不从内存堆中分配,内存池为独立一块内存实现。与MEM_USE_POOLS 只能选择其一

MEM_USE_POOLS:该宏定义表示是否使用LwIP 内存池分配策略实现内存堆的分配(即:要从内存堆中获取内存时,实际是从内存池中分配)。

默认情况下为0,表示不使用从内存池中分配,内存堆为独立一块内存实现。与MEMP_MEM_MALLOC 只能选择其一

要使用内存池的方式实现内存堆分配,则需要将MEM_USE_POOLS 与MEMP_USE_CUSTOM_POOLS 定义为 1,并且宏定义MEMP_MEM_MALLOC 必须为 0,除此之外还需要做一下处理:

创建一个lwippools.h 文件,在该文件中添加类似代码 初始化内存池相关的代码,内存池的大小及数量是由用户自己决定的 。

LWIP_MALLOC_MEMPOOL_START

LWIP_MALLOC_MEMPOOL(20, 256)

LWIP_MALLOC_MEMPOOL(10, 512)

LWIP_MALLOC_MEMPOOL(5, 1512)

LWIP_MALLOC_MEMPOOL_END

注意一点的是,内存池的大小要依次增大,在编译阶段,编译器就会将这些内存个数及大小添加到系统的内存池之中,用户在申请内存的时候,根据其需要的大小在这些内存池中选择最合适的大小的内存块进行分配,如果具有最匹配的内存池中的内存块已经用完,则选择更大的内存池进行分配,只不过这样子会浪费更多的内存,当然,内存池的分配效率也是最高的,也相对于是我们常说的以空间换时间。

总结:通过以上分析,可知,lwIP中,无论是动态内存池还是动态内存堆,都是单向链表的相关操作。