1、前言
本文基于DPDK-17.05.2分析总结, DPDK通过使用hugetlbfs,减少CPU TLB表的Miss次数,提高性能。

2、Hugetlbfs初始化
DPDK的内存初始化工作,主要是将hugetlbfs的配置的大内存页,根据其映射的物理地址是否连续、属于哪个Socket等,有效的组织起来,为后续管理提供便利。

2.1 eal_hugepage_info_init()
eal_hugepage_info_init()主要是获取配置好的Hugetlbfs的相关信息,并将其保存在struct internal_config数据结构中。
主要工作如下:
1、读取/sys/kernel/mm/hugepages目录下的各个子目录,通过判断目录名称中包含"hugepages-"字符串,获取hugetlbfs的相关子目录,并获取hugetlbfs配置的内存页大小。如:

2、get_hugepage_dir()通过读取/proc/mounts信息,找到hugetlbfs的挂载点。如:  

3、get_num_hugepages()通过读取/sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages,获取配置的hugepages个数。

4、以打开文件的方式,打开挂载点目录,为其FD设置互斥锁,上述所有获取的信息,都保存在internal_config.hugepage_info[MAX_HUGEPAGES_SIZE]中,hugepage_info数据结构如下:

struct hugepage_info {size_t hugepage_sz; /**< size of a huge page */const char *hugedir; /**< dir where hugetlbfs is mounted */uint32_t num_pages[RTE_MAX_NUMA_NODES];/**< number of hugepages of that size on each socket */int lock_descriptor; /**< file descriptor for hugepage dir */
};

具体赋值如下,
  hpi->hugepage_sz = 2M;
  hpi->hugedir = /mnt/huge-dpdk;
  hpi->num_pages[0] = 64; // 由于此时还不知道哪些内存页分处在哪个socket上,故,都先放在socket-0上。
在读取hugetlbfs配置的时候,需要锁住整个目录。当所有hugepage都mmap完成后,会解锁。

5、将internal_config.hugepage_info[MAX_HUGEPAGES_SIZE]按内存页的大小排序。

2.2 rte_eal_config_create()
rte_eal_config_create()主要是初始化rte_config.mem_config。如果是以root用户运行dpdk程序的话,rte_config.mem_config指向/var/run/.rte_config文件mmap的一段sizeof(struct rte_mem_config)大小的内存。
rte_config.mem_config = /var/run/.rte_config文件mmap的首地址;

struct rte_config {uint32_t master_lcore;       /**< Id of the master lcore */... ...struct rte_mem_config *mem_config;
} __attribute__((__packed__));


2.3 rte_eal_hugepage_init()
te_eal_hugepage_init()主要是在/mnt/huge目录下创建hugetlbfs配置的内存页数(在本文中就是64)的rtemap_xx文件,并为每个rtemap_xx文件做mmap映射,保证mmap后的虚拟地址与实际的物理地址是一样的。
具体如下:
1、创建nr_hugepages个struct hugepage_file数组,即有多少个内存页,创建多少个struct hugepage_file数据结构。struct hugepage_file数据结构如下:

struct hugepage_file {void *orig_va;      /**< virtual addr of first mmap() */void *final_va;     /**< virtual addr of 2nd mmap() */uint64_t physaddr;  /**< physical addr */size_t size;        /**< the page size */int socket_id;      /**< NUMA socket ID */int file_id;        /**< the '%d' in HUGEFILE_FMT */int memseg_id;    /**< the memory segment to which page belongs */#ifdef RTE_EAL_SINGLE_FILE_SEGMENTSint repeated;       /**< number of times the page size is repeated */#endifchar filepath[MAX_HUGEPAGE_PATH]; /**< path to backing file on filesystem */
};


2、map_all_hugepages()中有多少个内存页,在挂载点目录下创建多少个rtemap_xx文件,如下所示,并为每一个文件mmap一个hugepage_sz大小的内存区域。其中,
   hugepage_file->orig_va = 记录每个rtemap_xx文件mmap的首地址;
   hugepage_file->file_id = 创建的rtemap_xx的顺序,就是xx的值;
   hugepage_file->filepath = /mnt/huge/rtemap_xx;
   hugepage_file->size = hugepage_sz,也就是2M; 
