从原理到实践:掌握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。

相关文章
|
17天前
|
KVM 虚拟化
KVM的热添加技术之内存
文章介绍了KVM虚拟化技术中如何通过命令行调整虚拟机内存配置,包括调小和调大内存的步骤,以及一些相关的注意事项。
37 4
KVM的热添加技术之内存
|
23天前
|
监控 算法 Java
Java内存管理:垃圾收集器的工作原理与调优实践
在Java的世界里,内存管理是一块神秘的领域。它像是一位默默无闻的守护者,确保程序顺畅运行而不被无用对象所困扰。本文将带你一探究竟,了解垃圾收集器如何在后台无声地工作,以及如何通过调优来提升系统性能。让我们一起走进Java内存管理的迷宫,寻找提高应用性能的秘诀。
|
1月前
|
安全 Java 开发者
Java 内存模型解析与实践
在Java的世界中,理解内存模型对于编写高效、线程安全的代码至关重要。本文将深入探讨Java内存模型的核心概念,并通过实例分析其对并发编程的影响,旨在为读者提供一套实用的策略和思考方式来优化多线程应用的性能与安全性。
49 0
|
18天前
ARM64技术 —— MMU处于关闭状态时,内存访问是怎样的?
ARM64技术 —— MMU处于关闭状态时,内存访问是怎样的?
|
1月前
|
缓存 Java 编译器
Go 中的内存布局和分配原理
Go 中的内存布局和分配原理
|
2月前
|
缓存 安全 算法
Java内存模型深度解析与实践应用
本文深入探讨Java内存模型(JMM)的核心原理,揭示其在并发编程中的关键作用。通过分析内存屏障、happens-before原则及线程间的通信机制,阐释了JMM如何确保跨线程操作的有序性和可见性。同时,结合实例代码,展示了在高并发场景下如何有效利用JMM进行优化,避免常见的并发问题,如数据竞争和内存泄漏。文章还讨论了JVM的垃圾回收机制,以及它对应用程序性能的影响,提供了针对性的调优建议。最后,总结了JMM的最佳实践,旨在帮助开发人员构建更高效、稳定的Java应用。
|
2月前
|
存储 算法 Linux
操作系统中的内存管理:从原理到实践
本文深入探讨了操作系统中至关重要的内存管理机制,揭示了从理论到实现的复杂过程。通过分析内存分配、虚拟内存以及分页和交换等概念,本篇文章旨在为读者提供对现代操作系统内存管理技术的全面理解。结合最新的技术动态和研究成果,文章不仅阐述了内存管理的基本原理,还讨论了其在实际操作系统中的应用和优化策略。
43 1
|
2月前
|
存储 缓存 Java
Android性能优化:内存管理与LeakCanary技术详解
【7月更文挑战第21天】内存管理是Android性能优化的关键部分,而LeakCanary则是进行内存泄漏检测和修复的强大工具。
|
2月前
|
机器学习/深度学习 存储 缓存
操作系统中的内存管理技术
在数字世界的复杂架构中,操作系统扮演着枢纽的角色,其中内存管理作为其核心组件之一,保障了计算资源的高效利用与稳定运行。本文将深入探讨操作系统中内存管理的关键技术,包括虚拟内存、分页和分段机制,以及现代操作系统如何通过这些技术优化性能和提高系统稳定性。通过具体实例和数据分析,我们将揭示这些技术如何在实际应用中发挥作用,并讨论它们面临的挑战及未来发展方向。 【7月更文挑战第16天】
|
2月前
|
存储 缓存 Java
(一) 玩命死磕Java内存模型(JMM)与 Volatile关键字底层原理
文章的阐述思路为:先阐述`JVM`内存模型、硬件与`OS`(操作系统)内存区域架构、`Java`多线程原理以及`Java`内存模型`JMM`之间的关联关系后,再对`Java`内存模型进行进一步剖析,毕竟许多小伙伴很容易将`Java`内存模型(`JMM`)和`JVM`内存模型的概念相互混淆,本文的目的就是帮助各位彻底理解`JMM`内存模型。

热门文章

最新文章