从原理到实践:掌握DPDK内存池技术(上)

简介: 从原理到实践:掌握DPDK内存池技术

前言:本文整理下之前的学习笔记,基于DPDK17.11版本源码分析。主要分析一下内存管理部分代码。

一、概述

内存管理是数据面开发套件(DPDK)的一个核心部分,以此为基础,DPDK的其他部分和用户应用得以发挥其最佳性能。本系列文章将详细介绍DPDK提供的各种内存管理的功能。

但在此之前,有必要先谈一谈为何DPDK中内存管理要以现有的方式运作,它背后又有怎样的原理,再进一步探讨DPDK具体能够提供哪些与内存相关的功能。本文将先介绍DPDK内存的基本原理,并解释它们是如何帮助DPDK实现高性能的。

请注意,虽然DPDK支持FreeBSD,而且也会有正在运行的Windows端口,但目前大多数与内存相关的功能仅适用于Linux。

先看一下下面的图片,其中左边部分为DPDK内存层级结构,下面三层在rte_eal_init初始化时完成,上面三层由用户调用API生成。右边为每层内存结构提供的API,供上层或者APP使用。

下图为内存管理相关的数据结构,其中rte_config->mem_config指向共享内存文件 /var/run/.rte_config,对应的结构体为struct rte_mem_config,其中mem_cfg_addr保存了映射文件/var/run/.rte_config后的虚拟地址,从进程使用此值进行映射,保证主从进程可以使用相同的虚拟地址访问结构体rte_mem_config。

此共享内存struct rte_mem_config还保存了如下几个重要的结构体:

a. memseg: 将同时满足下面四个条件的大页放在一个memseg中

1. 同socket
2. hugepage 大小相同
3. 物理地址连续
4. 虚拟地址连续

b. malloc_heap: 将相同socket的memseg挂在同一个malloc_heap上,对外提供API的最底层实现

c. memzone: 用来申请整块内存

d. tailq_head: 共享队列实现,主从进程可以同时访问。

内存初始化流程如下,后面会详细看每个函数的实现。

rte_eal_init
    //internal_config为本进程全局变量,用来保存参数等信息
    eal_reset_internal_config(&internal_config);
    //解析参数,保存到 internal_config
    eal_parse_args(argc, argv);
    //收集系统上可用的大页内存,保存到 internal_config->hugepage_info[3]
    eal_hugepage_info_init();
    //初始化全局变量rte_config,并将 rte_config->mem_config 进行映射
    rte_config_init();
    //大页内存映射,并保存到 rte_config->mem_config->memseg[]
    rte_eal_memory_init();
    //将 memseg 插入 rte_config->mem_config->malloc_heaps[]
    rte_eal_memzone_init();
    //主进程初始化完成
    rte_eal_mcfg_complete();
        /* ALL shared mem_config related INIT DONE */
        //主进程完成了所有共享内存的初始化,设置RTE_MAGIC。
        //从进程在等待magic变成RTE_MAGIC后才能继续下去。
        if (rte_config.process_type == RTE_PROC_PRIMARY)
            rte_config.mem_config->magic = RTE_MAGIC;

二、标准大页

现代CPU架构中,内存管理并不以单个字节进行,而是以页为单位,即虚拟和物理连续的内存块。这些内存块通常(但不是必须) 存储在RAM中。在英特尔®64和IA-32架构上,标准系统的页面大小为4KB。

基于安全性和通用性的考虑,软件的应用程序访问的内存位置使用的是操作系统分配的虚拟地址。运行代码时,该虚拟地址需要被转换为硬件使用的物理地址。这种转换是操作系统通过页表转换来完成的,页表在分页粒度级别上(即4KB一个粒度)将虚拟地址映射到物理地址。为了提高性能,最近一次使用的若干页面地址被保存在一个称为转换检测缓冲区(TLB)的高速缓存中。每一分页都占有TLB的一个条目。如果用户的代码访问(或最近访问过)16 KB的内存,即4页,这些页面很有可能会在TLB缓存中。

如果其中一个页面不在TLB缓存中,尝试访问该页面中包含的地址将导致TLB查询失败;也就是说,操作系统写入TLB的页地址必须是在它的全局页表中进行查询操作获取的。因此,TLB查询失败的代价也相对较高(某些情况下代价会非常高),所以最好将当前活动的所有页面都置于TLB中以尽可能减少TLB查询失败。

