LwIP 之五 详解动态内存管理 内存堆(mem.c/h)

写在前面

  目前网上有很多介绍LwIP内存的文章,但是绝大多数都不够详细,甚至很多介绍都是错误的!无论是代码的说明还是给出的图例,都欠佳!下面就从源代码,到图例详细进行说明。
  目前,网络上多数文章所使用的LwIP版本为1.4.1。最新版本为2.0.3。从1.4.1到2.0.3(貌似从2.0.0开始),LwIP的源码有了一定的变化,甚至于源码的文件结构也不一样,内部的一些实现源文件也被更新和替换了。

简介

  对于嵌入式开发来说,内存管理及使用是至关重要的,内存的使用多少、内存泄漏等时刻需要注意!合理的内存管理策略将从根本上决定内存分配和回收效率,最终决定系统的整体性能。LwIP为了能够灵活的使用内存,为使用者提供两种简单却又高效的动态内存管理机制:***动态内存堆管理(heap)、动态内存池管理(pool)***。这两中内存管理策略的实现分别对应着源码文件mem.c/hmemp.c/h
  其中,***动态内存池管理(heap)***又可以分为两种: C 运行时库自带的内存分配策略、LwIP自己实现的内存堆分配策略。这两者的选择需要通过宏值MEM_LIBC_MALLOC来选择,且二者只能选择其一。
  其次,LwIP在自己的内存堆和内存池具体实现上也比较灵活。内存池可有由内存堆实现,反之,内存堆也可以有内存池实现。通过宏值MEM_USE_POOLS和MEMP_MEM_MALLOC来选择,且二者只能选择其一。

内存堆和内存池

  动态内存堆分配策略原理就是在一个事先定义好大小的内存块中进行管理,其内存分配的策略是采用最快合适( First Fit)方式,只要找到一个比所请求的内存大的空闲块,就从中切割出合适的块,并把剩余的部分返回到动态内存堆中。内存的释放时,重新将申请到的内存返回堆中。
  其优点就是内存浪费小,比较简单,适合用于小内存的管理,其缺点就是如果频繁的动态分配和释放,可能会造成严重的内存碎片,如果在碎片情况严重的话,可能会导致内存分配不成功。

这其中也有个问题,就是内存合并问题。因为内存堆的管理通常为链表的形式进行管理。可选择将小的链表节点(较小的内存)进行合并。

  内存池的特点是预先开辟多组固定大小的内存块组织成链表,实现简单,分配和回收速度快,不会产生内存碎片,但是大小固定,并且需要预估算准确。

内存对齐

  一般来说,每一种处理器都会有自己的内存对齐要求,这样做的目的很大程度上是为了处理器读取内存数据的效率,且与对应硬件上的设计也有很大的关系。LwIP中,对于内存的操作函数都用到了内存对齐。

  在LwIP中,用户需要处理一个重要的部分sys_arch。具体可以参见另一片博文。整个arch框架中,就包含了内存对其这一块的配置:

/** Allocates a memory buffer of specified size that is of sufficient size to align
 * its start address using LWIP_MEM_ALIGN.
 * You can declare your own version here e.g. to enforce alignment without adding
 * trailing padding bytes (see LWIP_MEM_ALIGN_BUFFER) or your own section placement
 * requirements.\n
 * e.g. if you use gcc and need 32 bit alignment:\n
 * \#define LWIP_DECLARE_MEMORY_ALIGNED(variable_name, size) u8_t variable_name[size] \_\_attribute\_\_((aligned(4)))\n
 * or more portable:\n
 * \#define LWIP_DECLARE_MEMORY_ALIGNED(variable_name, size) u32_t variable_name[(size + sizeof(u32_t) - 1) / sizeof(u32_t)]
 */
 /* 定义全局数组作为内存堆的内存,LwIP就是实现的如何管理这块内存的。这块内存时经过对其操作的 */
