• dpdk legacy memory 模型浅析


    dpdk legacy memory 模型浅析

    dpdk 早期版本使用 legacy memory 模型实现,此模型架构图如下(图片来自互联网):
    在这里插入图片描述
    在这种模型中,大页内存 VA layout 按照 PA layout 进行布局,VA 与 PA 的 layout 都是固定的
    linux 平台实现代码中会对所有的大页进行两次映射来实现上述架构,这两次映射的功能如下:

    1. 映射所有的大页的 PA 并进行排序,排序过程即为 PA layout 过程
    2. 以排序结果为数据源,第二次 mmap 所有大页,以多个 PA 连续的大页为单位 layout VA,将大页映射到 VA 空间中

    完成了上述过程后, VA 与 PA layout 就固定下来,在程序运行过程中不会发生变化。 linux 平台下此模型实现有如下特点:

    1. 程序初始化时默认映射所有的大页,程序启动后不能动态扩展大页
    2. 基于大页分配的内存空间物理地址连续,单次能够分配的空间取决于连续的物理地址空间大小
    3. 必须挂载 hugetlbfs

    第 2 条特点实际描述了一个常见问题——为什么总的大页内存足够但是还是会分配内存失败?

    这是这种实现天然存在的问题,常规的内存分配应该是预留多少内存就能够分配多少(实际会少于预留值),但 dpdk legacy memory 实现下内存的分配却受限于大页的物理内存连续性,常常需要在系统启动更前的地方预留更多的大页内存,也限制了 dpdk 的应用场景。

    在本文中我将分析下 legacy memory 模型的实现原理,为后续研究 dpdk 18.05+ 的 modern memory 模型打下基础。

    基础信息

    dpdk 版本:17.11

    平台: linux

    dpdk 启动参数:未指定 —huge-dir 参数

    硬件架构:x86_64

    dpdk 编译参数:未使能 RTE_EAL_NUMA_AWARE_HUGEPAGES

    hugepage_info 结构

    功能:

    保存每一种类型大页的信息,如 2M hugepage 信息、512M hugepage 信息、1G hugepage 信息等。

    成员:

    成员功能
    uint64_t hugepage_sz大页内存一页的大小
    const char *hugedirhugetlbfs 挂载目录
    uint32_t num_pages[RTE_MAX_NUMA_NODES]每个 numa 节点上的大页数量
    int lock_descriptor大页 map 文件所在目录的文件描述符

    hugepage_info 的初始化——eal_hugepage_info_init 函数

    1. 遍历 /sys/kernel/mm/hugepages 目录,使用 “hugepages-” 前缀匹配其下的子目录名称,匹配成功且 hugepage 类型未超过 MAX_HUGEPAGE_SIZES 时,解析数据填充 internal_config 中的每个 hugepage_info 结构,以下简称 hpi。
      1. 将子目录名称中 “hugepages-” 之后的大页大小字符串转化为数字保存到 hpi 的 hugepage_sz 字段中,如 2M 的大页对应的 hugepage_sz 值为 2097152,单位为字节。

      2. 遍历 /proc/mounts 文件,解析每一行挂载点信息,以空格为分隔符切割字符串,使用第三列的文件系统类型字符串匹配 “hugetlbfs” 匹配到 hugetlbfs 的挂载点信息,命中后,解析 hugetlbfs 中的 pagesize,解析失败则使用默认值(解析 /proc/meminfo 中的 Hugepagesize: 字段值获取),此后将 pagesize 与第 2 步中的 hugepage_sz 比较,相等则使用 strdup 复制第二列的挂载点字符串并填充到 hpi 的 hugedir 字段中。

        hugetlbfs 挂载点信息示例如下:

        nodev /dev/hugepages hugetlbfs rw,relatime,pagesize=2M 0 0
        
        • 1
      3. open hpi 中的 hugedir 保存的挂载点目录,并保存 fd 到 hpi 的 lock_descriptor 字段中,然后调用 flock 获取互斥锁

      4. 清空目标 hugepage 挂载目录中的所有未使用的大页 map 文件,使用 “map_” 通配符匹配挂载目录中的文件名,对每一个命中的文件名执行如下操作:

        1. 打开文件
        2. 以非阻塞方式获取文件互斥锁
        3. 成功获取到锁表明需要清理,解锁互斥锁并调用 unlinkat 函数释放文件完成清理工作
      5. 解析 /sys/kernel/mm/hugepages/hugepages-xxx 子目录下的文件,获取 free_hugepages 文件与 resv_hugepages 文件的内容,计算可用大页数量并保存到 hpi 的 num_pages[0] 中。

    2. 以每个 hpi 的 hugepagesz 为单位从大到小排序 internal_config 中的保存的解析完成的 hugepage_info 结构并校验内容。

    每个 hugepage_info 中的每个 hugepage_file 初始化——eal_memory_init

    hugepage_file 结构

    功能:

    保存每一个映射的大页信息

    成员:

    成员功能
    void *orig_va首次 mmap 的虚拟地址
    void *final_va第二次 mmap 的虚拟地址
    uint64_t physaddr物理地址
    size_t size页大小
    int socket_id所在 numa 节点
    int file_idHUGEFILE_FMT 中的下标
    int memseg_id归属于的 memory segments 下标
    char filepath[MAX_HUGEPAGE_PATH]在文件系统中的存储路径

    primary 进程执行逻辑——rte_eal_hugepage_init

    1. 判断是否禁用 hugetlbfs,禁用后 mmap 根据配置的内存大小映射一块匿名空间并初始化 rte_mem_config(以下简称 mcfg ) 中的 memseg[0] 结构然后返回。
    2. 遍历 internel_config 中所有 hugepage_info 信息,依次赋值 used_hp hugepage_info 结构体中对应位置的 hugepage_sz 字段,并计算 internal_config 中所有 hugepage_info 结构保存的大页总数量并保存到 nr_hugepages 变量中。
    3. 申请 nr_hugepages 个 hugepage_file 结构空间并清空,将地址赋值给 tmp_hp 变量。
    4. 设置 hp_offset 为 0,开始映射大页。
    5. 注册一个 sigbus 信号处理函数。
    6. 保存 internal_config 中的 socket_mem 结构数组到本地变量 memory 数组中。
    7. 遍历 internal_config 中的每一个 hugepage_info 结构(以下简称 hpi ),依次执行如下操作:
      1. 暂存 hpi 中的 numa_pages[0] 字段到 pages_old 变量中,此字段保存当前大页类型所有可用的大页数量。

      2. 以 tmp_hp[hp_offset] 的地址以及 hpi 及 memory 数组地址及 1 为参数调用 map_all_hugepages 函数 map 所有的大页,并保存其返回值到 pages_new 中

      3. map_all_hugepages 函数以 orig 参数标识第一次 mmap 与第二次 mmap

        首次 mmap 时 map_all_hugepages 函数逻辑如下:

        1. vma_len 初始化为 0 vma_addr 初始化为 NULL
        2. 从 0 开始遍历 hugepage_file 结构,执行如下操作
        3. 初始化 hugepage_file 结构,设置 file_id,hugepage_sz 并生成 filepath 路径
        4. 打开 hugepage_file 结构的 filepath 路径获取到描述符 fd,以此 fd 为参数执行 mmap 操作,设置 mmap 的地址为 vma_addr,由于此时为第一次映射,内核会将映射区域内存清 0
        5. 保存映射成功的虚拟地址到每个 hugepage_file 结构的 orig_va 字段中
        6. 处理内核触发的 sigbus 异常
        7. 非阻塞方式获取 fd 指向的大页文件的共享锁后关闭 fd
        8. 更新 vma_addr 为 vma_addr + hugepage_sz 预留下一块大页的 mmap 虚拟地址,并将 vma_len 减掉 hugepage_sz
      4. 比较 pages_new 与 pages_old 并根据结果更新 hpi num_pages[0] 的值为能够成功映射的大页数目

      5. 获取 hugepage_file 表示的每个大页文件的 orig_va 对应的物理地址,当物理地址可获取时通过解析 /proc/self/pagemap 文件获得,解析的地址将会填充到每个 hugepage_file 的 physaddr 字段中;不可获取时则以一个静态变量的值为起始地址以 hugepage_sz 大小递增赋值给 physaddr 字段

      6. 解析 /proc/self/numa_maps 文件,使用解析得到的虚拟地址匹配 hugepage_file 的 orig_va 地址,匹配到后更新 hugepage_file 的 socket_id 字段保存 numa 节点信息

      7. 以 hugepage_file 的 phyaddrs 字段为单位从大到小排序 hugepage_file

      8. 设置 orig_va 参数为 0 再次调用 map_all_hugepages 函数,第二次映射大页

        第二次 mmap 时 map_all_hugepages 函数逻辑如下:

        1. vma_len 初始化为 0 vma_addr 初始化为 NULL

        2. 设置 i 为 0 开始遍历 hugepage_file 结构,执行下面的操作

        3. 判断 vma_len 是否为 0,为 0 则表明需要重新计算映射基地址,计算方法为遍历正在处理的 hugepage_file 及之后的所有项目,找到下一次物理地址不连续的 hugepage_file 下标,使用此下标减去当前 hugepage_file 下标得到连续的大页数量 num_pages,使用 numa_pages 乘以大页单位大小得到总的需要分配的虚拟内存空间更新 vma_len,然后以 vma_len 与 hpi 中的 hugepage_sz 为参数,调用 get_virtual_area 函数获取虚拟地址,获取失败则将 vma_len 设置为 hugepage_sz,此时只能映射一个大页

          get_virtual_area 函数的主要逻辑如下:

          1. 判断 base_virtaddr 是否设置,设置则将目标地址设置为 base_virtaddr 与内部偏移的 baseaddr_offset 相加的值;否则将目标地址设置为 NULL。目标地址存储在 addr 变量中
          2. 打开 /dev/zero 文件获取到 fd
          3. 使用 addr 变量作为参数,并使用传入的 size 加一个 hugepage_sz 大小作为总大小 mmap,mmap 失败则以 hugepage_sz 为单位递减 size,直到 size 递减为 0 或 mmap 成功,mmap 成功返回的地址也保存到 addr 中。
          4. munmap addr 并关闭 fd
          5. 将 addr 地址以 hugepage_sz 大小对齐(这就是 mmap 时给传入的 size 加入 hugepage_sz 的原因)
          6. 更新 baseaddr_offset 以预留空间,最后返回 addr
        4. 打开 hugepage_file 结构的 filepath 路径获取到描述符 fd,以此 fd 为参数执行 mmap 操作,设置 mmap 的地址为 vma_addr

        5. 保存映射成功的虚拟地址到每个 hugepage_file 结构的 final_va 字段中

        6. 非阻塞方式获取 fd 指向的大页文件的共享锁后关闭 fd

        7. 更新 vma_addr 为 vma_addr + hugepage_sz 预留下一块大页的 mmap 虚拟地址,并将 vma_len 减掉 hugepage_sz,意味着每 map 一个大页可用空间减少 hugepage_sz 大小,当 vma_len 减到 0 时第 3 步中的过程继续执行,为下一个块连续空间申请虚拟地址

      9. 调用 unmap_all_hugepages_orig 函数 unmap 每个 hugepage_file 中的 orig_va 空间,并将 orig_va 地址设置为 NULL

      10. hp_offset 向后递增 hpi 结构的 num_pages[0] 个,继续处理其它大小大页的映射

      11. 还原旧的 sigbus 信号处理函数

      12. 设定 internal_config 中的 memory 字段并清空 internal_config 中每个 hugepage_info 结构中的 numa_pages 数组

      13. 遍历所有的 hugepage_file 结构,获取 socket_id,使用此 socket_id 为下标递增与当前 hugepage_file 的 size 相等的 hugepage_info 中的 numa_pages 字段以更新每个 hugepage_info 中的 num_pages 数组内容

      14. 暂存 internel_config 中的 socket_mem 数组到 memroy 本地数组中

      15. 计算最终要使用的大页的数量 nr_hugepages 并输出每个 hugepage_info 上每个 numa 节点映射的大页数目

      16. 创建用于记录 hugepage_file 信息的共享内存,大小为 nr_hugepages 个 hugepage_file 结构,创建成功后将内存清零,内存地址保存到 hugepage 变量中

      17. unmap 掉不需要使用的大页后将前 nr_hugepages 个 hugepage_file 内存拷贝到共享内存中

      18. 判断是否设置 unlink 标志,设置之后 unlink hugepage file

      19. 释放 tmp_hp

      20. 遍历共享内存中的 hugepage_file 信息,初始化 rte_config 的 memseg 结构,同时将 memseg id 赋值给 hugepage_file 的 meseg_id 字段

        一个 memseg 结构标识一块物理地址连续的内存空间,memseg 的 len 成员表示这块内存空间的总大小, addr 成员表示这块内存空间的起始虚拟地址在 x86_64 架构中是第一块大页的第二次 mmap 获取的 final_va 地址

      21. munmap hugepage 指向的共享内存

    secondary 进程执行逻辑——rte_eal_hugepage_attach

    1. 获取 rte_config 共享内存
    2. 打开 /dev/zero 文件保存文件描述符到 fd_zero 变量中,打开 hugepage_info 文件保存文件描述符到 fd_hugepage 变量中
    3. 遍历 rte_config 中的每一个 memseg 结构,此结构的 addr 字段为物理地址连续区域的首地址,len 字段为内存长度,mmap 到 fd_zero 文件并判断返回地址,返回地址与 memseg 中的 addr 不一致则返回错误,保证 secondary 进程的大页与 primary 进程映射到的虚拟地址完全一致
    4. 获取 hugepae_info 文件的总大小, mmap 此文件获取到 primary 进程初始化的 hugepage_file 结构的首地址
    5. 从小到大遍历 rte_config 中的每个 memseg,先暂存 memseg 的 addr 地址到 base_addr 中,然后 unmap 此地址,然后使用 memseg 的 id 在所有的 hugepage_file 中匹配,与 hugepage_file 的 memseg_id 字段对比,找到属于此 memseg 的所有大页并依次 map hugepage_file 的 filepath 指向的大页文件到 memseg 内部对应的地址中实现与 primary 进程共享内存的效果
    6. munmap hugepage_info 文件并关闭 fd_hugepage 与 fd_zero 文件描述符

    总结

    开篇中概括了 dpdk legacy memory 架构,描述了此架构的一些特点,从工作流程中能够看出这种架构实现复杂且不优雅,使用中也有诸多限制。18.5+ 版本后新的内存框架引入,解决了 legacy memory 框架存在的几个关键问题,实现更简单,支持的场景更多,在后续的文章中我将进行分析。

  • 相关阅读:
    python实验10_文件与异常Ⅱ
    谷粒商城-商品服务(平台属性)
    goland远程调试k8s上容器
    注解开发:spring的强项(1)
    (图论) 连通分量(模板) + 强连通分量(模板)
    java常见集合框架的区别
    SpreadsheetGear for Windows 9.X UPDATE
    PS图片背景透明(抠图)
    非零基础自学Java (老师:韩顺平) 第7章 面向对象编程(基础部分) 7.7 作用域
    互联网摸鱼日报(2023-10-20)
  • 原文地址:https://blog.csdn.net/Longyu_wlz/article/details/127592265