然而,TLB的大小有限,而且实际上非常小,和DPDK通常处理的数据量(有时高达几十GB)比起来,在任一给定的时刻,4KB 标准页面大小的TLB所覆盖的内存量(几MB)微不足道。这意味着,如果DPDK采用常规内存,使用DPDK的应用会因为TLB频繁的查询失败在性能上大打折扣。

还不熟悉的朋友,这里可以先领取一份dpdk新手学习资料包(入坑不亏):

为解决这个问题,DPDK依赖于标准大页。从名字中很容易猜到,标准大页类似于普通的页面,只是会更大。有多大呢?在英特尔®64和1A-32架构上,目前可用的两种大页大小为2MB和1GB。也就是说,单个页面可以覆盖2 MB或1 GB大小的整个物理和虚拟连续的存储区域。

TLB内存覆盖量比较

这两种页面大小DPDK都可以支持。有了这样的页面大小,就可以更容易覆盖大内存区域,也同时避免(同样多的)TLB查询失败。反过来,在处理大内存区域时,更少的TLB查询失败也会使性能得到提升,DPDK的用例通常如此。

2.1eal_hugepage_info_init 收集可用大页内存

遍历/sys/kernel/mm/hugepages下面的目录收集可用的大页。

/*
 * when we initialize the hugepage info, everything goes
 * to socket 0 by default. it will later get sorted by memory
 * initialization procedure.
 */
int
eal_hugepage_info_init(void)
{
    const char dirent_start_text[] = "hugepages-";
    const size_t dirent_start_len = sizeof(dirent_start_text) - 1;
    unsigned i, num_sizes = 0;
    DIR *dir;
    struct dirent *dirent;
    //打开目录 /sys/kernel/mm/hugepages
    dir = opendir(sys_dir_path);
    for (dirent = readdir(dir); dirent != NULL; dirent = readdir(dir)) {
        struct hugepage_info *hpi;
        if (strncmp(dirent->d_name, dirent_start_text,
                dirent_start_len) != 0)
            continue;
        if (num_sizes >= MAX_HUGEPAGE_SIZES)
            break;
        hpi = &internal_config.hugepage_info[num_sizes];
        hpi->hugepage_sz =
            rte_str_to_size(&dirent->d_name[dirent_start_len]);
        //打开文件 /proc/mounts 遍历所有挂载点,找到 hugetlbfs 
        //相关的挂载点,再找到和hpi->hugepage_sz相等的挂载
        //点,返回其挂载目录。
        hpi->hugedir = get_hugepage_dir(hpi->hugepage_sz);
        //如果对应hugepage_sz的挂载点,则跳过此种类型大页
        /* first, check if we have a mountpoint */
        if (hpi->hugedir == NULL) {
            uint32_t num_pages;
            num_pages = get_num_hugepages(dirent->d_name);
            if (num_pages > 0)
                RTE_LOG(NOTICE, EAL,
                    "%" PRIu32 " hugepages of size "
                    "%" PRIu64 " reserved, but no mounted "
                    "hugetlbfs found for that size\n",
                    num_pages, hpi->hugepage_sz);
            continue;
        }
        /* try to obtain a writelock */
        hpi->lock_descriptor = open(hpi->hugedir, O_RDONLY);
        /* if blocking lock failed */
        flock(hpi->lock_descriptor, LOCK_EX);
        //删除挂载点目录下以"map_"开头的文件
        /* clear out the hugepages dir from unused pages */
        clear_hugedir(hpi->hugedir);
        //获取空闲的大页数量,free_hugepages减去
        //resv_hugepages。此时还不知道大页位于哪个socket,
        //所以先将大页数量统一放到socket0上
        /* for now, put all pages into socket 0,
         * later they will be sorted */
        hpi->num_pages[0] = get_num_hugepages(dirent->d_name);
        //大页种类加一
        num_sizes++;
    }
    closedir(dir);
    //保存大页种类: 2M, 1G等
    internal_config.num_hugepage_sizes = num_sizes;
    //按照大页大小排序 hugepage_info,从大到小
    /* sort the page directory entries by size, largest to smallest */
    qsort(&internal_config.hugepage_info[0], num_sizes,
          sizeof(internal_config.hugepage_info[0]), compare_hpi);
    //如果有可用的大页,返回0,否则返回-1
    /* now we have all info, check we have at least one valid size */
    for (i = 0; i < num_sizes; i++)
        if (internal_config.hugepage_info[i].hugedir != NULL &&
            internal_config.hugepage_info[i].num_pages[0] > 0)
            return 0;
    /* no valid hugepage mounts available, return error */
    return -1;
}