#ifndef LWIP_DECLARE_MEMORY_ALIGNED
#define LWIP_DECLARE_MEMORY_ALIGNED(variable_name, size) u8_t variable_name[LWIP_MEM_ALIGN_BUFFER(size)]
#endif

/** Calculate memory size for an aligned buffer - returns the next highest
 * multiple of MEM_ALIGNMENT (e.g. LWIP_MEM_ALIGN_SIZE(3) and
 * LWIP_MEM_ALIGN_SIZE(4) will both yield 4 for MEM_ALIGNMENT == 4).
 */
 /* 数据占用空间大小对齐计算 */
#ifndef LWIP_MEM_ALIGN_SIZE
#define LWIP_MEM_ALIGN_SIZE(size) (((size) + MEM_ALIGNMENT - 1U) & ~(MEM_ALIGNMENT-1U))
#endif

/** Calculate safe memory size for an aligned buffer when using an unaligned
 * type as storage. This includes a safety-margin on (MEM_ALIGNMENT - 1) at the
 * start (e.g. if buffer is u8_t[] and actual data will be u32_t*)
 */
#ifndef LWIP_MEM_ALIGN_BUFFER
#define LWIP_MEM_ALIGN_BUFFER(size) (((size) + MEM_ALIGNMENT - 1U))
#endif

/** Align a memory pointer to the alignment defined by MEM_ALIGNMENT
 * so that ADDR % MEM_ALIGNMENT == 0
 */
 /* 数据起始地址对齐 */
#ifndef LWIP_MEM_ALIGN
#define LWIP_MEM_ALIGN(addr) ((void *)(((mem_ptr_t)(addr) + MEM_ALIGNMENT - 1) & ~(mem_ptr_t)(MEM_ALIGNMENT-1)))
#endif

为什么要对齐

  这个其实和各种处理器的硬件设计息息相关,具体可以参见该博文《为什么要内存对齐》。

对齐的本质

  首先,这里所说的对其是指 2 k 2^k 2k 字节的对其(其中k取0,1,2,3…正整数)。如果你纠结于什么3字节的对齐等。这里不适用! 在计算机中,所有的数据都是二进制!所谓对齐,就是将一串二进制的最后N位抹成 0。具体几位呢?这就是根据自己要对齐的字节数来定了。1字节对齐时N == 0;2 字节对齐时N == 1,4 字节对齐时N == 2;以此类推。对齐字节数是根据硬件平台来的,一般不会随便来。
  再一点就是,对齐的抹 0,是需要向上取整的。为什么要向上取整呢?如果网下取整,那么,实际返回的大小就比用户实际需要的要小了。这可就麻烦了,甚至由于不满足自己需要的大小,申请的这块内存都没法使用!

关于任意自己对齐,可以参考一下文章:
  1. 实现任意字节对齐的内存分配和释放
  2. 任意字节对齐的内存分配和释放

LWIP_MEM_ALIGN_SIZE(size)

  这个宏的作用就是将指定的大小处理成对其后大小,对其的大小由用户提供宏值MEM_ALIGNMENT来决定。其中size为想要分配的大小。下面来看看这个宏:

  • ~(MEM_ALIGNMENT-1U): 这一步就是按照对应的对齐字节数据,将二进制的最后最后几位置为 0。例如,MEM_ALIGNMENT为4 ,则该步就将后2位置为了 0。自己可以转为二进制计算一下!
  • ((size) + MEM_ALIGNMENT - 1U): 这里其实就是为了在处理时,能够向上取整。

  下面是针对其中不同值得计算结果(MEM_ALIGNMENT表示要对其的字节,size 为想要的大小,ALIG表示对齐后实际的大小)

 MEM_ALIGNMENT = 1       MEM_ALIGNMENT = 2       MEM_ALIGNMENT = 4       MEM_ALIGNMENT = 8
