LwIP主要使用动态内存池(pool)和动态内存堆(heap)来进行内存管理。

谈及内存管理,常见的分配策略有两种,一种使用固定大小的内存分配策略,也就是说,用户每次申请都只能申请固定长度的内存;另外一种则是使用堆进行动态分配,这种方式可以分配出不同长度的内存。在LwIP中,两种方式都在特定的场景被使用,除此之外,还支持使用C库的malloc/free进行内存分配,不过这种方式并不建议使用,因为每次分配的时间开销并不确定,这会引发很多问题。

一、动态内存池(Pool)

一种申请内存大小必须是固定大小字节的分配策略,系统将可用区域以固定大小进行分割,可用的内存使用一个简单的链表连接起来,由于每个内存块大小都一样,需要分配时直接取出第一个交给用户就行了。

在LwIP中,为什么要使用动态内存池?因为协议栈里面有许多协议,每个协议的首部长度往往是固定的,使用pool,可以预先为这些固定长度的协议首部分配内存,以后每次需要处理协议首部的时候,都可以直接使用这段内存。

1、动态内存池的预处理

LwIP定义Pool的方式非常的有意思,首先我们关注一个文件,memp_std.h,位于include/lwip/priv这个目录下,这里我们关注如下这些宏定义

memp_std.h代码(部分)

#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 */

#if LWIP_ALTCP && LWIP_TCP
LWIP_MEMPOOL(ALTCP_PCB,      MEMP_NUM_ALTCP_PCB,       sizeof(struct altcp_pcb),      "ALTCP_PCB")
#endif /* LWIP_ALTCP && LWIP_TCP */

#if LWIP_IPV4 && IP_REASSEMBLY
LWIP_MEMPOOL(REASSDATA,      MEMP_NUM_REASSDATA,       sizeof(struct ip_reassdata),   "REASSDATA")
#endif /* LWIP_IPV4 && IP_REASSEMBLY */
#if (IP_FRAG && !LWIP_NETIF_TX_SINGLE_PBUF) || (LWIP_IPV6 && LWIP_IPV6_FRAG)
LWIP_MEMPOOL(FRAG_PBUF,      MEMP_NUM_FRAG_PBUF,       sizeof(struct pbuf_custom_ref),"FRAG_PBUF")
#endif /* IP_FRAG && !LWIP_NETIF_TX_SINGLE_PBUF || (LWIP_IPV6 && LWIP_IPV6_FRAG) */

#if LWIP_NETCONN || LWIP_SOCKET
LWIP_MEMPOOL(NETBUF,         MEMP_NUM_NETBUF,          sizeof(struct netbuf),         "NETBUF")
LWIP_MEMPOOL(NETCONN,        MEMP_NUM_NETCONN,         sizeof(struct netconn),        "NETCONN")
#endif /* LWIP_NETCONN || LWIP_SOCKET */

观察上面的代码会发现,根据使能的协议、配置项不同,使用LWIP_MEMPOOL()这个宏的数量也不同,通过名称可以得出,这个宏似乎和内存池有着很强的联系,我们接着看以下的代码。

memp.h代码(部分)

 typedef enum
 {
 #define LWIP_MEMPOOL(name,num,size,desc)  MEMP_##name,
 #include "lwip/priv/memp_std.h"
     MEMP_MAX
 } memp_t;

结合这两段代码,我们可以发现,LWIP_MEMPOOL这个宏似乎用于定义这个枚举,当将所有编译选项/协议宏使能,预编译完成后,这个枚举类型应该是下面这样

memp.h代码(预编译完成后)

 typedef enum
 {
     MEMP_RAW_PCB,
     MEMP_UDP_PCB,
     MEMP_TCP_PCB,
     MEMP_TCP_PCB_LISTEN,
     MEMP_TCP_SEG,
     MEMP_ALTCP_PCB,
     MEMP_REASSDATA,
     MEMP_NETBUF,
     MEMP_NETCONN,
     MEMP_MAX
 } memp_t;

