LwIP源码分析(3):内存堆和内存池代码详解

文章目录

  • 1 内存堆
    • 1.1 mem_init
    • 1.2 mem_malloc
    • 1.3 mem_free
  • 2 内存池
    • 2.1 memp_init
    • 2.2 memp_malloc
    • 2.3 memp_free
  • 3 内存管理宏定义

在嵌入式系统中,内存池有助于快速有效地分配内存。LwIP提供了两个灵活的方式来管理和组织内存池的大小:内存堆和内存池。当然它还支持C库中的 mallocfree来申请和释放内存,但是这种分配方式可能会产生很多堆碎片,最后造成没有内存可以分配,而且运行地越久寻找新内存的时间可能也越长,这里不建议使用这种方式。下面通过代码来详细地看一下内存堆和内存池的实现

1 内存堆

来看看LwIP中实现的堆内存管理的实现:

1.1 mem_init

先来看看mem_init函数,相关结构体和宏定义写在前面:

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

#define MEM_ALIGNMENT 4
#define LWIP_MEM_ALIGN_SIZE(size) (((size) + MEM_ALIGNMENT - 1U) & ~(MEM_ALIGNMENT-1U))
#define SIZEOF_STRUCT_MEM    LWIP_MEM_ALIGN_SIZE(sizeof(struct mem))

/* 用户自定义的堆内存大小 */
#define MEM_SIZE (32 * 1024)
#define MEM_SIZE_ALIGNED     LWIP_MEM_ALIGN_SIZE(MEM_SIZE)
#define LWIP_DECLARE_MEMORY_ALIGNED(variable_name, size) u8_t variable_name[LWIP_MEM_ALIGN_BUFFER(size)]
LWIP_DECLARE_MEMORY_ALIGNED(ram_heap, MEM_SIZE_ALIGNED + (2U * SIZEOF_STRUCT_MEM));
#define LWIP_RAM_HEAP_POINTER ram_heap

static struct mem * ptr_to_mem(mem_size_t ptr)
{
  return (struct mem *)(void *)&ram[ptr];
}

static struct mem * LWIP_MEM_LFREE_VOLATILE lfree;
static u8_t *ram;
---------------------------------------------------------------
void mem_init(void)
{
    struct mem *mem;
	/* 检查struct mem是否是四字节对齐 */
    LWIP_ASSERT("Sanity check alignment",
    (SIZEOF_STRUCT_MEM & (MEM_ALIGNMENT - 1)) == 0);

    /* 将ram变量的指针指向上面声明的ram_heap数组,其大小为(MEM_SIZE+2*mem结构体大小) */
    ram = (u8_t *)LWIP_MEM_ALIGN(LWIP_RAM_HEAP_POINTER);
    /* 上面ram_heap数组的大小多了两个mem结构体,分别填充在ram_heap的起始和结尾 */
    /* 初始化堆的起始 */
    mem = (struct mem *)(void *)ram;
    /* 下一个内存的偏移量为MEM_SIZE_ALIGNED,即结束 */
    mem->next = MEM_SIZE_ALIGNED;
    /* 前一个内存的偏移量 */
    mem->prev = 0;
    /* 当前内存没有被使用 */
    mem->used = 0;
    /* 初始化堆的结束:指向上一个内存的结束位置 */
    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_free()后调用,确保堆链表结构体内内存的合法性和是否对齐 */
    MEM_SANITY();

    /* 将堆内存中第一块可用内存的指针赋值给lfree */
    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);
    }
}

初始化后ram_heap数组的内存分配如下所示:

LwIP源码分析(3):内存堆和内存池代码详解_第1张图片

1.2 mem_malloc

首先来看看内存分配的函数,它的原理是First Fit,即遍历空闲内存,找到第一个满足大小的堆内存进行分配,并将剩下的返回内存堆。

对于mem_malloc有三种方式,第一种为C库的malloc,一种为使用内存池(memory pool)方式,还有一种就是内存堆方式,这里对内存堆分配方式进行介绍(具体实现见代码注释):

  • 该函数中有一个宏定义:LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT,这是允许用户可以在中断或不允许等待信号量的上下文中释放pbuf,它的原理就是使用信号量和SYS_ARCH_PROTECT保护mem_malloc函数,使用SYS_ARCH_PROTECT保护mem_free函数。只有mem_mallocSYS_ARCH_UNPROTECTs后,mem_free才可以允许。

    • 很明显,这种方式要经常开关中断,如果在内存很少的嵌入式系统中,mem_malloc的速度会非常慢,一般都不会打开,所以这里不对相关宏定义的代码进行解析。
  • 函数中对于overflow检查的相关代码也将省略

void* mem_malloc(mem_size_t size_in)
{
    mem_size_t ptr, ptr2, size;
    struct mem *mem, *mem2;
    /* 声明一个unsigned long的变量lev用于同步 */
    LWIP_MEM_ALLOC_DECL_PROTECT();
    /* 分配的内存大小为0则直接返回 */
    if (size_in == 0)
    {
        return NULL;
    }

    /* 保证分配的内存是字节对齐的 */
    size = (mem_size_t)LWIP_MEM_ALIGN_SIZE(size_in);
    /* 分配的内存的大小必须比MIN_SIZE_ALIGNED(12)大:防止分配过多小内存造成内存碎片 */
    if (size < MIN_SIZE_ALIGNED)
    {
        /* every data block must be at least MIN_SIZE_ALIGNED long */
        size = MIN_SIZE_ALIGNED;
    }
    /* 如果size大于总的堆内存的大小或者小于待分配内存的大小,则返回NULL */
    if ((size > MEM_SIZE_ALIGNED) || (size < size_in))
    {
        return NULL;
    }

    /* 获得在mem_init中创建的互斥量,以并发地使用内存管理函数 */
    sys_mutex_lock(&mem_mutex);
    /* 在有OS的情况下:如果当前函数在中断中运行,则lev=basepri并设置优先级高于lev的中断才能打断;
       否则进入临界区 */
    LWIP_MEM_ALLOC_PROTECT();

    /* 从lfree指针开始用first fit方式寻找第一个足够大的内存 */
    for (ptr = mem_to_ptr(lfree); ptr < MEM_SIZE_ALIGNED - size;
            ptr = ptr_to_mem(ptr)->next)
    {
        mem = ptr_to_mem(ptr);
        /* 如果该内存没有used且大小满足(size+内存头mem结构体)的大小 */
        if ((!mem->used) && (mem->next - (ptr + SIZEOF_STRUCT_MEM)) >= size)
        {
            /* 如果该内存除去我们申请的内存后,剩余的大小还比(struct mem + 最小申请内存MIN_SIZE_ALIGNED)大
               则将剩下的内存返回内存堆中 */
            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;
                }
            }
            else
            {
                /* 剩下的内存不足以分配一个新的内存堆,只好浪费,标记整块内存used */
                mem->used = 1;
            }
            /* 如果分配的内存为第一块,则需要更新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)
                {
                    cur = ptr_to_mem(cur->next);
                }
                lfree = cur;
                LWIP_ASSERT("mem_malloc: !lfree->used", ((lfree == ram_end) || (!lfree->used)));
            }
            /* 对应前面的protect,若在中断中则使能中断并清空basepri寄存器;
               若不在中断中,则退出临界区 */
            LWIP_MEM_ALLOC_UNPROTECT();
            /* 释放互斥锁 */
            sys_mutex_unlock(&mem_mutex);
            /* 返回申请的内存的地址,这里不打开完整性检查,即MEM_SANITY_OFFSET为0 */
            return (u8_t *)mem + SIZEOF_STRUCT_MEM + MEM_SANITY_OFFSET;
        }
    }
    /* 找不到足够大的内存,释放锁 */
    LWIP_MEM_ALLOC_UNPROTECT();
    sys_mutex_unlock(&mem_mutex);
    return NULL;
}

1.3 mem_free

mem_free函数也很好理解,具体见注释:

void mem_free(void *rmem)
{
  struct mem *mem;
  /* 和mem_malloc一样,声明一个lev变量 */
  LWIP_MEM_FREE_DECL_PROTECT();

  if (rmem == NULL) {
    return;
  }
  /* 由于分配的内存都是四字节对齐的,若不对齐则传入的参数错误 */
  if ((((mem_ptr_t)rmem) & (MEM_ALIGNMENT - 1)) != 0) {
    LWIP_MEM_ILLEGAL_FREE("mem_free: sanity check alignment");
    return;
  }

  /* 获取该段内存前面的结构体的内容 */
  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) {
    return;
  }

  /* 痛mem_malloc中的LWIP_MEM_ALLOC_PROTECT() */
  LWIP_MEM_FREE_PROTECT();
  /* 如果mem->used为0,说明参数出错,直接返回 */
  if (!mem->used) {
    return;
  }
  /* 该函数检查mem->prev和mem->next的参数合法性 */
  if (!mem_link_valid(mem)) {
    LWIP_MEM_FREE_UNPROTECT();
    return;
  }

  /* 标记该mem为not used */
  mem->used = 0;
  /* 如果释放的mem为第一块内存,则更新lfree */
  if (mem < lfree) {
    /* the newly freed struct is now the lowest */
    lfree = mem;
  }

  /* 最后,如果释放的内存的prev和next有not used的话,则合并 */
  plug_holes(mem);
  LWIP_MEM_FREE_UNPROTECT();
}

2 内存池

在LwIP的数据段中保留了一个固定大小的静态内存,这段内存被分为多个内存池(pools),用来描述各种不同的数据结构,比如TCP、UDP、IP等报文的首部,它们的长度是固定的,这样每次分配和释放都不会产生内存碎片。比如有一个内存池用于TCP控制结构体struct tcp_pcb,另一个内存池用于UDP控制结构体struct udp_pcb。每个内存池都可以配置它可以容量的最大数量的数据结构,比如上面的TCP和UDP控制块的个数可以通过MEMP_NUM_TCP_PCBMEMP_NUM_UDP_PCB进行修改。

下面来对内存池的代码进行分析,代码中去掉了统计分析和overflow检查的相关代码。

2.1 memp_init

首先是内存池的初始化函数:

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是什么:

const struct memp_desc *const memp_pools[MEMP_MAX] = {
#define LWIP_MEMPOOL(name,num,size,desc) &memp_ ## name,
#include "lwip/priv/memp_std.h"
};

其中MEMP_MAX的定义如下:

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

可以看出来,在memp_std.h中定义了多个内存池,每个内存池的参数包括:名字(name)、个数(num)、大小(size)和描述(desc),下面来看一下这个文件:

#define LWIP_DECLARE_MEMORY_ALIGNED(variable_name, size) u8_t variable_name[LWIP_MEM_ALIGN_BUFFER(size)]

#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 \
  };
  
#define LWIP_MEMPOOL(name,num,size,desc) LWIP_MEMPOOL_DECLARE(name,num,size,desc)
------------
#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 */
  • 对于代码中#的使用参考文章:C语言 #和##的使用

可以看到一个宏定义LWIP_MEMPOOL就根据参数声明了一个内存堆数组,还有mempmemp_desc结构体。

现在回过头来看MEMP_MAX,前面的#define LWIP_MEMPOOL(name,num,size,desc) MEMP_##name就是暂时用一下文件中这个定义,将LWIP_MEMPOOL的宏定义再进行一层替换,作为enum的值的名称,所以MEMP_MAX就是内存堆的总个数。

memp_pools就是将在memp_std.h中用LWIP_MEMPOOL声明的各个内存池的memp_desc结构体的地址包含进来。

最后再回来看memp_init_pool函数:

struct memp {
  struct memp *next;
};

struct memp_desc {
  /** Element size */
  u16_t size;
  /** Number of elements */
  u16_t num;
  /** Base address */
  u8_t *base;
  /** First free element of each pool. Elements form a linked list. */
  struct memp **tab;
};
--------
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);

  /* 初始化为0:如果MEMP_MEM_INIT宏定义为1的话 */
  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);
  }
}