size = 1   ALIG = 1     size = 1   ALIG = 2     size = 1   ALIG = 4     size = 1   ALIG = 8
size = 2   ALIG = 2     size = 2   ALIG = 2     size = 2   ALIG = 4     size = 2   ALIG = 8
size = 3   ALIG = 3     size = 3   ALIG = 4     size = 3   ALIG = 4     size = 3   ALIG = 8
size = 4   ALIG = 4     size = 4   ALIG = 4     size = 4   ALIG = 4     size = 4   ALIG = 8
size = 5   ALIG = 5     size = 5   ALIG = 6     size = 5   ALIG = 8     size = 5   ALIG = 8
size = 6   ALIG = 6     size = 6   ALIG = 6     size = 6   ALIG = 8     size = 6   ALIG = 8
size = 7   ALIG = 7     size = 7   ALIG = 8     size = 7   ALIG = 8     size = 7   ALIG = 8
size = 8   ALIG = 8     size = 8   ALIG = 8     size = 8   ALIG = 8     size = 8   ALIG = 8
size = 9   ALIG = 9     size = 9   ALIG = 10    size = 9   ALIG = 12    size = 9   ALIG = 16
size = 10  ALIG = 10    size = 10  ALIG = 10    size = 10  ALIG = 12    size = 10  ALIG = 16
size = 11  ALIG = 11    size = 11  ALIG = 12    size = 11  ALIG = 12    size = 11  ALIG = 16
size = 12  ALIG = 12    size = 12  ALIG = 12    size = 12  ALIG = 12    size = 12  ALIG = 16
size = 13  ALIG = 13    size = 13  ALIG = 14    size = 13  ALIG = 16    size = 13  ALIG = 16
size = 14  ALIG = 14    size = 14  ALIG = 14    size = 14  ALIG = 16    size = 14  ALIG = 16
size = 15  ALIG = 15    size = 15  ALIG = 16    size = 15  ALIG = 16    size = 15  ALIG = 16
size = 16  ALIG = 16    size = 16  ALIG = 16    size = 16  ALIG = 16    size = 16  ALIG = 16
size = 17  ALIG = 17    size = 17  ALIG = 18    size = 17  ALIG = 20    size = 17  ALIG = 24
size = 18  ALIG = 18    size = 18  ALIG = 18    size = 18  ALIG = 20    size = 18  ALIG = 24
size = 19  ALIG = 19    size = 19  ALIG = 20    size = 19  ALIG = 20    size = 19  ALIG = 24
size = 20  ALIG = 20    size = 20  ALIG = 20    size = 20  ALIG = 20    size = 20  ALIG = 24

LWIP_MEM_ALIGN(addr)

  这个宏用来处理数据起始地址对齐。其处理方式与上面的数据大小的处理没有任何区别。关于数据类型在arch.h的开头部分有定义,这里有个文件stdint.h需要注意一下!在某写平台,可能没有该文件,需要用户自己来添加。

#if !LWIP_NO_STDINT_H
#include <stdint.h>
typedef uint8_t   u8_t;
typedef int8_t    s8_t;
typedef uint16_t  u16_t;
typedef int16_t   s16_t;
typedef uint32_t  u32_t;
typedef int32_t   s32_t;
typedef uintptr_t mem_ptr_t;   /* 这个通常为一个 unsigned int 类型 */
#endif