rte_config_init

映射文件 /var/run/.rte_config,用来保存结构体 rte_config->mem_config 的内容。主进程映射此文件,将映射后的虚拟地址保存到 rte_config->mem_config->mem_cfg_addr,从进程需要映射两次此文件,第一次是为了获取主进程使用的虚拟地址mem_cfg_addr,然后再使用此虚拟地址进行映射,这样保存主从进程可以使用相同的虚拟地方访问 rte_config->mem_config指向的共享内存。

/* Sets up rte_config structure with the pointer to shared memory config.*/
static void
rte_config_init(void)
{
    rte_config.process_type = internal_config.process_type;
    switch (rte_config.process_type){
    case RTE_PROC_PRIMARY:
        //主进程初始化
        rte_eal_config_create();
        break;
    case RTE_PROC_SECONDARY:
        //从进程attach
        rte_eal_config_attach();
        //等待主进程初始化完成
        rte_eal_mcfg_wait_complete(rte_config.mem_config);
        //再次attach
        rte_eal_config_reattach();
        break;
    case RTE_PROC_AUTO:
    case RTE_PROC_INVALID:
        rte_panic("Invalid process type\n");
    }
}

主进程映射文件 /var/run/.rte_config

/* create memory configuration in shared/mmap memory. Take out
 * a write lock on the memsegs, so we can auto-detect primary/secondary.
 * This means we never close the file while running (auto-close on exit).
 * We also don't lock the whole file, so that in future we can use read-locks
 * on other parts, e.g. memzones, to detect if there are running secondary
 * processes. */
static void
rte_eal_config_create(void)
{
    void *rte_mem_cfg_addr;
    int retval;
    //获取config所在路径 /var/run/.rte_config
    const char *pathname = eal_runtime_config_path();
    if (internal_config.no_shconf)
        return;
    /* map the config before hugepage address so that we don't waste a page */
    if (internal_config.base_virtaddr != 0)
        rte_mem_cfg_addr = (void *)
            RTE_ALIGN_FLOOR(internal_config.base_virtaddr -
            sizeof(struct rte_mem_config), sysconf(_SC_PAGE_SIZE));
    else
        rte_mem_cfg_addr = NULL;
    //打开config文件路径 /var/run/.rte_config
    if (mem_cfg_fd < 0){
        mem_cfg_fd = open(pathname, O_RDWR | O_CREAT, 0660);
        if (mem_cfg_fd < 0)
            rte_panic("Cannot open '%s' for rte_mem_config\n", pathname);
    }
    //将文件大小修改为 mem_config 的长度
    retval = ftruncate(mem_cfg_fd, sizeof(*rte_config.mem_config));
    if (retval < 0){
        close(mem_cfg_fd);
        rte_panic("Cannot resize '%s' for rte_mem_config\n", pathname);
    }
    //给文件mem_cfg_fd加上锁,可以调用eal_proc_type_detect检测是主进程还是从进程。
    //只有主进程能lock成功。
    static struct flock wr_lock = {
            .l_type = F_WRLCK,
            .l_whence = SEEK_SET,
            .l_start = offsetof(struct rte_mem_config, memseg),
            .l_len = sizeof(early_mem_config.memseg),
    };
    fcntl(mem_cfg_fd, F_SETLK, &wr_lock);
    //映射config文件
    rte_mem_cfg_addr = mmap(rte_mem_cfg_addr, sizeof(*rte_config.mem_config),
                PROT_READ | PROT_WRITE, MAP_SHARED, mem_cfg_fd, 0);
    //将early_mem_config内容保存到共享内存 rte_mem_cfg_addr
    memcpy(rte_mem_cfg_addr, &early_mem_config, sizeof(early_mem_config));
    //将映射后的虚拟地址保存到 rte_config.mem_config
    rte_config.mem_config = rte_mem_cfg_addr;
    //最后将映射后的虚拟地址 rte_mem_cfg_addr 保存到共享内存 mem_config->mem_cfg_addr,
    //以便从进程获取后映射相同的虚拟地址。
    /* store address of the config in the config itself so that secondary
     * processes could later map the config into this exact location */
    rte_config.mem_config->mem_cfg_addr = (uintptr_t) rte_mem_cfg_addr;
}

