SPDK | 如何实现地址转换


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_REGISTERSPDK_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。

声明:一丁点儿的网络日志|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - SPDK | 如何实现地址转换


勿在浮沙筑高台,每天进步一丁点儿!