LwIP中宏配置及使用

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

  • MEM_LIBC_MALLOC: 该宏值定义是否使用C 运行时库自带的内存分配策略。该值默认情况下为0,表示不使用C 运行时库自带的内存分配策略。即默认使用LwIP提供的内存堆分配策略。
      如果要使用C运行时库自带的分配策略,则需要把该值定义为 1。此时,宏值MEM_USE_POOLS必然不能为 1。
  • MEMP_MEM_MALLOC: 该宏值定义是否使用lwip内存堆分配策略实现内存池分配(即:要从内存池中获取内存时,实际是从内存堆中分配)。默认情况下为 0,表示不从内存堆中分配,内存池为独立一块内存实现。MEM_USE_POOLS只能选择其一
  • MEM_USE_POOLS:该宏值定时是否使用lwip内存池分配策略实现内存堆的分配(即:要从内存堆中获取内存时,实际是从内存池中分配)。默认情况下为 0,表示不使用。MEMP_MEM_MALLOC只能选择其一
      要使用内存池的方式,则需要将该宏值定义为 1,且MEMP_MEM_MALLOC必须为 0,除此之外还需要做一下处理:
  1. MEMP_USE_CUSTOM_POOLS == 1
  2. 新建文件lwippools.h,并且在该文件中定义如下内存池(想多定义几个时,必须在宏LWIP_MALLOC_MEMPOOL_START和LWIP_MALLOC_MEMPOOL_END之间添加):
LWIP_MALLOC_MEMPOOL(20, 256)
LWIP_MALLOC_MEMPOOL(10, 512)
LWIP_MALLOC_MEMPOOL(5, 1512)
LWIP_MALLOC_MEMPOOL_END

  根据宏值的不同,mem.c/h会部分有效,下面简单看看mem.c文件的结构:

#if MEM_LIBC_MALLOC
/* 这里表示使用C库时的部分 */
#elif MEM_USE_POOLS
/* 这里表示使用内存池方式实现的内存堆函数 */
#else
/* 使用内存堆实现分配函数 */
#endif

而对于memp.c/h来说,就没这么复杂了。因为该文件就是独立实现内存池的管理策略的,无论宏值怎么配置,里面的函数就是那些。唯一有影响的宏值是MEMP_MEM_MALLOC后面会说明。
  总结来说,无论宏值怎么配置,LwIP都有两种内存管理策略:内存堆内存池

  • 内存堆: 可以来自C库,也可以使用LwIP自己实现的。
  • 内存池: 可以单独实现,也可以从内存堆中分配实现。

  上面说了如何进行配置,那么在LwIP内部是如何做到兼容以上的灵活配置的呢?其实很简单,LwIP内部全部使用统一的内存分配函数,只是在不同模式下,对相应的函数进行了重新定义,具体函数如下:

  • void mem_init(void):内存堆初始化函数,主要设置内存堆的起始地址,以及初始化空闲列表,lwip初始化时调用,内部接口。配置不同时,其实现也不同(可能为空)
  • void *mem_trim(void *mem, mem_size_t size): 减小mem_malloc所分配的内存。mem为申请到的内存的地址,size为新的大小。

与标准C函数realloc不同,该函数只能缩减分配内存

  • void *mem_malloc(mem_size_t size):申请分配内存,size为需要申请的内存字节数,返回值为最新分配的内存块的数据地址。
  • void *mem_calloc(mem_size_t count, mem_size_t size):是对mem_malloc()函数的简单包装,两个入口参数,count为元素的总个数,size为每个元素大小,两个参数的乘积就是实际要分配的内存空间的大小。mem_malloc()不同的是它会把动态分配的内存清零
  • void mem_free(void *mem):内存释放函数,mem前面申请到的内存时分配得地址。

这样,无论用户选择了什么配置方式,对于LwIP内部来说,函数全都是一个样的!下面,详细说明每种方式下,以上函数是怎么实现的。

C库分配方式

  上面已经说了要使用该方式如何进行配置,下面结合源码看看,如果用户选择了该方式,LwIP内部是如何处理的。该部分对应的源码文件为mem.c
  首先,如果用户选择了该方式,那么内存处理函数void mem_init(void)和void* mem_trim(void *mem, mem_size_t size)将没有实际的实现内容,既然选择了C库策略,此时也必然没法实现。
  接下来我们将看到如下代码:

/* in case C library malloc() needs extra protection,
 * allow these defines to be overridden.
 */
