Lwip 内存管理

Lwip 用它自己的动态内存分配方式替代了标准C的malloc();
它可以使用MEM_POOL的方式分配内存,即开辟几个不同尺寸的缓冲池,给数据分配合适的缓冲区来存放,就类似于使用数组的方式,可能会比较浪费空间,但是可以有效避免内存碎片产生, 这在第二部分讲。两种方式分别在mem.c memp.c里面实现,下面先总结一般的内存堆分配方式。
(一) 内存堆动态分配方式

//mem.h
/** 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

首先是内存对齐,这里用到的MCU是按4字节对齐的。这一语句功能是将addr 调整成最靠近addr的且能被4整除的值,其实就是(addr+3)&(~(u32)(0x03)),+3之后有余数的自然就进位了然后再舍掉余数。其实也可以用 addr = (addr&0x03)?(addr+1):addr; 这样应该也可以,更容易理解一些,但是没有上面的简便。

#ifndef LWIP_RAM_HEAP_POINTER
/** the heap. we need one struct mem at the end and some room for alignment */
u8_t ram_heap[MEM_SIZE_ALIGNED + (2*SIZEOF_STRUCT_MEM) + MEM_ALIGNMENT];
#define LWIP_RAM_HEAP_POINTER ram_heap
#endif /* LWIP_RAM_HEAP_POINTER */
...
/**
 * The heap is made up as a list of structs of this type.
 * This does not have to be aligned since for getting its size,
 * we only use the macro SIZEOF_STRUCT_MEM, which automatically alignes.
 */
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;
};

这里申请了一个内存堆ram_heap, 大小包括整个MEM_SIZE + 两个STRUCT_MEM的大小 + 4, 假设申请了MEM_SIZE 为8k。

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

mem_init(void)
{
...
  /* 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;
  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;
...
}

内存初始化。ram 指向了ram_heap开头, 并且在开头初始化了一个struct mem。
ram_end 指向了MEM_SIZE之后的第一个STRUCT_MEM, 这里不知道是不是bug,我觉得应该指向&ram[MEM_SIZE_ALIGNED+SIZEOF_STRUCT_MEM]; 没时间了,以后再慢慢研究。
lfree最开始也指向了开头。
->next 等这些参数都是相对地址,相对ram的,方便之后用数组的方式访问。

void *
mem_malloc(mem_size_t size)
{
  /* 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;
  }
...
    /* 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) {
      mem = (struct mem *)(void *)&ram[ptr];
   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 excact 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));
        }
}

内存分配函数。首先对齐size,并且size不能太小或者太大,太小容易产生过多的碎片导致后期内存不足。
然后搜索heap,找到适合的块。lfree指向未分配的内存位置,刚开始它是在开头。
ptr=lfree-ram 即是lfree相对ram的位置,即已经用掉的内存大小。
ptr mem 先指向lfree所指的位置,当这块内存未被使用并且剩下的内存大于size时,…略复杂,待续…

(二) 内存池的动态分配
内存池顾名思义就是一个个划分好的池子,不像内存堆一样东西乱堆。
pbuf是协议栈里最常用的数据类型,它有多种类型的存储方式,如下:

* - PBUF_RAM: buffer memory for pbuf is allocated as one large
* chunk. This includes protocol headers as well.
* - PBUF_ROM: no buffer memory is allocated for the pbuf, even for
* protocol headers. Additional headers must be prepended
* by allocating another pbuf and chain in to the front of
* the ROM pbuf. It is assumed that the memory used is really
* similar to ROM in that it is immutable and will not be
* changed. Memory which is dynamic should generally not
* be attached to PBUF_ROM pbufs. Use PBUF_REF instead.
* - PBUF_REF: no buffer memory is allocated for the pbuf, even for
* protocol headers. It is assumed that the pbuf is only
* being used in a single thread. If the pbuf gets queued,
* then pbuf_take should be called to copy the buffer.
* - PBUF_POOL: the pbuf is allocated as a pbuf chain, with pbufs from
* the pbuf pool that is allocated during pbuf_init().

我们从pbuf入手研究一下MEM_POOL内存池的编写方式
pbuf_alloc(PBUF_POOL)调用的是:
p = (struct pbuf *)memp_malloc(MEMP_PBUF_POOL);
我们来看看MEMP_PBUF_POOL是如何声明的:

//memp_std.h
/*
 * A list of pools of pbuf's used by LWIP.
 *
 * LWIP_PBUF_MEMPOOL(pool_name, number_elements, pbuf_payload_size, pool_description)
 *     creates a pool name MEMP_pool_name. description is used in stats.c
 *     This allocates enough space for the pbuf struct and a payload.
 *     (Example: pbuf_payload_size=0 allocates only size for the struct)
 */
LWIP_PBUF_MEMPOOL(PBUF,      MEMP_NUM_PBUF,            0,                             "PBUF_REF/ROM")
LWIP_PBUF_MEMPOOL(PBUF_POOL, PBUF_POOL_SIZE,           PBUF_POOL_BUFSIZE,             "PBUF_POOL")


/* This treats "pbuf pools" just like any other pool.
 * Allocates buffers for a pbuf struct AND a payload size */
#define LWIP_PBUF_MEMPOOL(name, num, payload, desc) LWIP_MEMPOOL(name, num, (MEMP_ALIGN_SIZE(sizeof(struct pbuf)) + MEMP_ALIGN_SIZE(payload)), desc)

memp_std.h里定义了各种类型POOL.

//memp.c
/** This array holds the element sizes of each pool. */
const u16_t memp_sizes[MEMP_MAX] = {
#define LWIP_MEMPOOL(name,num,size,desc)  LWIP_MEM_ALIGN_SIZE(size),
#include "lwip/memp_std.h"
};
/** This array holds the number of elements in each pool. */
static const u16_t memp_num[MEMP_MAX] = {
#define LWIP_MEMPOOL(name,num,size,desc)  (num),
#include "lwip/memp_std.h"
};
/** This is the actual memory used by the pools (all pools in one big block). */
static u8_t memp_memory[MEM_ALIGNMENT - 1 
#define LWIP_MEMPOOL(name,num,size,desc) + ( (num) * (MEMP_SIZE + MEMP_ALIGN_SIZE(size) ) )
#include "lwip/memp_std.h"
];

memp.c 定义了 几个数组来保存每个pool的元素大小和数量。定义一个memp_memory[]内存块保存所有的pool。

/* 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/memp_std.h"
  MEMP_MAX
} memp_t;

## 是连接符号。这样memp_t的枚举类型里就有各种的POOL名字,如编译之后就成为:

typedef enum {
  MEMP_PBUF,
  MEMP_PBUF_POOL,
  ...
  MEMP_MAX
} memp_t;

总结一下
memp_std.h里包含许多条POOL的声明:
LWIP_PBUF_MEMPOOL(PBUF_POOL, PBUF_POOL_SIZE, PBUF_POOL_BUFSIZE, “PBUF_POOL”)
然后在memp.c 里面利用这种套路将memp_std.h 里的声明全包进去:

static u8_t memp_memory[MEM_ALIGNMENT - 1 
#define LWIP_MEMPOOL(name,num,size,desc) + ( (num) * (MEMP_SIZE + MEMP_ALIGN_SIZE(size) ) )
#include "lwip/memp_std.h"
];

就可以生成我们想要的结果。

static u8_t memp_memory[MEM_ALIGNMENT - 1 
 + ( (num) * (MEMP_SIZE + MEMP_ALIGN_SIZE(size) ) )
 + 
 ...
];

这是一种很聪明的写法。领教了!
回过头来我们就明白MEMP_PBUF_POOL内存到底多大了,Demo设置的是48*512byte。
另外前面有一个是MEMP_PBUF的内存池,它的BUFSIZE是0,仅能用来保存结构体指针,所以是专门给PBUF_REF/ROM类型使用的。
声明好内存池后,我们看看p = (struct pbuf *)memp_malloc(MEMP_PBUF_POOL); 是怎么工作的。

/** This array holds the first free element of each pool.
 *  Elements form a linked list. */
static struct memp *memp_tab[MEMP_MAX];

/**
 * Initialize this module.
 * 
 * Carves out memp_memory into linked lists for each pool-type.
 */
void
memp_init(void)
{
  struct memp *memp;
  u16_t i, j;
  ...
  /* for every pool: */
  for (i = 0; i < MEMP_MAX; ++i) {
    memp_tab[i] = NULL;

    /* create a linked list of memp elements */
    for (j = 0; j < memp_num[i]; ++j) {
      memp->next = memp_tab[i];
      memp_tab[i] = memp;
      memp = (struct memp *)(void *)((u8_t *)memp + MEMP_SIZE + memp_sizes[i]

      );
    }
  }
 ...
}

/**
 * Get an element from a specific pool.
 *
 * @param type the pool to get an element from
 *
 * the debug version has two more parameters:
 * @param file file name calling this function
 * @param line number of line where this function is called
 *
 * @return a pointer to the allocated memory or a NULL pointer on error
 */
void *
#if !MEMP_OVERFLOW_CHECK
memp_malloc(memp_t type)
#else
memp_malloc_fn(memp_t type, const char* file, const int line)
#endif
{
  struct memp *memp;
  SYS_ARCH_DECL_PROTECT(old_level);

  SYS_ARCH_PROTECT(old_level);

  memp = memp_tab[type];
  
  if (memp != NULL) {
    memp_tab[type] = memp->next;

    memp = (struct memp*)(void *)((u8_t*)memp + MEMP_SIZE);
  } else {
     ...
  }

  SYS_ARCH_UNPROTECT(old_level);

  return memp;
}
/**
 * Put an element back into its pool.
 *
 * @param type the pool where to put mem
 * @param mem the memp element to free
 */
void
memp_free(memp_t type, 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);
  
  memp->next = memp_tab[type]; 
  memp_tab[type] = memp;

  SYS_ARCH_UNPROTECT(old_level);
}

memp_tab[]在init 函数里初始化,将内存池的每个元素链成链表。做成链表就很简单了,memp_malloc() 的时候直接就是返回memp_tab[]所指向的内存块即可,并把memp_tab指向下一个,相当于从链表中剔除。memp_free()的时候就把它重新链上链表即可。
具体的就去看源代码了,这里不方便贴太多。

后记之Lwip内存使用
从网卡驱动部分的源代码看,所有输入的包使用的PBUF_POOL的内存,用户在分配自己包的内存时,千万不要用PBUF_POOL类型的内存,因为如果PBUF_POOL很有可能会被外来的包全部占用,导致一直分配不到内存。这点从netbuf_alloc函数里可以看到,它使用的是PBUF_RAM类型的内存,就是有此意。还有从tcp_write函数中也可以看到,在flag 有WRITE_COPY 标志时,调用的也是PBUF_RAM类型的内存。
从协议栈输入到应用层,都只是pbuf头的层层剥离与指针的传递,没有任何的数据拷贝过程,这正是LwIP的特点所在,为嵌入式系统而写的轻量级协议栈。只有在tcp_write 发送数据时,可以选择一个WRITE_COPY选项,将用户的数据拷贝到tcp层,之后用户即可释放自己的内存,剩下的事就由tcp负责处理。udp的发送过程没有数据拷贝,如果所发送的pbuf前头没有足够的内存放包头的话,会申请一点PBUF_RAM类型的内存放传输层及以下的帧头,把数据chain到其之后。
另外内存分配函数是可以放心在其它线程内存调用的,但NO ISR!用户若想单独想开辟一个内存池,可以在memp_std里定义即可。

About: Tagore


Leave a Reply

Your email address will not be published. Required fields are marked *