当然,事情似乎并没有如此简单,在协议栈中,我们会发现,在多处都会定义一次LWIP_MEMPOOL,并且每次在定义的地方,都会使用 #include "lwip/priv/memp_std.h",例如

文件memp.c(开头部分)

#define LWIP_MEMPOOL(name,num,size,desc) LWIP_MEMPOOL_DECLARE(name,num,size,desc)
#include "lwip/priv/memp_std.h"

如果将宏全部展开,和我们前面发现的枚举似乎时两码事了。这便是LwIP设计的高明之处,memp_std.h这个文件像是一个模板,上面列举了我们目前需要准备的东西,例如UDP的pcb内存池里一个单元要多大,要几个,名称是啥。当包含这个头文件时,根据这些信息,制定不同的功能。例如memp.h用它来定义枚举类型,memp.c用它来制定不同协议的pcb内存,这样做虽然阅读起来可能会有点费劲。但是可以保证配置的简洁性,提示编译的效率。

例如前面的memp.c展开后,例如

LWIP_MEMPOOL(RAW_PCB,        MEMP_NUM_RAW_PCB,         sizeof(struct raw_pcb),        "RAW_PCB")

可能就会变成(上述配置不同,结构体的参数也会不同,整个内存的长度也是不同的)

 u8_t memp_memory_RAW_PCB_base[((4 * 24) + 4 - 1U)];

 static struct memp *memp_tab_RAW_PCB;

 const struct memp_desc memp_RAW_PCB ={
     ((24) + 4 - 1U) & ~(4-1U)),
     (24),
     4
     memp_memory_RAW_PCB_base,
     &memp_tab_RAW_PCB
 };

其中struct memp_desc的成员如下

/** Memory pool descriptor */
struct memp_desc {
  /* 内存池中每个块的大小 */
  u16_t size;

  /* 内存池中有多少个块 */
  u16_t num;

  /** 内存的起始地址 */
  u8_t *base;

  /**第一个空闲的块 */
  struct memp **tab;

};

内存池单元块的描述结构体如下:

struct memp {
  struct memp *next;
};

2、动态内存池的初始化

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]);

}

这里的memp_pools也是用上一章节提到的方式添加的,它是一个由struct *memp_desc组成的数组,前面提到的 const struct memp_desc memp_RAW_PCB 的地址就是其中一员 。

memp_init_pool的作用就是将整个内存分成desc->num个,每个大小是desc->size,并将每个块都有一个next指针指向下一个块,第一个可用的块memp_desc->tab放在内存的末尾。

3、动态内存池的使用

[1] 动态内存池的分配

memp.c中,删减掉不必要的代码后

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

  memp = *desc->tab;
  if (memp != NULL) {
    *desc->tab = memp->next;
    SYS_ARCH_UNPROTECT(old_level);
    return ((u8_t *)memp + MEMP_SIZE);
  } else {
    SYS_ARCH_UNPROTECT(old_level);
  }

  return NULL;
}

可以发现,分配过程非常的简单直接,从desc中取出当前可用的内存块(desc->tab),然后移动desc->tab到memp的next,返回memp给用户即可

[2] 动态内存池的释放

memp.c中,删减掉不必要的代码后

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

     SYS_ARCH_PROTECT(old_level);

     memp->next = *desc->tab;                                        (2)
     *desc->tab = memp;                                              (3)

     SYS_ARCH_UNPROTECT(old_level);
 }

可以看出,释放也是很高效的,直接将内存找到,重新建立链表(修改当前的等待释放的memp的next为tab),然后让这段memp变成tab。意思就是将这段内存释放后,下次分配时,这段内存会是第一个被分配的。

二、内存堆(heap)