3、rte_mem_virt2phy()中通过读取/proc/self/pagemap页表文件,得到本进程中虚拟地址与物理地址的映射关系。使用上一步中,每个rtemap_xx文件mmap得到的虚拟地址,除以操作系统内存页的大小4k,得到一个偏移量。根据这个偏移量,在/prox/self/pagemap中,得到物理地址的页框,假设为page,那么,物理页框page乘以操作系统内存页的大小4K,再加上虚拟地址的页偏移,就是物理地址。每个rtemap_xx映射的物理地址保存在对应的hugepage_file->physaddr中。
physaddr = ((page & 0x7fffffffffffffULL) * page_size) + ((unsigned long)virtaddr % page_size);
4、find_numasocket()中通过读取/proc/self/numa_maps,得到每个rtemap_xx文件mmap得到的虚拟地址在哪个Socket上,即,哪个CPU上。其socketid保存在对应的hugepage_file->socket_id中。
5、在hugepage_file数组中,根据物理地址,按从小到大的顺序,将hugepage_file排序。
6、map_all_hugepages()根据按物理地址排序后的结果,判断物理地址是否连续,重新mmap /mnt/huge/retmap_xx文件,使得物理地址等于第二次mmap后的虚拟地址。第二次mmap得到的虚拟地址保存在对应的hugepage_file->final_va中。



7、unmap_all_hugepages_orig()中,munmap释放第一步中各个rtemap_xx文件首次mmap得到的内存地址。
8、calc_num_pages_per_socket()中计算每个socket上包含多少个hugepage,信息保存在internal_config.hugepage_info[0].
num_pages[socket]中。

9、create_shared_memory()为/var/run/.rte_hugepage_info文件mmap一段nr_hugepages * sizeof(struct hugepage_file)大小的内存块,并将第一步中创建的hugepage_file数组中的所有内容,都copy到这一块内存中。
10、rte_config.mem_config->memseg[]数组记录hugepage_file映射后物理地址连续的块数,hugepage_file->memseg_id为该huepage_file的物理地址在哪个rte_config.mem_config->
memseg[]数组中。struct rte_memseg数据结构如下:

struct rte_memseg {phys_addr_t phys_addr;   /**< Start physical address. */union {void *addr;         /**< Start virtual address. */uint64_t addr_64;    /**< Makes sure addr is always 64 bits */};#ifdef RTE_LIBRTE_IVSHMEMphys_addr_t ioremap_addr; /**< Real physical address inside the VM */#endifsize_t len;               /**< Length of the segment. */size_t hugepage_sz;       /**< The pagesize of underlying memory */int32_t socket_id;         /**< NUMA socket ID. */uint32_t nchannel;        /**< Number of channels. */uint32_t nrank;           /**< Number of ranks. */
#ifdef RTE_LIBRTE_XEN_DOM0/**< store segment MFNs */uint64_t mfn[DOM0_NUM_MEMBLOCK];
#endif
} __attribute__((__packed__));

rte_config.mem_config->memseg[j].phys_addr = 各物理地址是连续的内存块的首地址。
rte_config.mem_config->memseg[j].addr = 各个物理地址是连续的内存块对应的虚拟地址的首地址。由于物理地址和虚拟地址是相同的,这个值应该等于phys_addr。
rte_config.mem_config->memseg[j].len = 各个物理地址是连续的内存块的大小。
rte_config.mem_config->memseg[j].socket_id = 内存块在哪个socket上。
rte_config.mem_config->memseg[j].hugepage_sz = hugepage内存页的大小。本文中是2M。

2.4 rte_eal_memdevice_init()
rte_eal_memdevice_init()初始化rte_config.mem_config->nchannel和rte_config.mem_config->
nrank。
  rte_config.mem_config->nchannel = 启动参数中“-n”指定的值,不能为0,不能大于4。
rte_config.mem_config->nrank = 启动参数中“-r”指定的值。不能为0,不能大于16。
2.5 rte_eal_memzone_init()
rte_eal_memzone_init()主要负责初始化rte_config.mem_config->free_memseg[]及rte_config.mem_config->memzone[]。其中,rte_config.mem_config->free_memseg[]记录空闲的rte_config.mem_config->memseg[]。
3、Hugepage部分总结

物理内存可能是分隔不连续的,所有的内存都由一个内存描述符表管理,且每个描述符(rte_memseg)指向的都是一段连续的物理内存。
如下图:
DPDK以两种方式对外提供内存管理方法,一个是rte_mempool,主要用于网卡数据包的收发;一个是rte_malloc,主要为应用程序提供内存使用接口。