#ifndef mem_clib_free
#define mem_clib_free free
#endif
#ifndef mem_clib_malloc
#define mem_clib_malloc malloc
#endif
#ifndef mem_clib_calloc
#define mem_clib_calloc calloc
#endif

#if LWIP_STATS && MEM_STATS
#define MEM_LIBC_STATSHELPER_SIZE LWIP_MEM_ALIGN_SIZE(sizeof(mem_size_t))
#else
#define MEM_LIBC_STATSHELPER_SIZE 0
#endif

这里将C库的标准内存处理函数进行了重定义,没啥太大作用,就是为了后面使用的方便。下面以void * mem_malloc(mem_size_t size)为例看看以上宏值的用法(其他函数类似,注意:有几个函数在文件的最末尾统一处理了):

void *
mem_malloc(mem_size_t size)
{
  /* 这里就是调用的malloc有木有! */
  void* ret = mem_clib_malloc(size + MEM_LIBC_STATSHELPER_SIZE);
  if (ret == NULL) {  /* 分配失败 */
    MEM_STATS_INC(err);
  } else { /* 需要重点注意的就是,内存的对其问题,下面就是处理对其的。然后直接返回申请到的内存地址。 */
    LWIP_ASSERT("malloc() must return aligned memory", LWIP_MEM_ALIGN(ret) == ret);
#if LWIP_STATS && MEM_STATS
    *(mem_size_t*)ret = size;
    ret = (u8_t*)ret + MEM_LIBC_STATSHELPER_SIZE;
    MEM_STATS_INC_USED(used, size);
#endif
  }
  return ret;
}

  最后,使用C库内存处理策略时,LwIP的处理就是这么简单,也没啥需要特殊注意的地方!如果说非得注意点啥,就是一旦选择了该种模式,内存池的配置问题。

LwIP内存堆

  上面已经说了要使用该方式如何进行配置,下面结合源码看看,如果用户选择了该方式,LwIP内部是如何处理的。该部分对应的源码文件为mem.c

用内存池实现

  首先,LwIP会判断用户是否定义了宏值MEM_USE_POOLS,如果定义了该宏值,表示内存堆的内存来自于内存池,需要调用内存池的相关函数来处理内存。
  既然是调用内存堆中的各函数,那么这边的实现也相对简单。这与上面的调用C库的各函数没啥太大区别,至少在处理逻辑上是一致的,当然,由于内存池的实现上的原因,该部分看着还是挺复杂的!下面仍旧以函数void *mem_malloc(mem_size_t size)来看看LwIP是如何处理的。

void *
mem_malloc(mem_size_t size)
{
  void *ret;
  struct memp_malloc_helper *element = NULL;
  memp_t poolnr;
  mem_size_t required_size = size + LWIP_MEM_ALIGN_SIZE(sizeof(struct memp_malloc_helper));

  for (poolnr = MEMP_POOL_FIRST; poolnr <= MEMP_POOL_LAST; poolnr = (memp_t)(poolnr + 1)) {
    /* is this pool big enough to hold an element of the required size
       plus a struct memp_malloc_helper that saves the pool this element came from? */
    if (required_size <= memp_pools[poolnr]->size) {
      element = (struct memp_malloc_helper*)memp_malloc(poolnr);
      if (element == NULL) {
        /* No need to DEBUGF or ASSERT: This error is already taken care of in memp.c */
#if MEM_USE_POOLS_TRY_BIGGER_POOL
        /** Try a bigger pool if this one is empty! */
        if (poolnr < MEMP_POOL_LAST) {
          continue;
        }
#endif /* MEM_USE_POOLS_TRY_BIGGER_POOL */
        MEM_STATS_INC(err);
        return NULL;
      }
      break;
    }
  }
  if (poolnr > MEMP_POOL_LAST) {
    LWIP_ASSERT("mem_malloc(): no pool is that big!", 0);
    MEM_STATS_INC(err);
    return NULL;
  }

  /* save the pool number this element came from */
  element->poolnr = poolnr;
  /* and return a pointer to the memory directly after the struct memp_malloc_helper */
  ret = (u8_t*)element + LWIP_MEM_ALIGN_SIZE(sizeof(struct memp_malloc_helper));

#if MEMP_OVERFLOW_CHECK || (LWIP_STATS && MEM_STATS)
  /* truncating to u16_t is safe because struct memp_desc::size is u16_t */
  element->size = (u16_t)size;
  MEM_STATS_INC_USED(used, element->size);
#endif /* MEMP_OVERFLOW_CHECK || (LWIP_STATS && MEM_STATS) */
#if MEMP_OVERFLOW_CHECK
  /* initialize unused memory (diff between requested size and selected pool's size) */
  memset((u8_t*)ret + size, 0xcd, memp_pools[poolnr]->size - size);
#endif /* MEMP_OVERFLOW_CHECK */
  return ret;
}

  该部分许多地方需要看看后面的内存池。再次不做过多深入说明。

