SPDK 在有些驱动场景下需要将虚拟地址转换(VA)为物理地址(PA)——这里先不考虑 IOVA 的概念。例如走 UIO 框架的 NVMe SSD 驱动,在填充 PRP Entry 时需要进行 VA 到 PA 转换。因此,SPDK 中提供了一个通用的函数 spdk_vtophys()
来实现地址转换功能。
本文基于 SPDK 22.03 版本,主要围绕以下两个问题进行分析:
- SPDK 通过 DPDK 库初始化并管理大页内存,SPDK 如何感知到使用了哪些物理内存?
spdk_vtophys()
函数的实现细节,它是如何实现地址转换的?
DPDK 内存映射信息获取
为了向上层 App 返回所有 memseg 信息,DPDK 提供了一个统一的接口 rte_memseg_contig_walk()
用于遍历其管理的所有内存地址。
mem: provide thread-unsafe contig walk variant
Sometimes, user code needs to walk memseg list while being inside a memory-related callback. Rather than making everyone copy around the same iteration code and depending on DPDK internals, provide an official way to do memseg_contig_walk() inside callbacks.
基于此函数接口,SPDK 在启动流程中调用了此函数,并传入了回调函数memory_iter_cb
。整体的启动流程如下函数调用栈:
rte_eal_init()
进行 DPDK 的初始化;- DPDK 初始化完成后,在
mem_map_init()
函数中调用了rte_memseg_contig_walk()
函数并注册回调memory_iter_cb()
;
spdk_env_init()
-> rte_eal_init() // DPDK 初始化
-> spdk_env_dpdk_post_init()
-> mem_map_init(legacy_mem)
-> rte_mem_event_callback_register("spdk", memory_hotplug_cb, NULL);
-> rte_memseg_contig_walk(memory_iter_cb, NULL);
-> vtophys_init()
DPDK 中实现的 rte_memseg_contig_walk()
函数如何遍历所有 memseg 我们不详细展开,重点分析 memory_iter_cb()
函数,看该函数拿到内存信息后都做了什么。
构建 g_mem_reg_map 记录
memory_iter_cb()
接收参数const struct rte_memseg *ms
记录了 mem_seg 的起始虚拟地址,len
参数记录了 mem_seg 的长度,再传入到spdk_mem_regsiter()
函数中。spdk_mem_regsiter()
函数主要构建 g_mem_reg_map 全局结构:
- 首先检查地址是否 2MB 对齐,以及长度是否 2MB 对齐;
- 通过地址有效性检查后,再检查该 mem_seg 是否已经在 g_mem_reg_map 中注册过;
- 若未注册则将该 mem_seg 按 2MB 大小分别记录到 g_mem_reg_map 结构中,打上标记
REG_MAP_REGISTERED
,未分段前的起始地址额外标记为REG_MAP_NOTIFY_START
; 记录完成后,将触发其他 mem_map 注册的回调函数;
memory_iter_cb(const struct rte_memseg_list *msl, const struct rte_memseg *ms, size_t len, void *arg) -> spdk_mem_register(ms->addr, len); -> spdk_mem_map_translate(g_mem_reg_map, (uint64_t)seg_vaddr, NULL) -> spdk_mem_map_set_translation(g_mem_reg_map, (uint64_t)vaddr, VALUE_2MB, seg_len == 0 ? REG_MAP_REGISTERED | REG_MAP_NOTIFY_START : REG_MAP_REGISTERED); -> map->ops.notify_cb(map->cb_ctx, map, SPDK_MEM_MAP_NOTIFY_REGISTER, seg_vaddr, seg_len);
spdk_mem_map_set_translation()
函数实现的主要逻辑即填充g_mem_reg_map 结构中struct map_256tb map_256tb
成员。map_256tb
整体结构如下图,包含1ULL << (SHIFT_256TB - SHIFT_1GB)
个 struct map_1gb
结构体指针(256TB 需要 256 * 1024 = 262144 个 1gb 结构体描述)。类似的,struct map_1gb
结构体中有1ULL << (SHIFT_1GB - SHIFT_2MB)
个struct map_2mb
成员(1GB 需要有 1 * 1024 / 2 = 512 个 2MB 结构体描述)。
最终 VA 到 PA 的映射记录在 struct map_2mb
的成员 uint64_t translation_2mb
中,因为 256TB 只需要使用到 48 bit 地址,所以translation_2mb
高 16 bit 用来记录前面提到的REG_MAP_REGISTERED
等 flag。
VA 到 PA 映射记录:g_vtophys_map
从上文 SPDK 整体启动流程的调用栈可以看到,g_mem_reg_map 初始化完成后(函数mem_map_init()
执行完成),紧接着就调用vtophys_init()
函数来创建记录 VA 到 PA 转换的 g_vtophys_map 。如下所示:通过调用spdk_mem_map_alloc()
函数创建一个新的 mem_map g_vtophys_map
,并注册回调函数 vtophys_notify()
。spdk_mem_map_alloc()
函数中除了创建g_vtophys_map
实例,还会调用mem_map_notify_walk()
函数来遍历 g_mem_reg_map 并调用g_vtophys_map
注册的回调函数来完成内存映射关系的记录。
int
vtophys_init(void)
{
const struct spdk_mem_map_ops vtophys_map_ops = {
.notify_cb = vtophys_notify,
.are_contiguous = vtophys_check_contiguous_entries,
};
// ...
if (g_huge_pages) {
g_vtophys_map = spdk_mem_map_alloc(SPDK_VTOPHYS_ERROR, &vtophys_map_ops, NULL);
if (g_vtophys_map == NULL) {
DEBUG_PRINT("vtophys map allocation failed\n");
spdk_mem_map_free(&g_phys_ref_map);
return -ENOMEM;
}
}
return 0;
}
至此,整体流程串起来了,我们进一步分析一下拿到 DPDK 管理的内存地址后,SPDK 如何建立起 VA 到 PA 的映射关系,即vtophys_notify()
的具体实现。该函数代码很长,主要逻辑如下:
- 检查地址是否有效:起始地址是否 2MB 对齐,长度是否为 2MB 的整数倍;
- 传入虚拟地址到
vtophys_get_paddr_memseg()
函数获取 vaddr 对应的物理地址,这个函数是 DPDK 提供的rte_mem_virt2memseg
函数的一个简单封装; - 根据 action 执行不同的动作,主要分为
SPDK_MEM_MAP_NOTIFY_REGISTER
和SPDK_MEM_MAP_NOTIFY_UNREGISTER
,分别对应内存映射注册和取消映射路径,我们主要看内存注册路径;
当vtophys_get_paddr_memseg()
函数返回成功时,表示该段内存是由 DPDK 管理的,则直接按 2MB 分段获取对应的 paddr 并注册到g_vtophys_map
中,如下代码所示。这里采用的记录结构和我们上面提到 g_mem_reg_map 所采用的结构是一样的,区别点在于g_mem_reg_map
使用的是 struct map_2mb
的高位记录 flag,g_vtophys_map
使用的地位实际记录了 PA。
/* This is an address managed by DPDK. Just setup the translations. */
while (len > 0) {
paddr = vtophys_get_paddr_memseg((uint64_t)vaddr);
if (paddr == SPDK_VTOPHYS_ERROR) {
DEBUG_PRINT("could not get phys addr for %p\n", vaddr);
return -EFAULT;
}
rc = spdk_mem_map_set_translation(map, (uint64_t)vaddr, VALUE_2MB, paddr);
if (rc != 0) {
return rc;
}
vaddr += VALUE_2MB;
len -= VALUE_2MB;
}
前面我们一直基于一个前提(或者说假设):SPDK 只维护 DPDK 所管理的内存地址范围,spdk_mem_register()
函数只会在memory_iter_cb()
回调函数中调用。实际 SPDK 内部可以主动调用spdk_mem_register()
函数注册一段内存地址。例如,SPDK 的 NVMe 驱动将 NVMe SSD 的CMB(Controller Memory Buffer)——SSD控制器内部的读写存储缓冲区——注册到g_vtophys_map
中,后续可直接使用spdk_vtophys()
获取其对应的物理地址。
所以vtophys_get_paddr_memseg()
函数可能会返回失败,表示该段内存不是由 DPDK 管理的。因此,在vtophys_notify()
内存注册路径上会对此场景进行额外的映射处理:
- IOMMU 使能并且 DPDK 的 iova_mode 为 VA 模式时,paddr 取 vaddr,调用
vtophys_iommu_map_dma()
函数注册到 IOMMU 中;并按 2MB 分段注册到g_vtophys_map
中; PA 模式时,从进程的
/proc/self/pagemap
文件中获取 VA 起始地址对应的 PA- 如果获取失败则尝试从 PCI 设备接口获取对应的 PA,如果成功则标记该段内存属于 PCIe 设备空间;
- 如果获取成功则标记该段内存属于正常的内存段;
- 根据内存段所属的范围(PCI 设备或内存区域)分别按 2MB 分段调用
vtophys_get_paddr_pci((uint64_t)vaddr)
函数和vtophys_get_paddr_pagemap((uint64_t)vaddr)
函数获取 VA 对应的 PA; - 如果 IOMMU 使能,但是 DPDK 处于 PA 模式,则还是调用
vtophys_iommu_map_dma()
函数注册内存以保证一致性; - 最后调用
spdk_mem_map_set_translation()
函数将记录保存到g_vtophys_map
中。
总结
本文分析了 SPDK 如何感知 DPDK 使用了哪些物理内存:以 DPDK 提供的接口rte_memseg_contig_walk()
遍历其管理的所有内存地址并记录到 g_mem_reg_map 和 g_vtophys_map
结构体中。spdk_vtophys()
函数即通过查g_vtophys_map
中记录的表获取 VA 对应的 PA。同时,SPDK 不仅仅建立了 DPDK 所管理内存的映射关系,还支持内部通过接口注册新的内存段,以方便复用spdk_vtophys()
函数获取 VA 对应的 PA。
Comments | NOTHING