4、rte_mbuf_pool初始化
rte_membuf_pool由函数rte_pktmbuf_pool_create()负责创建,从rte_config.mem_config->
free_memseg[]中取出合适大小的内存,放到rte_config.mem_config->memzone[]中。

4.1 rte_membuf_pool的创建

“mbuf_pool”:创建的rte_membuf_pool的名称。
NB_MBUF:rte_mempool包含的rte_mbuf元素的个数。
MBUF_SIZE:每个rte_mbuf元素的大小。
#define RTE_PKTMBUF_HEADROOM 128
#define MBUF_SIZE (2048 + sizeof(struct rte_mbuf) + RTE_PKTMBUF_HEADROOM)
#define NB_MBUF 8192
rte_membuf_pool由函数rte_mempool_create_empty()负责创建。首先创建rte_memzone,再创建rte_mempool,并建立两者之间的关联。

4.1.1创建并初始化rte_memzone

调用rte_memzone_reserve(),在rte_config.mem_config->malloc_heaps[]中查找一个合适的free_memseg(查找规则是free_memseg中剩余内存大于等于需要分配的内存,但是多余的部分是最小的),从该free_memseg中分配指定大小的内存并初始化rte_memzone;
#define RTE_MAX_MEMZONE 2560

memzone分配器的角色就是保存一段物理连续的内存。这些内存区的内存被申请到使用时会有一个唯一名字来标示。
rte_memzone描述符也在配置结构体中。可以使用rte_eal_get_configuration()接口访问这个结构体。通过名字查找一个内存区会返回一个有内存区物理地址的描述符。

4.1.2创建并初始化rte_mempool
a、计算需要为rte_mempool申请的内存空间。包含:sizeof(struct rte_mempool)、struct private_data_size,以及n * rte_mempool_cache。


b、初始化新创建的rte_mempool,和rte_memzone建立关系;
#define RTE_MBUF_DEFAULT_MEMPOOL_OPS “ring_mp_mc”
4.1.3 初始化rte_pktmbuf_pool_private
调用rte_pktmbuf_pool_init()初始化rte_mempool的私有数据结构mbp_priv。

4.1.4 初始化rte_mbuf
调用rte_mempool_populate_default(),以及rte_pktmbuf_init()初始化rte_mempool的每个rte_mbuf元素。
4.1.5 mbuf部分总结
相关数据结构的关联关系如下图:

5、rte_malloc的调用
rte_malloc()为程序运行过程中分配内存,模拟从堆中动态分配内存空间。

void * rte_malloc(const char *type, size_t size, unsigned align)
{return rte_malloc_socket(type, size, align, SOCKET_ID_ANY);
}

rte_malloc()函数调用关系如下图:
rte_malloc()----rte_malloc_socket()----malloc_heap_alloc()----malloc_elem_alloc()

rte_malloc_socket():指定从哪个socket上分配内存空间,默认是指定SOCKET_ID_ANY,即,程序在哪个socket上运行,就从哪个socket上分配内存。如果指定的socket上没有合适的内存空间,就再从其它socket上分配。

malloc_heap_alloc():从rte_config.mem_config->malloc_heaps[]数组中找到指定socket对应的堆(使用struct malloc_heap描述堆),即从这个堆中分配空间。调用find_suitable_element()在堆中查找是否有合适内存可以分配,如果有,调用malloc_elem_alloc()在堆中将需要分配的内存划分出去。


malloc_elem_alloc():查找到合适大小的内存块后,将这一块内存从堆中划分出去。




在malloc库内部有两个数据结构类型使用:
结构体malloc_heap:用于跟踪每个socket上的空闲内存
结构体 malloc_elem:分配的基本元素且库内用于跟踪空闲内存空间

结构体:malloc_heap
malloc_heap结构体用于管理每个socket上的空闲内存空间。实际上,每一个NUMA node上都有一个heap结构体对象,这样就可以实现线程从其所在运行的NUMA node上分配内存。但不能保证所使用的内存是在运行的NUMA node上,那就和在混合或者随机的node上分配内存好不到那里去。

堆结构体中的主要成员和函数描述如下:
**lock:**锁成员是为了实现同步访问堆。堆中的空闲内存是一个链表维护的,为了防止两个线程同时访问这个链表就需要加锁。
**free_head:**空闲内存链表头指针指向malloc_heap的空闲内存链表的第一个成员。

注意:malloc_heap结构体并不监测使用的内存块,所以这些内存块除非被重新释放否则就绝对无法接触到,重新释放就是将指向内存块的指针作为参数传给free()函数。