独立实现内存堆

  如果没有定义宏值MEM_USE_POOLS,那么LwIP提供了一套独立实现的内存堆处理接口。
  内存堆的本质是对一个事先定义好的内存块进行合理有效的组织和管理。主要用于任意大小的内存分配,实现较复杂,分配需要查找,回收需要合并,容易产生内存碎片,需要合理估算内存堆的总大小。LwIP使用类似于链表的结构来组织管理堆内存。节点的结构如下(在该部分的源码实现中,第一部分的代码便是以下结构体):

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;          /* 此内存快是否被用。1使用、0 未使用 */
};

  LwIP的内存堆实现中,分配的内存块有个最小大小的限制,要求请求的分配大小不能小于 MIN_SIZE,默认 MIN_SIZE为 12 字节。所以在该部分实现中,我看到的第二部分便是如下结构(第一部分是链表结构体):

/** All allocated blocks will be MIN_SIZE bytes big, at least!
 * MIN_SIZE can be overridden to suit your needs. Smaller values save space,
 * larger values could prevent too small blocks to fragment the RAM too much. */
#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)				/* 用户定义的堆大小做对齐处理,后面均用对齐后的该宏值 */

这就有一个问题,为什么默认值是12呢?因为这正好是2个sizeof(struct mem)的长度。为什么是2个sizeof(struct mem)。后面再说内存堆的定义及初始化时,我们会详细说明。
  接下来就是使用宏值LWIP_DECLARE_MEMORY_ALIGNED定义内存堆所使用的内存空间的了,这个宏值在上面一节由介绍,具体如下。从下面的定义中,我们可以看到***内存堆空间就是定义的一个名为ram_heap,大小为MEM_SIZE_ALIGNED + (2U*SIZEOF_STRUCT_MEM)的数组***。内存堆管理的具体实现,就是实现如何管理使用这个数组!

/** 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 */
/* 下面用全局变量的形式(名字为ram_heap),定义堆内存空间。并且由于对齐问题,不能直接使用数组名。因此,后面再用时全部处理为指针 */
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 */

注意:这里的大小多加了两个SIZEOF_STRUCT_MEM

  在定义完内存空间后,接下来就是一些为了管理内存堆,而定义的结构性的全局变量的定义。再后面是一些内存保护相关的宏,这里不具体说明。下面看看,为了管理内存堆,LwIP都定义了那些变量。

/** 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;		/* 指向已被释放的索引号最小的内存块(内存堆最前面的已被释放的)。*/

下面,就结合各接口函数来说说LwIP对内存堆的管理是如何实现的。先说第一个函数void mem_init(void)。先好看看源码,具体说明见其中的注释部分:

void
mem_init(void)
{
  struct mem *mem;

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

  /* align the heap 对内存堆的地址(全局变量的名)进行对齐。*/
  ram = (u8_t *)LWIP_MEM_ALIGN(LWIP_RAM_HEAP_POINTER);
  /* initialize the start of the heap 建立第一个内存块,内存块由内存块头+空间组成。 */
  mem = (struct mem *)(void *)ram;
  mem->next = MEM_SIZE_ALIGNED;		/* 下一个内存块不存在,因此指向内存堆的结束 */
  mem->prev = 0;					/* 前一个内存块就是它自己,因为这是第一个内存块 */
  mem->used = 0;					/* 第一个内存块没有被使用 */
  /* initialize the end of the heap 建立最后一个内存块 */
  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;/* 前一个不存在,因此指向内存堆的结束 */

  /* 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);
  }
}

  经过初始化,LwIP将内存堆划分为了如下格式:
MemInit
上图中已经非常详细的给出了从定义到初始化后,详细的内存堆的结构!在整个内存堆的开头和结尾,各有一个内存块头。开头的内存块头用来定义第一个内存块(也就是整个内存堆只有一个内存块),最后一个内存块头用来标识内存块的结尾。 看图的时候,结合源码和上一节的内存对齐部分。因为在上图中,到处都是对齐!
  接下来,在看看函数void *mem_malloc(mem_size_t size)。仍旧是先看源代码,具体语句在源码中都有注释,对于源码中,保护部分暂不作说明!

void *
mem_malloc(mem_size_t size)
{
  mem_size_t ptr, ptr2;
  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 == 0) {
    return NULL;
  }

  /* Expand the size of the allocated memory region so that we can
     adjust for alignment. */
  size = LWIP_MEM_ALIGN_SIZE(size);

  if (size < MIN_SIZE_ALIGNED) {  /* 这里处理 最小内存块的限制问题 不能小于前面定义的宏值 */
    /* every data block must be at least MIN_SIZE_ALIGNED long */
    size = MIN_SIZE_ALIGNED;
  }

  if (size > MEM_SIZE_ALIGNED) {
    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_size_t)((u8_t *)lfree - ram); ptr < MEM_SIZE_ALIGNED - size;
         ptr = ((struct mem *)(void *)&ram[ptr])->next) {/* 遍历内存堆,注意遍历时的开始地址:永远从最前面开始找。因为lfree指向已释放的最靠前的内存块 */
      mem = (struct mem *)(void *)&ram[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 */

      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 = ptr + SIZEOF_STRUCT_MEM + size;   /* 指向申请后的位置,即:建立下一个未使用的内存块的头部。即:插入一个新空内存块 */
          /* create mem2 struct */
          mem2 = (struct mem *)(void *)&ram[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;		/* 将当前分配的内存块标记为 已使用 */

          if (mem2->next != MEM_SIZE_ALIGNED) {/* 中间节点 建立后,原来的下一个节点的前一个要指向 新建的空节点 */
            ((struct mem *)(void *)&ram[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_size_t)((u8_t *)mem - ram));
        }
#if LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT
mem_malloc_adjust_lfree:
#endif /* LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT */
        if (mem == lfree) {		/* 这里处理:当分配出去的内存正好是lfree时,因为该内存块已经被分配出去了,必须修改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 = (struct mem *)(void *)&ram[cur->next];		/* 下一个内存块 */
          }
          lfree = cur; /* 指向找到的 第一个已释放的内存块。如果上面没有找到,则lfree =    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);

        return (u8_t *)mem + SIZEOF_STRUCT_MEM;		/* 这里返回 内存块的空间的地址,排除内存块的头 */
      }
    }
#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 */
  LWIP_DEBUGF(MEM_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("mem_malloc: could not allocate %"S16_F" bytes\n", (s16_t)size));
  MEM_STATS_INC(err);
  LWIP_MEM_ALLOC_UNPROTECT();
  sys_mutex_unlock(&mem_mutex);
  return NULL;
}

  分配内存的过程,就是从初始化后的内存块中,划分新内存块的过程。新内存块划分完成后,将其标记为已使用,并且新建的空内存块。在划分过程中,需要处理已经被释放的内存块的问题。划分后如下图所示:
MemMalloc
  首先需要重点注意的就是遍历查找新内存块时的起始位置:ptr = (mem_size_t)((u8_t *)lfree - ram)。前面说过,变量lfree永远指向内存堆中最靠前的那个已经释放的内存块。这也就是意味值,每次总是从整个内存堆的开始的第一个空闲内存块开始找合适的内存块的,即便后面可能有更大的空间!
  这其中有个需要注意的地方:if (mem->next - (ptr + SIZEOF_STRUCT_MEM) >= (size + SIZEOF_STRUCT_MEM + MIN_SIZE_ALIGNED))。这个地方具体是干嘛的呢?看下图(图例不考虑对齐问题):
MemMalloc2
  接下来一个注意的地方就是if (mem2->next != MEM_SIZE_ALIGNED) { 省略...。这个地方又是干什么用的呢?具体看下图(图例不考虑对齐问题)
MemMalloc3
  其实上面两个问题,就是针对中间已经释放过的内存块由重新划分时的处理。一旦从释放过的内存块中划分出新的内存后,则可能会导致剩余的内存块有可能充足,也可能不充足,这就是针对以上两个情况!
  最后再看看内存的释放函数,和上面相同,仍旧从源代码说起,具体见注释。

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;
  }
  LWIP_ASSERT("mem_free: sanity check alignment", (((mem_ptr_t)rmem) & (MEM_ALIGNMENT-1)) == 0);

  LWIP_ASSERT("mem_free: legal memory", (u8_t *)rmem >= (u8_t *)ram &&
    (u8_t *)rmem < (u8_t *)ram_end);

  if ((u8_t *)rmem < (u8_t *)ram || (u8_t *)rmem >= (u8_t *)ram_end) {		/* 检查是否在内存堆范围内 */
    SYS_ARCH_DECL_PROTECT(lev);
    LWIP_DEBUGF(MEM_DEBUG | LWIP_DBG_LEVEL_SEVERE, ("mem_free: illegal memory\n"));
    /* protect mem stats from concurrent access */
    SYS_ARCH_PROTECT(lev);
    MEM_STATS_INC(illegal);
    SYS_ARCH_UNPROTECT(lev);
    return;
  }
  /* 第二步:查找指定的内存块,标记为未使用 */
  /* protect the heap from concurrent access */
  LWIP_MEM_FREE_PROTECT();
  /* 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_malloc的到的地址是不含 struct mem 的 */
  /* ... which has to be in a used state ... */
  LWIP_ASSERT("mem_free: mem->used", mem->used);
  /* ... and is now unused. */
  mem->used = 0;
  /* 第三步:需要移动全局的释放指针,因为lfree始终指向内存堆中最小索引的那个已经释放的内存块 */
  if (mem < lfree) {
    /* the newly freed struct is now the lowest */
    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(mem);
#if LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT
  mem_free_count = 1;
#endif /* LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT */
  LWIP_MEM_FREE_UNPROTECT();
}

释放过程非常简单,就是将指定的内存块标记为未使用,其他不做任何处理。唯一处理的一个地方就就是全局变量lfree,必须检查是否为最前面的内存块,释放后如下图:
MemFree
内存块释放后,其原先建立的连接关系是不会改变的。
  这部分中还有一个函数:static void plug_holes(struct mem *mem),用来对相邻且未用的内存块进行合并。该函数暂时未使用,这里就不过多说明了!

LwIP内存池

见后文 LwIP 之 详解动态内存管理 内存池(memp.c/h)

ZC·Shou CSDN认证博客专家 学渣 码字员 砖家?
进步始于交流,收获源于分享!
进步始于交流,收获源于分享!
进步始于交流,收获源于分享!
进步始于交流,收获源于分享!
进步始于交流,收获源于分享!
已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页