其中base就是通过LWIP_MEMPOOL_DECLARE声明的第一项大小为num*(MEMP_SIZE+MEMP_ALIGN_SIZE(size))的数组。除了每个内存池的大小是固定的,它的原理和内存堆类似,内存池的大小包括内存池的实际存数据大小和内存池结构体struct memp的大小。
tab就是通过LWIP_MEMPOOL_DECLARE声明的struct *memp指针变量的地址,它表示内存池中第一个没有使用的pool。

所以这个函数就是将每个声明的内存池通过链表连接起来,其中desc->tab为每个内存池的基地址。

最后,根据memp_init_pool函数,memp_pools中的每个memp_desc初始化后的示意图如下:
LwIP源码分析(3):内存堆和内存池代码详解_第2张图片

2.2 memp_malloc

来看一下内存池分配函数:

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_fn(memp_pools[type], file, line);
	return memp;
}

参数type的定义如下:

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

memp_t就是根据LWIP_MEMPOOL定义的各个内存池的name而组成的枚举类型,我们就通过这个枚举类型来申请相应的内存池中的内存。

然后来看看do_memp_malloc_pool_fn函数:

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的tab */
	memp = *desc->tab;
	if (memp != NULL) {
		/* 指向下一个memp结构体 */
		*desc->tab = memp->next;
		SYS_ARCH_UNPROTECT(old_level);
		/* 将内存的首地址返回 */
		return ((u8_t *)memp + MEMP_SIZE);
	} else {
		SYS_ARCH_UNPROTECT(old_level);
	}
	
	return NULL;
}

2.3 memp_free

void memp_free(memp_t type, void *mem)
{
	if (mem == NULL) {
		return;
	}
	do_memp_free_pool(memp_pools[type], mem);
}

同样地来看看do_memp_free_pool函数:

static void do_memp_free_pool(const struct memp_desc *desc, void *mem)
{
	struct memp *memp;
	SYS_ARCH_DECL_PROTECT(old_level);
	/* 获得该内存前面的结构体的地址 */
	memp = (struct memp *)(void *)((u8_t *)mem - MEMP_SIZE);
	SYS_ARCH_PROTECT(old_level);
	/* 将该内存地址加入到desc->tab的最前面,表示空闲 */
	memp->next = *desc->tab;
	*desc->tab = memp;
	SYS_ARCH_UNPROTECT(old_level);
}

3 内存管理宏定义

(1)MEM_LIBC_MALLOC:使用C库中的mallocfree管理内存
(2)MEMP_MEM_MALLOC:使用内存堆方式来分配内存池的内存。上面内存池的分配中我把相关代码注释掉了,如果此宏打开,在分配和释放内存时会直接使用mem_mallocmem_free
(3)MEM_USE_POOLS:使用内存池的方式来分配内存堆的内存。同上。
若此宏打开,还需将宏MEMP_MEM_MALLOC关闭,并将MEMP_USE_CUSTOM_POOLS宏打开,表示创建一个lwippools.h文件,然后用LWIP_MALLOC_MEMPOOL来声明自己的内存。
下面给出一个lwippools.h的配置例子:

/* OPTIONAL: Pools to replace heap allocation
 * Optional: Pools can be used instead of the heap for mem_malloc. If
 * so, these should be defined here, in increasing order according to
 * the pool element size.
 *
 * LWIP_MALLOC_MEMPOOL(number_elements, element_size)
 */
#if MEM_USE_POOLS
LWIP_MALLOC_MEMPOOL_START
LWIP_MALLOC_MEMPOOL(100, 256)
LWIP_MALLOC_MEMPOOL(50, 512)
LWIP_MALLOC_MEMPOOL(20, 1024)
LWIP_MALLOC_MEMPOOL(20, 1536)
LWIP_MALLOC_MEMPOOL_END
#endif /* MEM_USE_POOLS */

/* Optional: Your custom pools can go here if you would like to use
 * lwIP's memory pools for anything else.
 */
LWIP_MEMPOOL(SYS_MBOX, 22, 100, "SYS_MBOX")
  • 除了声明内存池给内存堆分配外,可以在最后声明一个内存池供自己使用
  • 这种方式适合用在SRAM很大的情况下,以空间换取执行速度

你可能感兴趣的:(LwIP,网络,网络协议)