0%

RDMA Queue Buffer

参考:

  1. https://zhuanlan.zhihu.com/p/164908617
  2. Linux kernel scatterlist API介绍

Queue Buffer

创建 QP

  1. 用户态调用 Create QP API
  2. 进入用户态驱动程序
  3. 陷入内核态,进入内核驱动程序

用户态驱动程序

  • 校验参数
  • 申请 QP 的 Buffer
    根据用户指定的 Queue 的深度和硬件 WQE 大小,申请一片虚拟内存: mmap
  • 软件资源的初始化

内核态驱动程序

参数:Buffer 大小相关的信息以及起始虚拟地址

1. ib_umem_get() 获取 SG Table

  • 为虚拟页面映射物理页,并固定虚拟页和物理页间的映射关系
    • 内核提供了 Pin 机制 (防止换页)
  • 获取物理页的 DMA 地址
    • 系统提供了 DMA 地址映射接口以获取物理内存页的 DMA 地址
    • 如果系统支持 IOMMU/SMMU,那么这里获取到的是 IOVA,如果不支持,得到的就是 PA

最终返回一个 Scatter Gather Table: 描述虚拟地址连续、物理地址离散的 Buffer

  • 每个元素在内核中被称为 scatterlist
  • 每个 scatterlist 保存其指向的 Buffer 的虚拟地址、长度、DMA 地址、所在的物理页以及页内偏移等信息
  • 多个 scatterlist 放在一起,描述一组离散的 Buffer

ib_umem_get() 产生 SG Table 的过程中,会对相邻的物理页面做合并操作:比如相邻三个 4K 的物理页,会被视为一个 12K 的 scatterlist 元素

2. 将这些 Buffer 传递给 RDMA 网卡

SG Table 本身也可能物理地址不连续(链表), 需要驱动程序另外申请一片物理地址连续的 Buffer,用来拷贝 SG Table 中物理 Buffer 的首地址和长度等信息,最后将这个新申请的用于存放 Buffer 信息的 DMA 首地址填入到 QPC 中

不同的厂商可能有不同的实现:

  • 大多数厂商的硬件只支持同一大小的物理 Buffer
    • SG Table 里不需要记录长度
    • 只在 QPC 里面记录一份
  • 有的 RDMA 网卡为支持更大的总大小和更离散的物理 Buffer,支持多级 SG Table (类似页表)

使用 QP

  1. 用户通过 API 下发 WR
    • WR 中包含了要发送给对端的请求的信息,比如操作类型 (Send/RDMA Write 等), Payload 的虚拟地址和一些控制信息等等
  2. 进入用户态驱动程序
  3. 通过 Doorbell 机制通知硬件

用户态驱动程序

  1. 从之前申请好的 QP Buffer 中找到下一个 WQE 应该存放的位置
  2. 按照格式配置硬件相关的 WQE 的内容
  3. 通过 Doorbell 机制通知硬件
    • 类似中断,携带 QPN ,以及头指针的位置等信息

硬件

  1. 解析 Doorbell
  2. 从 QPC 中获取 QP Buffer 信息
  3. 获取并解析 WQE
    • Buffer Table 的基地址,每个物理 Buffer 的长度
    • Doorbell 中的头指针

用户态 MR Buffer

注册 MR

  1. 用户态应用程序申请内存
    • 根据需要的 Buffer 长度,通过系统提供的 malloc/calloc 等函数申请内存
1
struct ibv_mr *ibv_reg_mr(struct ibv_pd *pd, void *addr, size_t length, int access);
  • 参数:
    • pd:MR 关联的 PD 指针
    • addr:起始虚拟地址
    • length:MR 长度
    • access:MR 的本地和远端访问权限
  • 返回值: 一个 MR 的指针
    1
    2
    3
    4
    5
    6
    7
    8
    9
    struct ibv_mr {
    struct ibv_context *context; //进程上下文指针
    struct ibv_pd *pd; //关联的 PD 指针
    void *addr; //实际起始虚拟地址
    size_t length; //实际长度
    uint32_t handle; //内核态用于索引资源的句柄,用户不用关注
    uint32_t lkey; //L_Key
    uint32_t rkey; //R_Key
    };
  1. 用户在应用程序中调用标准 Verbs API 之后,rdma-core 的公共代码会调用对应设备注册的回调函数,进入用户态驱动
    • 用户态驱动中注册 MR 只是对系统调用的一层抽象
    • 然后陷入内核态
  2. 陷入内核态

内核态

  1. 获取 MR Buffer 信息
    • 调用各厂商内核驱动注册的回调函数
    • 记录 MR Buffer 的长度、起始虚拟地址和权限等信息
  2. 为虚拟页面映射物理页,并固定虚拟页和物理页间的关系: 同样调用 ib_umem_get()
    同 QP Buffer 的区别在于: 内核就不需要给某些虚拟页分配新的物理页,只需要做 Pin 操作
    因为 MR Buffer 是用户在调用 Verbs 接口前自己申请的,用户很有可能在注册 MR 之前,已经对这片内存进行过读写操作,比如把要发送的数据已经放到 Buffer 里了, 那么对应的物理页就已经存在了(通过缺页异常)
  3. 获取物理页的 DMA 地址
    • 通过操作系统提供的接口来获取物理页的 DMA 地址,可能是 IOVA,也可能是 PA
    • RDMA 网卡使用 DMA 地址访问物理内存
  4. 以硬件可以识别的方式组织物理内存页的 DMA 地址
    • 把上一个步骤中得到的所有 DMA 地址以一种方便硬件解析的方式组织起来
    • 同 Queue Buffer 一样,各厂商的实现会有一些差异: 多级页表
  5. 生成 L_Key 和 R_Key
    • MR 的 key.index
      • 由内核驱动分配,用于索引 MR Context
      • 一般是用 bitmap 管理的,以保证 index 的唯一性
      • 对于同一个 RDMA 设备来说,每个 MR 都有唯一的 L_Key/R_Key
      • 每个 L_Key/R_Key 都对应唯一的 MR
    • MR 的 key.key
      • 一般是由驱动生成一个随机值,用于校验
      • MRC 表项里存放 8 bits 的 key (因为 index 是用于索引 MRC 的)
  6. 填写 MRC
    • 通过和硬件之间的控制通道将上述信息下发给硬件
    • 硬件填写到对应的 MRC 表项中, 其中包括:
      • 特殊页表的信息
      • MR 的起始虚拟地址, 长度, key 和权限等等
  7. 返回 MR 信息给用户
    • 驱动程序会记录并且返回 MR 的信息给上层用户
    • 包括:L_Key, R_Key (因为用户指示 HCA 读写 MR 时必须要提供 Key)

使用 MR

Write 请求

  1. 准备数据 (payload)
  2. 下发 WR: 调用 ibv_post_send() 或者其他下发 WR 的 Verbs API, 来指示 HCA 发包
    • WR 中需要包括的关于 MR 信息
      • 本端 Buffer 信息
        • 待发送的 Payload 同样以 SG Table 的形式描述
          1
          2
          3
          4
          5
          struct ibv_sge {
          uint64_t addr; //Buffer 的起始虚拟地址
          uint32_t length; //Buffer 的长度
          uint32_t lkey; //MR 的 L_Key
          };