因为只有主进程能lock,所以可以使用函数eal_proc_type_detect检查当前进程是主进程还是从进程。

/* Detect if we are a primary or a secondary process */
enum rte_proc_type_t
eal_proc_type_detect(void)
{
    enum rte_proc_type_t ptype = RTE_PROC_PRIMARY;
    const char *pathname = eal_runtime_config_path();
    /* if we can open the file but not get a write-lock we are a secondary
     * process. NOTE: if we get a file handle back, we keep that open
     * and don't close it to prevent a race condition between multiple opens */
    if (((mem_cfg_fd = open(pathname, O_RDWR)) >= 0) &&
            (fcntl(mem_cfg_fd, F_SETLK, &wr_lock) < 0))
        ptype = RTE_PROC_SECONDARY;
    RTE_LOG(INFO, EAL, "Auto-detected process type: %s\n",
            ptype == RTE_PROC_PRIMARY ? "PRIMARY" : "SECONDARY");
    return ptype;
}

从进程第一此映射/var/run/.rte_config,可能在主进程之前就映射了,映射后主从进程都可以操作这块内存,但主要目的还是为了获取主进程的虚拟地址 mem_cfg_addr。

/* attach to an existing shared memory config */
static void
rte_eal_config_attach(void)
{
    struct rte_mem_config *mem_config;
    //获取config所在路径 /var/run/.rte_config
    const char *pathname = eal_runtime_config_path();
    if (internal_config.no_shconf)
        return;
    //打开文件 /var/run/.rte_config
    if (mem_cfg_fd < 0){
        mem_cfg_fd = open(pathname, O_RDWR);
        if (mem_cfg_fd < 0)
            rte_panic("Cannot open '%s' for rte_mem_config\n", pathname);
    }
    //映射config文件,只读模式。而且第一个参数为NULL,即不知道虚拟地址
    /* map it as read-only first */
    mem_config = (struct rte_mem_config *) mmap(NULL, sizeof(*mem_config),
            PROT_READ, MAP_SHARED, mem_cfg_fd, 0);
    if (mem_config == MAP_FAILED)
        rte_panic("Cannot mmap memory for rte_config! error %i (%s)\n",
              errno, strerror(errno));
    rte_config.mem_config = mem_config;
}

等待主进程初始化完成。主进程初始化完成后会调用rte_eal_mcfg_complete将mcfg->magic设置为 RTE_MAGIC。

inline static void
rte_eal_mcfg_wait_complete(struct rte_mem_config* mcfg)
{
    /* wait until shared mem_config finish initialising */
    while(mcfg->magic != RTE_MAGIC)
        rte_pause();
}

主进程初始化成功后,从进程就可以使用主进程映射的虚拟地址 mem_cfg_addr进行第二次映射

/* reattach the shared config at exact memory location primary process has it */
static void
rte_eal_config_reattach(void)
{
    struct rte_mem_config *mem_config;
    void *rte_mem_cfg_addr;
    if (internal_config.no_shconf)
        return;
    //从共享配置文件获取主进程映射的虚拟地址 mem_cfg_addr
    /* save the address primary process has mapped shared config to */
    rte_mem_cfg_addr = (void *) (uintptr_t) rte_config.mem_config->mem_cfg_addr;
    //去除从进程之前的对mem_config的映射
    /* unmap original config */
    munmap(rte_config.mem_config, sizeof(struct rte_mem_config));
    //指定主进程映射的虚拟地址 mem_cfg_addr重新映射,获取和主进程一样的虚拟地址
    /* remap the config at proper address */
    mem_config = (struct rte_mem_config *) mmap(rte_mem_cfg_addr,
            sizeof(*mem_config), PROT_READ | PROT_WRITE, MAP_SHARED,
            mem_cfg_fd, 0);
    if (mem_config == MAP_FAILED || mem_config != rte_mem_cfg_addr) {
        if (mem_config != MAP_FAILED)
            /* errno is stale, don't use */
            rte_panic("Cannot mmap memory for rte_config at [%p], got [%p]"
                  " - please use '--base-virtaddr' option\n",
                  rte_mem_cfg_addr, mem_config);
        else
            rte_panic("Cannot mmap memory for rte_config! error %i (%s)\n",
                  errno, strerror(errno));
    }
    close(mem_cfg_fd);
    //将映射后的mem_config保存到从进程本地变量 rte_config.mem_config
    rte_config.mem_config = mem_config;
}

这样主从进程都能使用相同的虚拟地址访问共享配置 rte_config.mem_config。

相关文章
|
29天前
|
存储 缓存 安全
Java内存模型深度解析:从理论到实践####
【10月更文挑战第21天】 本文深入探讨了Java内存模型(JMM)的核心概念与底层机制,通过剖析其设计原理、内存可见性问题及其解决方案,结合具体代码示例,帮助读者构建对JMM的全面理解。不同于传统的摘要概述,我们将直接以故事化手法引入,让读者在轻松的情境中领略JMM的精髓。 ####
35 6
|
1月前
|
算法 JavaScript 前端开发
新生代和老生代内存划分的原理是什么?
【10月更文挑战第29天】新生代和老生代内存划分是JavaScript引擎为了更高效地管理内存、提高垃圾回收效率而采用的一种重要策略,它充分考虑了不同类型对象的生命周期和内存使用特点,通过不同的垃圾回收算法和晋升机制,实现了对内存的有效管理和优化。
|
2月前
|
C++
【C++】深入解析C/C++内存管理:new与delete的使用及原理(二)
【C++】深入解析C/C++内存管理:new与delete的使用及原理
|
2月前
|
编译器 C++ 开发者
【C++】深入解析C/C++内存管理:new与delete的使用及原理(三)
【C++】深入解析C/C++内存管理:new与delete的使用及原理
|
2月前
|
存储 C语言 C++
【C++】深入解析C/C++内存管理:new与delete的使用及原理(一)
【C++】深入解析C/C++内存管理:new与delete的使用及原理
|
3月前
|
KVM 虚拟化
KVM的热添加技术之内存
文章介绍了KVM虚拟化技术中如何通过命令行调整虚拟机内存配置,包括调小和调大内存的步骤,以及一些相关的注意事项。
97 4
KVM的热添加技术之内存
|
10天前
|
人工智能 物联网 C语言
SVDQuant:MIT 推出的扩散模型后训练的量化技术,能够将模型的权重和激活值量化至4位,减少内存占用并加速推理过程
SVDQuant是由MIT研究团队推出的扩散模型后训练量化技术,通过将模型的权重和激活值量化至4位,显著减少了内存占用并加速了推理过程。该技术引入了高精度的低秩分支来吸收量化过程中的异常值,支持多种架构,并能无缝集成低秩适配器(LoRAs),为资源受限设备上的大型扩散模型部署提供了有效的解决方案。
38 5
SVDQuant:MIT 推出的扩散模型后训练的量化技术,能够将模型的权重和激活值量化至4位,减少内存占用并加速推理过程
|
4月前
|
监控 算法 Java
Java内存管理:垃圾收集器的工作原理与调优实践
在Java的世界里,内存管理是一块神秘的领域。它像是一位默默无闻的守护者,确保程序顺畅运行而不被无用对象所困扰。本文将带你一探究竟,了解垃圾收集器如何在后台无声地工作,以及如何通过调优来提升系统性能。让我们一起走进Java内存管理的迷宫,寻找提高应用性能的秘诀。
|
22天前
|
安全 Java 程序员
Java内存模型的深入理解与实践
本文旨在深入探讨Java内存模型(JMM)的核心概念,包括原子性、可见性和有序性,并通过实例代码分析这些特性在实际编程中的应用。我们将从理论到实践,逐步揭示JMM在多线程编程中的重要性和复杂性,帮助读者构建更加健壮的并发程序。
|
1月前
|
存储 监控 Java
深入理解计算机内存管理:优化策略与实践
深入理解计算机内存管理:优化策略与实践