结构体:malloc_elem
malloc_elem结构体用于各种内存块的通用结构,用于3中不同的方式:
作为一个空闲或者是分配的内存块的头-正常情况
作为一个内存块内的填充头
作为一个内存表段的结束标记
结构体中最重要的成员和如何使用如下描述:
注意:在上面的三个使用情况中没有用到的特定成员,这些成员可以认为在那种情况下没有明确的值。例如, 对于填充头padding header,只有成员state和pad有可用的值。

**heap:**这个指针是堆结构体分配的内存块的引用返回值。它用于释放的普通内存块,将其添加到堆的空闲内存链表

**prev:**这个指针是指向在内存表中当前位置后面(不应该是前面吗?)紧靠着的头结构对象/内存块。当释放一个内存块的时候,这个指针指向的内存块会被检查是否也是空闲的,如果是,就将这两个空闲内存块合并成一个大的内存块。(减少内存碎片)

**next_free:**这个指针用于将没有分配的内存块连接到空闲内存链表上。它只用于正常的内存块,malloc()时就查看一个合适的空闲内存块分配,free()时就是将重新释放的内存块添加到空闲链表上。

**state:**这个成员有三种值:FREE,BUSY,PAD。前两个表明普通内存块的分配状态,后则表明结构体是一个在内存块起始位置填充无意义数据的尾部的虚设结构体。那就是说,数据在内存块中的开始位置不是在数据库的头上,这是由于数据对齐的限制。如此的话,填充头结构就是用于定位块内实际分配的内存的结构体头部。在内存表的尾部,结构体内这个值为BUSY,确保不会再有元素了。在释放的时候会跳过这个搜索其它的内存块来合并成大的空闲内存块。

**pad:**这个代表了当前内存块开始位置填充无用段的长度。在一个正常的内存块头部,将它与头结构体尾部地址相加就是数据段开始地址,就是说这个会作为malloc的返回值给程序。在这段填充区内有虚设的头,头内成员pad有同样的值,从该头结构的地址减去pad值就是实际分配的内存块头结构地址。

**size:**数据块的大小,包括自身的头部分。在内存表尾部的那个虚设结构体中,这个size是0,尽管它从没有被检查过。在一个标准的内存块释放时,这个值会代替next指针来定位靠在一起的下一个内存块,万一后者是FREE状态,那么二者就可以合二为一了。
内存分配

在EAL初始化时,所有的内存表都是组织到malloc堆下,这是会将内存表的尾部设置一个BUSY状态的虚设结构体。当CONFIG_RTE_MALLOC_DEBUG选项打开且在内存表的头部有一个FREE状态元素头,虚设机构体中就可能包含一个哨兵值。FREE元素会被加入到malloc堆的空闲链表中。

当程序调用类似malloc函数时,malloc函数会先查看调用线程的lcore_config结构体,确定该线程所在的NUMA节点。NUMA节点用作malloc_heap结构体数组的下标,且会作为其中一个参数和其它参数请求内存大小、类型、对齐值、边界一起传递给heap_alloc()函数。

heap_malloc()会先扫描heap的空闲链表,试图找到一个匹配请求存储数据大小和对齐方式、边界限制的空闲内存块,
当一个匹配的空闲元素标记时,算出的内存指针会被返回给用户。而cacheline大小的内存会在指针之前用malloc_elem装填。由于对齐和边界限制,在元素的开头和结尾会有空白空间,这回导致一下问题:

检查尾部空间。如果尾部空间足够大,也就是说>128字节,就会分割这个元素。如果不是,那么就忽略这个尾部空间(白白浪费掉的空间)

检查元素头空间。如果空间很小,就是<=128字节,就会用部分空间作为填充头结构,其它的也是浪费掉。然而,如果头空间足够大,那么就将这个空闲元素分割成两个。

从现有的元素的尾部分配内存的好处就是不用调整空闲链表来代替-空闲链表上现有元素只需要调整size变量,且其后的元素也只需将prev指针指向新产生的元素就可以了。

释放内存
要释放一段内存,数据段开始地址地址会传递给free函数。指针值减去malloc_elem结构体剩下就是这个内存块的元素头。如果头中type是PAD,那就将指针减去pad值得到实际的内存块元素头结构。

从这个元素头中,我们就拿到了从堆中分配的内存块指针,且它需要在哪里释放。和prev指针一样,通过size可以计算出紧挨着的后面一个元素的头指针。检查前后元素是否是FREE,如果是就与当前的元素合并。这意味着我们不可能有两个FREE状态的元素靠在一起,它们总是会被合并成一个单独的内存块。

DPDK内存管理总结相关推荐

  1. dpdk内存管理分析

    dpdk内存管理分析 文章目录 dpdk内存管理分析 1.1 简述 1.2 `rte_config_init`分析 1.3 `eal_hugepage_info_init`的分析 1.4 `rte_e ...

  2. DPDK 内存管理---malloc_heap和malloc_elem

    博文是基于dpdk20.5代码阅读所写,如理解有错误或不当之处,烦请指正,不甚感激.也可以私信我一起探讨. 两种数据结构体介绍 Malloc 库内部使用了两种数据结构类型(可以参考dpdk官方文档3. ...

  3. dpdk内存管理——内存初始化

    *说明:本系列博文源代码均来自dpdk17.02* 1.1内存初始化 1.1.1 hugepage技术 hugepage(2M/1G..)相对于普通的page(4K)来说有几个特点: (1) huge ...

  4. dpdk内存管理之rte_eal_hugepage_init()函数分析

    dpdk版本:dpdk-stable-16.11.11 今天我们来看一下rte_eal_hugepage_init() 函数都干了哪些事. 1.计算大页总数 在调用rte_eal_hugepage_i ...

  5. dpdk 内存管理 原理剖析

    dpdk 的内存管理层次结构 物理巨页的管理 虚拟地址空间的管理 heap管理(变长buffer) mempool管理(定长buffer) 物理巨页的管理 dpdk中通过 数组 hugepg_tbl[ ...

  6. DPDK内存管理二:初始化

    DPDK 内存的初始化主要在rte_eal_init()函数中进行: eal_hugepage_info_init() /* 获取系统中hugepage种类以及数量信息到internal_config ...

  7. DPDK内存管理 ----- (四) rte_mbuf

    本文主要介绍 rte_mbuf 与 rte_mempool 数据结构之间的组织关系.以及网卡接收到的数据是如何存储在 rte_mbuf 中的. 一.   rte_mbuf.rte_mempool及网卡 ...

  8. 收藏:DPDK内存基本概念

    点击上方"朱小厮的博客",选择"设为星标" 后台回复"书",获取 来源:DPDK与SPDK开源社区 作者简介:Anatoly Burakov ...

  9. DPDK内存篇(三): 标准大页、NUMA、DMA、IOMMU、IOVA、内存池

    作者简介:Anatoly Burakov,英特尔软件工程师,目前在维护DPDK中的VFIO和内存子系统. 目录 引言 标准大页 将内存固定到NUMA节点 硬件.物理地址和直接内存存取(DMA) IOM ...

最新文章

  1. [原] 利用Radix树作为Key-Value 键值对的数据路由
  2. 如何发送Head请求
  3. 使用Dezender对zend加密后的php文件进行解密
  4. WINCE BSP中source文件中的宏定义
  5. java蓝桥杯 试题-基础练习-十六进制转八进制
  6. OJ1064: 加密字符(C语言)
  7. 编译原理基础---思维导图
  8. 解决Intellij IDEA运行报Command line is too long的问题
  9. SSIS工具的ETL过程,全量ETL和增量ETL过程教程
  10. 关于提高网站性能的几点建议(二)
  11. php 微信小程序签到功能,微信小程序每日签到
  12. 糖尿病视网膜病变研究的基准:分割、分级和可转移性笔记
  13. 手机离线地图——基于OruxMaps离线高清卫星地图制作发
  14. 产品经理修炼之道读后感
  15. 雷军—我十年的程序员生涯
  16. SpringBoot + Vue 前后端分离项目部署到服务器上
  17. Python从入门到实践
  18. 浅析计算机主板故障,浅析计算机主板故障及维修
  19. 博主制作的开源JAVA WEB游戏-《天命.罗生门》
  20. Portraiture中文版最新mac3.5版win5.0版插件介绍

热门文章

  1. 【软件】网梭浏览器v2.4.7 思路
  2. js iframe 打印 打印预览 页眉页脚的设置
  3. leetcode1083. 销售分析 II(SQL必会呀)
  4. uniapp获取视频第一帧展示,及视频的层级问题,亲测有效
  5. python获取B站单个视频的封面
  6. 第三十五天:XSS跨站反射存储DOM盲打劫持
  7. 第1期技术: DQN算法原理及实现过程
  8. Drop tablespace
  9. Day01 每日英语
  10. 用SNMP协议实现系统监控