动态内存堆,可以申请任意大小的内存。有两种方式,一种是使用C库提供的Malloc/Free;另外一种是LwIP自身的内存堆管理策略。使用宏MEM_LIBC_MALLOC进行选择,宏值为1时,使用C库提供的内存分配接口。

1、动态内存堆的组织部分

struct mem {
  /*下一个内存块在内存堆中的索引*/
  mem_size_t next;
  /*上一个内存块在内存堆中的索引*/
  mem_size_t prev;
  /* 当前内存块是否被使用 */
  u8_t used;
};
/*申请的最小内存(需要大于struct mem这个结构)*/
 #define MIN_SIZE             12 

/*内存堆的定义 (用户可分配的(MEM_SIZE_ALIGNED) + 2个struct mem(最开始和结尾)的大小)*/
/*堆的名字叫heap*/
 LWIP_DECLARE_MEMORY_ALIGNED(ram_heap, MEM_SIZE_ALIGNED+(2U*SIZEOF_STRUCT_MEM));

/*使用宏进行重命名*/
#define LWIP_RAM_HEAP_POINTER       ram_heap

/*堆的起始地址*/
static u8_t *ram;

/*指向最后一个内存块的mem结构*/
static struct mem *ram_end;

/*内存保护锁*/
 static sys_mutex_t mem_mutex;  

/*mem类型指针,指向内存堆中低地址的空闲内存块*/
 static struct mem * LWIP_MEM_LFREE_VOLATILE lfree;

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 = (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 = ptr_to_mem(MEM_SIZE_ALIGNED);
  ram_end->used = 1;
  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);
  }
}

可以看出,上述代码就是在对上一节说的全局变量进行初始化。初始化完成后,内存堆就变成下面这个样子了

3、动态内存堆使用

[1]内存堆的内存分配

分配过程中,首先将用户申请的大小(size_in)进行对齐。对齐后,如果小于MIN_SIZE_ALIGNED,则设为最小的默认值,如果超出堆的大小,直接返回错误。

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

  size = (mem_size_t)LWIP_MEM_ALIGN_SIZE(size_in);
  if (size < MIN_SIZE_ALIGNED) {
    size = MIN_SIZE_ALIGNED;
  }
  if ((size > MEM_SIZE_ALIGNED) || (size < size_in)) {
    return NULL;
  }

接着是遍历整个堆,通过mem结构进行查询,当发现mem没被使用(mem->used == 0)且下一个mem块和当前mem块的间隔满足用户需求的内存块大小+mem结构大小,开始尝试从mem开始分配内存。

此时会有两种情况,一种是当前mem开始到mem->next之间的内存还有很大,此时需要分割,分割的方式就是在内存分配给用户后,在mem到mem->next中间再插入一个mem2,相当于mem2->next = mem->next; mem->next = mem2,当然,如果mem2的next此时并不是堆的末尾,那么说明那里有一个有效的mem,那么那个mem的prev应该是mem2。

当然,也有可能这段内存没有多余的部分,那么把mem标记成已经使用(mem->used = 1)就可以了。

 if (mem->next - (ptr + SIZEOF_STRUCT_MEM) >=
         (size + SIZEOF_STRUCT_MEM + MIN_SIZE_ALIGNED))
 {
     ptr2 = (mem_size_t)(ptr + SIZEOF_STRUCT_MEM + size);   
     LWIP_ASSERT("invalid next ptr",ptr2 != MEM_SIZE_ALIGNED);
     /* create mem2 struct */
     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;                                      

     if (mem2->next != MEM_SIZE_ALIGNED)
     {
         ptr_to_mem(mem2->next)->prev = ptr2;           
     }
     MEM_STATS_INC_USED(used, (size + SIZEOF_STRUCT_MEM));
 }
 else
 {
     mem->used = 1;                                     
     MEM_STATS_INC_USED(used, mem->next - mem_to_ptr(mem));
 }

在分配完成后,如果我们分配出去的内存堆是lfree,我们也应该更新lfree,方便下一次来分配内存。更新lfree的方法就是,直接从低地址遍历整个堆,找到第一个空闲的块。

例如分配一个24字节后的堆如下图所示:

[2]内存堆的内存释放

内存释放的操作也是比较简单的,根据用户需要释放的内存地址找到mem结构,将mem保存的内存块信息进行释放、合并,并将used字段清0,表示该段内存没被使用。

具体是这样做的,首先也会对传入的待释放内存进行检查,需要检查的点是内存是否为空、内存是否未对齐、内存块有没有被使用、内存块的prev和next正常不。如果上述有一个不正常,函数都直接返回。

检查完成后,将mem->used清0,调用plug_holes进行内存合并。

具体代码如下:

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

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

  if (mem < lfree) {
    /* the newly freed struct is now the lowest */
    lfree = mem;
  }

  /* finally, see if prev or next are free also */
  plug_holes(mem);


  LWIP_MEM_FREE_UNPROTECT();
}

static void
plug_holes(struct mem *mem)
{
  struct mem *nmem;
  struct mem *pmem;

  LWIP_ASSERT("plug_holes: mem >= ram", (u8_t *)mem >= ram);
  LWIP_ASSERT("plug_holes: mem < ram_end", (u8_t *)mem < (u8_t *)ram_end);
  LWIP_ASSERT("plug_holes: mem->used == 0", mem->used == 0);

  /* plug hole forward */
  LWIP_ASSERT("plug_holes: mem->next <= MEM_SIZE_ALIGNED", mem->next <= MEM_SIZE_ALIGNED);

  nmem = ptr_to_mem(mem->next);
  if (mem != nmem && nmem->used == 0 && (u8_t *)nmem != (u8_t *)ram_end) {
    /* if mem->next is unused and not end of ram, combine mem and mem->next */
    if (lfree == nmem) {
      lfree = mem;
    }
    mem->next = nmem->next;
    if (nmem->next != MEM_SIZE_ALIGNED) {
      ptr_to_mem(nmem->next)->prev = mem_to_ptr(mem);
    }
  }

  /* plug hole backward */
  pmem = ptr_to_mem(mem->prev);
  if (pmem != mem && pmem->used == 0) {
    /* if mem->prev is unused, combine mem and mem->prev */
    if (lfree == mem) {
      lfree = pmem;
    }
    pmem->next = mem->next;
    if (mem->next != MEM_SIZE_ALIGNED) {
      ptr_to_mem(mem->next)->prev = mem_to_ptr(pmem);
    }
  }
}

三、LwIP中的配置

1、是否使用LibC提供的Malloc实现堆,通过下面这个宏来决定,该值默认情况下为0,表示不使用C 标准库自带的内存分配策略。即默认使用LwIP提供的内存堆分配策略。如果要使用C标准库自带的分配策略,则需要把该值定义为 1

MEM_LIBC_MALLOC

2、使用内存堆来实现内存池分配(要从内存池分配时,其实是从内存堆进行分配),使用下面这个宏来决定,默认为0

MEMP_MEM_MALLOC

3、使用内存池来实现内存堆(要从内存堆中获取内存时,实际是从内存池中分配),使用下面这个宏来决定,默认为0

MEM_USE_POOLS

要使用这个功能,除了使能这个宏,还需要将MEMP_USE_CUSTOM_POOLS定义为1,并且在lwippools.h文件中定义一些内存池,例如

 LWIP_MALLOC_MEMPOOL_START

 LWIP_MALLOC_MEMPOOL(20, 256) //定义一个内存池,里面有20个块,每个块256个字节

 LWIP_MALLOC_MEMPOOL(10, 512)

 LWIP_MALLOC_MEMPOOL(5, 1512)

 LWIP_MALLOC_MEMPOOL_END

上述提到的 MEMP_MEM_MALLOCMEM_USE_POOLS 不可以同时被置1。