参考文献:dpdk中的librte_malloc库
《深入浅出DPDK》
一. librte_malloc 库
dpdk中的librte_malloc库提供了能够分配任意大小内存的API。
该库的目标是提供类似malloc的函数从hugepage中分配内存,以及帮助应用程序移植。通常情况下,这种类型的分配不应该在数据平面处理,因为其比基于内存池的分配更慢,并且在分配和释放时会使用锁。
1.1 Cookies
如果在配置文件中打开CONFIG_RTE_MALLOC_DEBUG,
分配的内存会包含覆盖保护区域,以识别缓冲区溢出问题。
1.2 对齐与NUMA Constraints
rte_malloc()函数包含一个align参数,用来要求内存区域对齐到该值的倍数(必须是2的倍数)。
在支持NUMA的系统中,调用rte_malloc()函数时,会在调用该函数的进程所在的socket上分配内存。
同时该库也提供了一组API,使用户可以直接在指定的NUMA socket上分配内存,
或者在另一个core所在的NUMA socket上分配内存。
1.3 用例
应用程序在初始化时使用类似malloc这样的函数时,可以使用该库。
要在运行时分配/释放内存数据,如果应用程序对速度有要求,
请用内存池库代替本库。
如果要使用一块需要知道物理地址的内存块,如硬件设备使用的内存块,
则应该使用memory zone。
1.4 数据结构
在malloc库的内部使用两种数据结构类型:
struct malloc_heap: 用来管理每个socket上的空闲空间
struct malloc_elem: 分配的基本元素,由库内部管理的空闲空间。
1.4.1 struct malloc_heap
该结构体用来管理每个socket上的空闲空间。
在库的内部,每个NUMA node上包含一个 heap结构体,
使我们可以根据线程运行所在的NUMA node,在对应的结点分配内存。
虽然不能保存一定会在指定的结点上分配内存,但比总在某个固定的的结点或随机结点分配要好。
heap的关键成员变量和成员函数描述如下:
mz_count: 保存本结点已经为heap内存分配的memory zone的数量。该值的唯一用途就是与numa_socket值组合为每个memory zone生成一个唯一的名字。
lock: 该变量用来做对heap访问的同步。考虑到heap中的空闲空间是由一个list管理的,所以我们需要一个锁来防止两个线程同时访问该list。
free_head: 该变量该malloc heap的free nodes list中的第一个元素。
注意: malloc_heap结构体不会管理已经分配的memzones,这么做是毫无意义的,因为它们不会被释放。
也不会管理使用中的内存块,因为除非它们被释放,否则是不会再次接触到这些内存块的。
在释放时,指向这些内存块的指针会作为free()函数的参数。
1.4.1.2 struct malloc_elem结构体
malloc_elem结构体被用作memzone中各种内存块的头部结构。
有三种不同的用法:
1、分配或释放内存块时的头部 - 普通情况
2、在内存块中作为padding头部
3、作为memzone结尾处的标记
下文描述了结构中最重要的部分以及用法。
注意:如果某种用法不属于上面描述的三种中的任何一种,则认为对应的变量是未定义的。
例如,只有当"state"和"pad"两个变量的值是有效值是,才认为其是一个padding header。
head:该指针是已经分配的内存块中指向heap结构的反向引用,即指向对应的heap。
普通内存块在释放时会使用该指针,将当前释放的内存块添加到heap的free list中
prev:该指针指向memzone中当前内存块紧前面的内存块的header element/block。
当释放一个内存块时,该指针用来引用前一个内存块,看其是否也需要释放。
如果需要,则两块内存组合成一块更大的内存块。
next_free:该指针用来将未分配的内存块链接到一起。
同样,该变量只在普通内存块中使用,在malloc()函数中找到一块符合需求的内存块来分配,
并且在调用free()函数将新释放的内存添加到free-list中。
state:该变量可以是以下三个值之一:“Free”, “Busy”或“Pad”。
前两个用业表示普通内存块的分配状态,
第三个用来表示在start-of-block padding的结尾处的元素结构体是一个dummy结构体。
(例如,由于强制对齐,内存块中数据的开始处不在内存块中。???)
在这种情况下,pad header用来定位实际分配的元素header。
对于end-of-memzone结构体,该值总是“busy”,
以确保在释放时没有元素为了整合成一个更大的内存块,而在memzone的结尾外面查找其它内存块。
pad:该变量保存内存块开始处的padding区域的长度。
如果是普通内存块header,该值会被加到header的结尾处的地址,以给出数据区域的正确地址。
例如,在调用malloc函数时传回的值。
在padding中的dummy header的内部,该值也会被保存,
and is subtracted from the address of the dummy header to yield the address of the actual block header.
size:表示数据内存块的大小,包含header自身。对于end-of-memzone结构,该值为0,虽然从不会检查该值。
对于被释放的普通内存块,该值用来代替“next”指针,用来计算下一个内存块所在的地址。
(因此如果下一个内存块也是free的,两个内存块可以整合成一个)。
1.4.2 内存分配
应用程序调用类似malloc的函数时,malloc函数首先会根据调用线程索引lcore_config结构,
以及根据该线程确定其所在的NUMA结点。
即用来索引malloc_head结构数组,之后以该数组为参数调用heap_alloc()函数,
同时作为参数的还有要分配的大小,类型和对齐。
heap_alloc()函数会扫描heap的free_list,并尝试找到一个合适大小的内存块来存储数据,同时强制对齐。
如果没有找到合适大小的内存块,例如,第一次在某结点上调用malloc函数时free-list是空的,
则会创建一个新的memzone并配置为heap元素,其会将一个dummy结构放置到memzone的结尾处,
作为一个标记,防止访问超出这块内存之外(由于该标记被置为“BUSY”,malloc库永远无法将这块内存分配出去)。
同时在memzone的开始处放置一个合适的element header。这个header标记了memzone中的所有空间,
bar the sentinel value at the end,end, as a single free heap element, and it is then added to the free_list for the heap.
新的memzone配置好之后,会重新对heap的free-list进行描述,这次描述会找到新添加的合适大小的元素,
将其作为memzone中保留内存的大小,至少是调用函数中指定的大小的数据内存块加上对齐,
至少是Intel DPDK运行时配置中指定的最小大小。
找到一个合适大小的空闲元素之后,会计算返回到用户的指针,包含提供给用户的空闲内存块结尾处的空间。
紧跟着这块内存的cache-line被填充一个struct malloc_elem头:
如果内存块中余下的空间比较小,如<=128字节,就会使用一个pad header,余下的空间就浪费了。
不过,如果余下的空间大于128字节,则这块空闲内存块就被分成两份,
一个新的,合适的malloc_elem头被放到返回的数据空间之前。
从已经存在的元素的结尾分配内存的好处是,在这种情况下,不需要调整free list——
free list中已经存在的元素已经调整过尺寸指针了,后面element的“prev”指针已经重新指向这个新创建的element了。
1.4.3 释放内存
要释放内存,需要将指向数据区域起始地址的指针传递给free函数。
函数会从指针中减去malloc_elem结构的大小以获取内存块的element header。
如果header的类型是“PAD”,则再从指针中减去pad的长度。
从该element指针中,可以获取到指向堆的来源和需要释放到哪里的指针,
以及指向前一个元素的,并且通过size变量,可以计算下一个元素的指针。
之后也会检查后面的和前面的元素,看其是否也需要被释放。
这意味着永远不会发生两个空闲内存块相邻的情况,这样的内存块总是会被整合成一个更大的内存块。
二. 源码分析
DPDK以两种方式对外提供内存管理方法,一个是rte_mempool,主要用于网卡数据包的收发;一个是rte_malloc,主要为应用程序提供内存使用接口。这里我们主要讲一下rte_malloc函数。
rte_malloc实现的大体流程如下图所示。
下面我们逐个函数分析。
1 /* 2 * Allocate memory on default heap. 3 */ 4 void * 5 rte_malloc(const char *type, size_t size, unsigned align) 6 { 7 return rte_malloc_socket(type, size, align, SOCKET_ID_ANY); 8 }
这个函数没什么可说的,直接调用rte_malloc_socket,但注意传入的socketid参数为SOCKET_ID_ANY。
rte_malloc_socket
从这个函数的入口检查可以看出,如果传入的分配内存大小size为0或对其align不是2次方的倍数就返回NULL。
1 void * 2 rte_malloc_socket(const char *type, size_t size, unsigned align, int socket_arg) 3 { 4 struct rte_mem_config *mcfg = rte_eal_get_configuration()->mem_config; 5 int socket, i; 6 void *ret; 7 8 /* return NULL if size is 0 or alignment is not power-of-2 */ 9 if (size == 0 || (align && !rte_is_power_of_2(align))) 10 return NULL; 11 12 if (!rte_eal_has_hugepages()) 13 socket_arg = SOCKET_ID_ANY; 14 /*如果传入的socket参数为SOCKET_ID_ANY ,则会先尝试在当前socket上分配内存*/ 15 if (socket_arg == SOCKET_ID_ANY) 16 socket = malloc_get_numa_socket(); /*获取当前socket_id*/ 17 else 18 socket = socket_arg; 19 20 /* Check socket parameter */ 21 if (socket >= RTE_MAX_NUMA_NODES) 22 return NULL; 23 /*尝试在当前socket上分配内存,如果分配成功则返回*/ 24 ret = malloc_heap_alloc(&mcfg->malloc_heaps[socket], type, 25 size, 0, align == 0 ? 1 : align, 0); 26 if (ret != NULL || socket_arg != SOCKET_ID_ANY) 27 return ret; 28 /*尝试在其他socket上分配内存,直到分配成功或者所有socket都尝试失败*/ 29 /* try other heaps */ 30 for (i = 0; i < RTE_MAX_NUMA_NODES; i++) { 31 /* we already tried this one */ 32 if (i == socket) 33 continue; 34 35 ret = malloc_heap_alloc(&mcfg->malloc_heaps[i], type, 36 size, 0, align == 0 ? 1 : align, 0); 37 if (ret != NULL) 38 return ret; 39 } 40 41 return NULL; 42 }
malloc_heap_alloc
这个函数用来模拟从heap中(也就是struct malloc_heap)分配内存,其调用逻辑图如下:
1 void * 2 malloc_heap_alloc(struct malloc_heap *heap, 3 const char *type __attribute__((unused)), size_t size, unsigned flags, 4 size_t align, size_t bound) 5 { 6 struct malloc_elem *elem; 7 /*将size调整为cache line对齐*/ 8 size = RTE_CACHE_LINE_ROUNDUP(size); 9 align = RTE_CACHE_LINE_ROUNDUP(align); 10 11 rte_spinlock_lock(&heap->lock); 12 /*找到合适的malloc_elem结构*/ 13 elem = find_suitable_element(heap, size, flags, align, bound); 14 if (elem != NULL) { 15 elem = malloc_elem_alloc(elem, size, align, bound); 16 /* increase heap's count of allocated elements */ 17 heap->alloc_count++; /*计数加一*/ 18 } 19 rte_spinlock_unlock(&heap->lock); 20 21 return elem == NULL ? NULL : (void *)(&elem[1]); 22 }
注意最后的返回值,返回的是elem[1]的地址,而不是elem的地址。elem[1]是什么呢?其实就是elem+1。说的直观点,rte_malloc其实就是分配了一个内存块,也可以说是分配了一个malloc_elem,这个malloc_elem作为这个内存块的一部分(存放在开头),相当于这个内存块的描述符,真正可以使用的内存是malloc_elem之后的内存区域。
如下图所示。
在补一张内存初始化中讲到的数据结构关系图。
下面看下find_suitable_element函数是如何找到合适的malloc_elem的。
l find_suitable_element
1 static struct malloc_elem * 2 find_suitable_element(struct malloc_heap *heap, size_t size, 3 unsigned flags, size_t align, size_t bound) 4 { 5 size_t idx; 6 struct malloc_elem *elem, *alt_elem = NULL; 7 /*根据申请内存的大小,在struct malloc_heap->free_head数组中找到合适的idx*/ 8 for (idx = malloc_elem_free_list_index(size); 9 idx < RTE_HEAP_NUM_FREELISTS; idx++) { 10 /*在heap->free_head[idx]链表中找到合适的malloc_elem*/ 11 for (elem = LIST_FIRST(&heap->free_head[idx]); 12 !!elem; elem = LIST_NEXT(elem, free_list)) { 13 if (malloc_elem_can_hold(elem, size, align, bound)) { 14 if (check_hugepage_sz(flags, elem->ms->hugepage_sz)) 15 return elem; 16 if (alt_elem == NULL) 17 alt_elem = elem; 18 } 19 } 20 } 21 22 if ((alt_elem != NULL) && (flags & RTE_MEMZONE_SIZE_HINT_ONLY)) 23 return alt_elem; 24 25 return NULL; 26 }
我们知道malloc_elem的组织结构是个二维的链表,如下图所示。所以第一步要找到合适的一维链表。也就是在struct malloc_heap->free_head数组中找到合适的idx。
我们在前面介绍过,struct malloc_heap->free_head数组的下标和数组中malloc_elem的大小有类似如下对应关系。所以malloc_elem_free_list_index就是返回能够满足申请大小size的最小的idx。
heap->free_head[0] - (0 , 2^8]
heap->free_head[1] - (2^8 , 2^10]
heap->free_head[2] - (2^10 ,2^12]
heap->free_head[3] - (2^12, 2^14]
heap->free_head[4] - (2^14, MAX_SIZE]
之后尝试heap->free_head[idx]上的malloc_elem分配内存,如果分配失败,再尝试更大一点的(idx++)。
下面malloc_elem_can_hold负责在heap->free_head[idx]找到一个合适的malloc_elem。而其内部只是调用了elem_start_pt。
l elem_start_pt
1 static void * 2 elem_start_pt(struct malloc_elem *elem, size_t size, unsigned align, 3 size_t bound) 4 { 5 const size_t bmask = ~(bound - 1); 6 /*在debug模式下MALLOC_ELEM_TRAILER_LEN为cacheline大小,正常为0*/ 7 uintptr_t end_pt = (uintptr_t)elem + 8 elem->size - MALLOC_ELEM_TRAILER_LEN; 9 uintptr_t new_data_start = RTE_ALIGN_FLOOR((end_pt - size), align); 10 uintptr_t new_elem_start; 11 12 /* check boundary */ 13 if ((new_data_start & bmask) != ((end_pt - 1) & bmask)) { 14 end_pt = RTE_ALIGN_FLOOR(end_pt, bound); 15 new_data_start = RTE_ALIGN_FLOOR((end_pt - size), align); 16 if (((end_pt - 1) & bmask) != (new_data_start & bmask)) 17 return NULL; 18 } 19 20 new_elem_start = new_data_start - MALLOC_ELEM_HEADER_LEN; 21 22 /* if the new start point is before the exist start, it won't fit */ 23 return (new_elem_start < (uintptr_t)elem) ? NULL : (void *)new_elem_start; 24 }
代码中的几个指针如下如所示,其本质就是在当前malloc_elem中尝试按照size分配一个新的malloc_elem,看下其起始地址是否越界。如果不越界就将当前malloc_elem返回(不是新的malloc_elem,这时还没有真的分配新malloc_elem)。
找到合适的malloc_elem后,就调用malloc_elem_alloc从此malloc_elem分配新的满足size大小的malloc_elem。
l malloc_elem_alloc
1 struct malloc_elem * 2 malloc_elem_alloc(struct malloc_elem *elem, size_t size, unsigned align, 3 size_t bound) 4 { 5 struct malloc_elem *new_elem = elem_start_pt(elem, size, align, bound); 6 const size_t old_elem_size = (uintptr_t)new_elem - (uintptr_t)elem; 7 /*trailer_size就是align-MALLOC_ELEM_TRAILER_LEN的大小,而MALLOC_ELEM_TRAILER_LEN在debug下为cacheline,否则为0*/ 8 const size_t trailer_size = elem->size - old_elem_size - size - 9 MALLOC_ELEM_OVERHEAD; 10 /*将老的elem从链表中删除*/ 11 elem_free_list_remove(elem); 12 13 if (trailer_size > MALLOC_ELEM_OVERHEAD + MIN_DATA_SIZE) { 14 /* split it, too much free space after elem */ 15 struct malloc_elem *new_free_elem = 16 RTE_PTR_ADD(new_elem, size + MALLOC_ELEM_OVERHEAD); 17 18 split_elem(elem, new_free_elem); 19 malloc_elem_free_list_insert(new_free_elem); 20 } 21 22 /*如果old_elem_size太小,就将老的elem状态设置为ELEM_BUSY*/ 23 if (old_elem_size < MALLOC_ELEM_OVERHEAD + MIN_DATA_SIZE) { 24 /* don't split it, pad the element instead */ 25 elem->state = ELEM_BUSY; 26 elem->pad = old_elem_size; 27 28 /* put a dummy header in padding, to point to real element header */ 29 if (elem->pad > 0){ /* pad will be at least 64-bytes, as everything 30 * is cache-line aligned */ 31 new_elem->pad = elem->pad; 32 new_elem->state = ELEM_PAD; 33 new_elem->size = elem->size - elem->pad;/*elem->size -old_elem_size*/ 34 set_header(new_elem); 35 } 36 37 return new_elem; 38 } 39 40 /* we are going to split the element in two. The original element 41 * remains free, and the new element is the one allocated. 42 * Re-insert original element, in case its new size makes it 43 * belong on a different list. 44 */ 45 /*如果old_elem_size足够大则将原有的elem分隔成两个elem,分别设置elem,new_elem的size*/ 46 split_elem(elem, new_elem); 47 new_elem->state = ELEM_BUSY;/*设置new_elem的状态*/ 48 malloc_elem_free_list_insert(elem);/*根据原有的elem调整后的size再找到合适的idx,将其插入heap->free_head[idx]*/ 49 50 return new_elem; 51 }
elem分裂前后对比如下图所示: