dpdk 早期版本使用 legacy memory 模型实现,此模型架构图如下(图片来自互联网):
在这种模型中,大页内存 VA layout 按照 PA layout 进行布局,VA 与 PA 的 layout 都是固定的。
linux 平台实现代码中会对所有的大页进行两次映射来实现上述架构,这两次映射的功能如下:
完成了上述过程后, VA 与 PA layout 就固定下来,在程序运行过程中不会发生变化。 linux 平台下此模型实现有如下特点:
第 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
功能:
保存每一种类型大页的信息,如 2M hugepage 信息、512M hugepage 信息、1G hugepage 信息等。
成员:
成员 | 功能 |
---|---|
uint64_t hugepage_sz | 大页内存一页的大小 |
const char *hugedir | hugetlbfs 挂载目录 |
uint32_t num_pages[RTE_MAX_NUMA_NODES] | 每个 numa 节点上的大页数量 |
int lock_descriptor | 大页 map 文件所在目录的文件描述符 |
将子目录名称中 “hugepages-” 之后的大页大小字符串转化为数字保存到 hpi 的 hugepage_sz 字段中,如 2M 的大页对应的 hugepage_sz 值为 2097152,单位为字节。
遍历 /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
open hpi 中的 hugedir 保存的挂载点目录,并保存 fd 到 hpi 的 lock_descriptor 字段中,然后调用 flock 获取互斥锁
清空目标 hugepage 挂载目录中的所有未使用的大页 map 文件,使用 “map_” 通配符匹配挂载目录中的文件名,对每一个命中的文件名执行如下操作:
解析 /sys/kernel/mm/hugepages/hugepages-xxx 子目录下的文件,获取 free_hugepages 文件与 resv_hugepages 文件的内容,计算可用大页数量并保存到 hpi 的 num_pages[0] 中。
功能:
保存每一个映射的大页信息
成员:
成员 | 功能 |
---|---|
void *orig_va | 首次 mmap 的虚拟地址 |
void *final_va | 第二次 mmap 的虚拟地址 |
uint64_t physaddr | 物理地址 |
size_t size | 页大小 |
int socket_id | 所在 numa 节点 |
int file_id | HUGEFILE_FMT 中的下标 |
int memseg_id | 归属于的 memory segments 下标 |
char filepath[MAX_HUGEPAGE_PATH] | 在文件系统中的存储路径 |
暂存 hpi 中的 numa_pages[0] 字段到 pages_old 变量中,此字段保存当前大页类型所有可用的大页数量。
以 tmp_hp[hp_offset] 的地址以及 hpi 及 memory 数组地址及 1 为参数调用 map_all_hugepages 函数 map 所有的大页,并保存其返回值到 pages_new 中
map_all_hugepages 函数以 orig 参数标识第一次 mmap 与第二次 mmap
首次 mmap 时 map_all_hugepages 函数逻辑如下:
比较 pages_new 与 pages_old 并根据结果更新 hpi num_pages[0] 的值为能够成功映射的大页数目
获取 hugepage_file 表示的每个大页文件的 orig_va 对应的物理地址,当物理地址可获取时通过解析 /proc/self/pagemap 文件获得,解析的地址将会填充到每个 hugepage_file 的 physaddr 字段中;不可获取时则以一个静态变量的值为起始地址以 hugepage_sz 大小递增赋值给 physaddr 字段
解析 /proc/self/numa_maps 文件,使用解析得到的虚拟地址匹配 hugepage_file 的 orig_va 地址,匹配到后更新 hugepage_file 的 socket_id 字段保存 numa 节点信息
以 hugepage_file 的 phyaddrs 字段为单位从大到小排序 hugepage_file
设置 orig_va 参数为 0 再次调用 map_all_hugepages 函数,第二次映射大页
第二次 mmap 时 map_all_hugepages 函数逻辑如下:
vma_len 初始化为 0 vma_addr 初始化为 NULL
设置 i 为 0 开始遍历 hugepage_file 结构,执行下面的操作
判断 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 函数的主要逻辑如下:
打开 hugepage_file 结构的 filepath 路径获取到描述符 fd,以此 fd 为参数执行 mmap 操作,设置 mmap 的地址为 vma_addr
保存映射成功的虚拟地址到每个 hugepage_file 结构的 final_va 字段中
非阻塞方式获取 fd 指向的大页文件的共享锁后关闭 fd
更新 vma_addr 为 vma_addr + hugepage_sz 预留下一块大页的 mmap 虚拟地址,并将 vma_len 减掉 hugepage_sz,意味着每 map 一个大页可用空间减少 hugepage_sz 大小,当 vma_len 减到 0 时第 3 步中的过程继续执行,为下一个块连续空间申请虚拟地址
调用 unmap_all_hugepages_orig 函数 unmap 每个 hugepage_file 中的 orig_va 空间,并将 orig_va 地址设置为 NULL
hp_offset 向后递增 hpi 结构的 num_pages[0] 个,继续处理其它大小大页的映射
还原旧的 sigbus 信号处理函数
设定 internal_config 中的 memory 字段并清空 internal_config 中每个 hugepage_info 结构中的 numa_pages 数组
遍历所有的 hugepage_file 结构,获取 socket_id,使用此 socket_id 为下标递增与当前 hugepage_file 的 size 相等的 hugepage_info 中的 numa_pages 字段以更新每个 hugepage_info 中的 num_pages 数组内容
暂存 internel_config 中的 socket_mem 数组到 memroy 本地数组中
计算最终要使用的大页的数量 nr_hugepages 并输出每个 hugepage_info 上每个 numa 节点映射的大页数目
创建用于记录 hugepage_file 信息的共享内存,大小为 nr_hugepages 个 hugepage_file 结构,创建成功后将内存清零,内存地址保存到 hugepage 变量中
unmap 掉不需要使用的大页后将前 nr_hugepages 个 hugepage_file 内存拷贝到共享内存中
判断是否设置 unlink 标志,设置之后 unlink hugepage file
释放 tmp_hp
遍历共享内存中的 hugepage_file 信息,初始化 rte_config 的 memseg 结构,同时将 memseg id 赋值给 hugepage_file 的 meseg_id 字段
一个 memseg 结构标识一块物理地址连续的内存空间,memseg 的 len 成员表示这块内存空间的总大小, addr 成员表示这块内存空间的起始虚拟地址在 x86_64 架构中是第一块大页的第二次 mmap 获取的 final_va 地址
munmap hugepage 指向的共享内存
开篇中概括了 dpdk legacy memory 架构,描述了此架构的一些特点,从工作流程中能够看出这种架构实现复杂且不优雅,使用中也有诸多限制。18.5+ 版本后新的内存框架引入,解决了 legacy memory 框架存在的几个关键问题,实现更简单,支持的场景更多,在后续的文章中我将进行分析。