参考:
Queue Buffer
创建 QP
- 用户态调用 Create QP API
- 进入用户态驱动程序
- 陷入内核态,进入内核驱动程序
用户态驱动程序
- 校验参数
- 申请 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
- 用户通过 API 下发 WR
- WR 中包含了要发送给对端的请求的信息,比如操作类型 (Send/RDMA Write 等), Payload 的虚拟地址和一些控制信息等等
- 进入用户态驱动程序
- 通过 Doorbell 机制通知硬件
用户态驱动程序
- 从之前申请好的 QP Buffer 中找到下一个 WQE 应该存放的位置
- 按照格式配置硬件相关的 WQE 的内容
- 通过 Doorbell 机制通知硬件
- 类似中断,携带 QPN ,以及头指针的位置等信息
硬件
- 解析 Doorbell
- 从 QPC 中获取 QP Buffer 信息
- 获取并解析 WQE
- Buffer Table 的基地址,每个物理 Buffer 的长度
- Doorbell 中的头指针
用户态 MR Buffer
注册 MR
- 用户态应用程序申请内存
- 根据需要的 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
9struct 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
};
- 用户在应用程序中调用标准 Verbs API 之后,rdma-core 的公共代码会调用对应设备注册的回调函数,进入用户态驱动
- 用户态驱动中注册 MR 只是对系统调用的一层抽象
- 然后陷入内核态
- 陷入内核态
内核态
- 获取 MR Buffer 信息
- 调用各厂商内核驱动注册的回调函数
- 记录 MR Buffer 的长度、起始虚拟地址和权限等信息
- 为虚拟页面映射物理页,并固定虚拟页和物理页间的关系: 同样调用
ib_umem_get()
同 QP Buffer 的区别在于: 内核就不需要给某些虚拟页分配新的物理页,只需要做 Pin 操作
因为 MR Buffer 是用户在调用 Verbs 接口前自己申请的,用户很有可能在注册 MR 之前,已经对这片内存进行过读写操作,比如把要发送的数据已经放到 Buffer 里了, 那么对应的物理页就已经存在了(通过缺页异常) - 获取物理页的 DMA 地址
- 通过操作系统提供的接口来获取物理页的 DMA 地址,可能是 IOVA,也可能是 PA
- RDMA 网卡使用 DMA 地址访问物理内存
- 以硬件可以识别的方式组织物理内存页的 DMA 地址
- 把上一个步骤中得到的所有 DMA 地址以一种方便硬件解析的方式组织起来
- 同 Queue Buffer 一样,各厂商的实现会有一些差异: 多级页表
- 生成 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 的)
- MR 的 key.index
- 填写 MRC
- 通过和硬件之间的控制通道将上述信息下发给硬件
- 硬件填写到对应的 MRC 表项中, 其中包括:
- 特殊页表的信息
- MR 的起始虚拟地址, 长度, key 和权限等等
- 返回 MR 信息给用户
- 驱动程序会记录并且返回 MR 的信息给上层用户
- 包括:L_Key, R_Key (因为用户指示 HCA 读写 MR 时必须要提供 Key)
使用 MR
Write 请求
- 准备数据 (payload)
- 下发 WR: 调用
ibv_post_send()或者其他下发 WR 的 Verbs API, 来指示 HCA 发包- WR 中需要包括的关于 MR 信息
- 本端 Buffer 信息
- 待发送的 Payload 同样以 SG Table 的形式描述
1
2
3
4
5struct ibv_sge {
uint64_t addr; //Buffer 的起始虚拟地址
uint32_t length; //Buffer 的长度
uint32_t lkey; //MR 的 L_Key
};
- 待发送的 Payload 同样以 SG Table 的形式描述
- 本端 Buffer 信息
- WR 中需要包括的关于 MR 信息