DPDK rte_malloc

参考文献: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分裂前后对比如下图所示:

 

分裂前

 


 

posted @ 2019-07-17 00:11  坚持,每天进步一点点  阅读(4447)  评论(0编辑  收藏  举报