Lwip 内存管理

Lwip 用它自己的动态内存分配方式替代了标准C的malloc(); 它可以使用MEM_POOL的方式分配内存,即开辟几个不同尺寸的缓冲池,给数据分配合适的缓冲区来存放,就类似于使用数组的方式,可能会比较浪费空间,但是可以有效避免内存碎片产生, 这在第二部分讲。两种方式分别在mem.c memp.c里面实现,下面先总结一般的内存堆分配方式。 (一) 内存堆动态分配方式 [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 [/c] 首先是内存对齐,这里用到的MCU是按4字节对齐的。这一语句功能是将addr 调整成最靠近addr的且能被4整除的值,其实就是(addr+3)&(~(u32)(0x03)),+3之后有余数的自然就进位了然后再舍掉余数。其实也可以用 addr = (addr&0x03)?(addr+1):addr; 这样应该也可以,更容易理解一些,但是没有上面的简便。 [c] #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; }; [/c] 这里申请了一个内存堆ram_heap, 大小包括整个MEM_SIZE + 两个STRUCT_MEM的大小 + 4, 假设申请了MEM_SIZE 为8k。 [c] ** 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; … } [/c] 内存初始化。ram 指向了ram_heap开头, 并且在开头初始化了一个struct mem。 ram_end 指向了MEM_SIZE之后的第一个STRUCT_MEM, 这里不知道是不是bug,我觉得应该指向&ram[MEM_SIZE_ALIGNED+SIZEOF_STRUCT_MEM]; 没时间了,以后再慢慢研究。 lfree最开始也指向了开头。 ->next 等这些参数都是相对地址,相对ram的,方便之后用数组的方式访问。 [c] 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)); } } [/c] 内存分配函数。首先对齐size,并且size不能太小或者太大,太小容易产生过多的碎片导致后期内存不足。 然后搜索heap,找到适合的块。lfree指向未分配的内存位置,刚开始它是在开头。 ptr=lfree-ram 即是lfree相对ram的位置,即已经用掉的内存大小。 ptr mem 先指向lfree所指的位置,当这块内存未被使用并且剩下的内存大于size时,…略复杂,待续… (二) 内存池的动态分配 内存池顾名思义就是一个个划分好的池子,不像内存堆一样东西乱堆。 pbuf是协议栈里最常用的数据类型,它有多种类型的存储方式,如下: [code] * - 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(). [/code] 我们从pbuf入手研究一下MEM_POOL内存池的编写方式。 pbuf_alloc(PBUF_POOL)调用的是: p = (struct pbuf *)memp_malloc(MEMP_PBUF_POOL); 我们来看看MEMP_PBUF_POOL是如何声明的: [c] //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) [/c] memp_std.h里定义了各种类型POOL. [c] //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” ]; [/c] memp.c 定义了 几个数组来保存每个pool的元素大小和数量。定义一个memp_memory[]内存块保存所有的pool。 [c] /* 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; [/c] ## 是连接符号。这样memp_t的枚举类型里就有各种的POOL名字,如编译之后就成为: [c] typedef enum { MEMP_PBUF, MEMP_PBUF_POOL, … MEMP_MAX } memp_t; [/c] 总结一下, memp_std.h里包含许多条POOL的声明: LWIP_PBUF_MEMPOOL(PBUF_POOL, PBUF_POOL_SIZE, PBUF_POOL_BUFSIZE, “PBUF_POOL”) 然后在memp.c 里面利用这种套路将memp_std.h 里的声明全包进去: [c] 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” ]; [/c] 就可以生成我们想要的结果。 [c] static u8_t memp_memory[MEM_ALIGNMENT - 1 + ( (num) * (MEMP_SIZE + MEMP_ALIGN_SIZE(size) ) ) + … ]; [/c] 这是一种很聪明的写法。领教了! 回过头来我们就明白MEMP_PBUF_POOL内存到底多大了,Demo设置的是48*512byte。 另外前面有一个是MEMP_PBUF的内存池,它的BUFSIZE是0,仅能用来保存结构体指针,所以是专门给PBUF_REF/ROM类型使用的。 声明好内存池后,我们看看p = (struct pbuf *)memp_malloc(MEMP_PBUF_POOL); 是怎么工作的。 [c] /** 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); } [/c] 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里定义即可。