ef_vi 学习笔记
ef_vi 是 OSI 二层的一套接口,用户态应用可以绕开 POSIX socket 的接口,直接通过 ef_vi 访问 Solarflare 网卡的数据路径,能够降低延迟并减少每个消息发送的开销。
ef_vi 是位于 OSI 二层的接口,应用可以通过这些接口处理原始二层以太帧的收发,此时上层协议就必须由应用实现。
另外 ef_vi 是打包在 OpenOnload 发行版里的,仓库在这里:https://github.com/Xilinx-CNS/onload
参考文档:
https://docs.amd.com/r/en-US/ug1586-onload-user/ef_vi
https://docs.amd.com/v/u/en-US/SF-114063-CD-ef_vi_User_Guide
总览
ef_vi 能够暴露一些网卡特性的接口,或者与这些特性协同工作,比如:
- 校验和 offload
- 硬件时戳
- 一些二层交换功能
- 哈希 RX 负载均衡
- 分流
- 数据路径的监控
- 极低延迟的 CTPIO 模式
- 低延迟 PIO 模式
- Scatter/gather 的传输与接收
- 虚拟环境下的 PCI 直通与 SO-IOV
与其他类似技术相比,ef_vi 能提供更好的灵活性。 每个 ef_vi 实例都可以认为是交换机上的一个虚拟接口,这个实例能够处理单个物理端口的包,也可以仅处理特定的包子集。 每个物理端口支持至多 2048 个虚拟网卡接口。 这就意味着可以通过多个 ef_vi 实例同时支持不同的上层网络实现,比方说 Linux 标准网络栈可以跟 AMD OpenOnload 的 socket 加速技术同时使用。 通过这些虚拟接口,我们可以把单个物理端口的流量负载分配的不同的 CPU 核心上处理,或者同时加速大量的虚拟机。
一些常见的应用场景:
-
套接字加速
可以使用 ef_vi 代替 POSIX socket API 或其他 API。 应用可以为各个的线程分配不同的 ef_vi 实例,并分别指定它们关心的特定流量,也就是 RX 过滤器。 比方说可以指定接收 UDP 协议的特定目的 IP 与端口号。 这些流量会被传递给 ef_vi 处理,而其他流量则会经由通用驱动照旧传递给内核网络栈。
-
包捕获
-
包回放
-
应用模拟终端
-
软件定义的桥接、交换与路由
概念
虚拟接口
每个 ef_vi 实例对应网卡的一个虚拟接口(VI)。 每个虚拟接口包括以下组件:
- 一个事件队列
- 一个 TX 描述符环
- 一个 RX 描述符环
这些组件都是可选的,但是如果不指定事件队列,就必须指定另一个具有事件队列的虚拟接口作为替代。
每个虚拟接口还具有一些硬件资源:
- 一个门铃寄存器,用于通知硬件已有新的 RX 缓冲区可用于写入
- 一个门铃寄存器,用于通知硬件已有新的 TX 缓冲区可用于发送
- 一些定时器
- 一个共享的中断
虚拟接口集
包含多个虚拟接口,通过 RSS 策略,将过滤器过滤出的流量负载自动分配到各个虚拟接口上。
注意一下,必须为虚拟接口集配置多个过滤器,因为单个流量过滤器的 RSS 哈希值是固定的。 这意味着单个过滤器的流量只会被分配到某个特定的虚拟接口上。
事件队列
用于向应用传递网卡上的信息,例如包的接收、发送完成等。
TX 描述符环
用于从应用向网卡传递需要发送的包。 TX 描述符环中的每一个描述符条目会对应一个缓冲区,需要发送的数据就被存储在这个缓冲区中。 一个包可以通过一个描述符发送,长包也可以通过多个描述符发送。
应用需要向缓冲区中写入包,并提交到 TX 描述符环中。 包传输就会在后台进行,传输完成时网卡会通过事件队列通知应用。
RX 描述符环
用于从网卡向应用传递接收到的包。 每个描述符对应一个空闲的缓冲区,网卡接收到包时会把它写入下一个空闲缓冲区中,并通过事件队列通知应用。 应用消费完包后,需要将恢复空闲状态的缓冲区重新提交到 RX 描述符环中。
视网卡型号不同,缓冲区的分配方式是不一样的。 比方说部分网卡的缓冲区需要由 ef_vi 库管理,而部分网卡则需要应用分配并管理。 具体需要参考官方文档。
保护域
一个保护域标识了一个用于 DMA 地址的独立地址空间,这些 DMA 地址会被传递给网卡。 保护域可以用于隔离不同的 ef_vi 应用所使用的 DMA 内存,或用于共享资源。
- 每个虚拟接口(ef_vi 实例)只能绑定到一个保护域上。
- 多个虚拟接口可以绑定到同一个保护域上。
- 每个内存区域可以绑定到多个保护域上。
- 每个内存区域只能由处于相同保护域内的虚拟接口使用。
传统的设备驱动能够直接把内存缓冲区的物理地址传递给 I/O 设备,因为驱动本身运行在内核态。 但使用 ef_vi 的上层应用通常不能使用未经保护的物理地址,所以需要通过保护域间接地获取缓冲区的物理地址,并将其传递给网卡。
内存区域
内存区域是通过 ef_memreg 接口注册的,可用于 TX 或 RX 缓冲区。
这能够确保内存区域满足 ef_vi 的要求:
- 内存会被固定(pinned),保证驻留在物理内存中,不会被交换到交换空间中。
- 内存被映射为 DMA 地址,如此一来网卡就可以访问这块内存。网卡会把用户提供的 DMA 地址翻译为总线上的 I/O 地址。
- 内存区域需要是页对齐的。
- 内存区域的大小需要是包缓冲区大小的倍数,从而避免浪费。
包缓冲区
包缓冲区是分配在主机上的内存,可供网卡读取需要发送的包,或将接收到的包写入进去。 其大小通常为 2KB。
包缓冲区会通过这样的方式映射给网卡:只有处在相同保护域中的虚拟接口才可访问,除非显式使用物理寻址模式(需要 root 用户配置驱动选项并授予一组用户)。
巨型包
通常来说,单个包缓冲区的一部分可以用来存储一些元数据(比如 DMA 地址),剩余部分应当为标准大小的包预留充足的空间。
在接收场景下,长包则需要通过多个包缓冲区接收。
发送场景下,也可以使用多个缓冲区:
- 凑出足够的空间以容纳更大的包。
- 依据特定的逻辑将包分离到多个缓冲区中(比如可以把公共的标准头抽离出来存放到独立的缓冲区中)。
包缓冲区描述符
每个包缓冲区都由一个描述符指向,包括:
- 指针
- 偏移量
- 长度
这里的描述符就是上文中 RX 与 TX 描述符环所维护的描述符。
CTPIO
CTPIO(Cut-through PIO)可以翻译为直通 PIO,它能够极大地优化发送延迟,三个模式:
-
直通模式
这个模式拥有最优的延迟。 数据帧通过 PCIe 总线传输至网卡,而网卡在数据帧仅有部分到达的情况下就开始通过物理端口以固定的速率将其发送至网络中。 这意味着如果数据帧的剩余部分未能及时到达网卡(欠载),网卡后续的发送操作自然也无法正常进行,此时这个数据帧会被毒化。 比如一种常见的场景是,应用进程在将数据帧传输至网卡时被中断。 总之在这类欠载的情况下,数据帧最后的 FCS 会被设为一个错误的值,从而使得这个数据帧被对端丢弃。
另外,可以通过配置,控制网卡在接收到数据帧指定数量的字节后再开始将报文发送出去。
-
存储转发模式
数据帧在传输到网络前,需要先被完整的缓存在网卡上。
-
避免毒化的存储转发模式
与存储转发模式相同,但是不会发送毒化的数据帧。
X2 系列网卡上的 CTPIO 与回退
X2 系列网卡上 CTPIO 失败的原因包括:传输数据帧时欠载、多线程同时使用 CTPIO 产生的竞争、CPU 向网卡传输数据帧时写入顺序有误。
这些失败情况都会导致 X2 系列网卡回退到传统的 DMA 发送机制(高延迟)。 因此无论 CTPIO 是否成功,其数据帧总会有一份有效的拷贝被发送出去。
注意:如果进行过一次 CTPIO 以外的发送(包括回退至 DMA 的情况),执行下一次 CTPIO 前必须保证 TX 队列被清空(即确保 TX 描述符环中的数据帧都发送完毕)。
通常来说只有直通模式下才会出现数据帧的毒化,但是存储转发模式也会有极低的概率发送出一个毒化的数据帧。 如果有严格避免毒化数据帧进入网络的必要,也可以全局禁用毒化。
PIO
用户可以通过 ef_pio 接口访问网卡上用于低延迟传输的一块内存。
使用这个接口时,包将会由 CPU 指令 push 到网卡上,而不是由网卡通过 DMA 拉取。
PIO 的延迟更低,因为它规避了 DMA 读取操作带来的延迟。
用户还可以在关键路径前将包提前写入网卡,进一步降低延迟。 再发送前,用户还可以对包进行更新。 这种方式能够减少关键路径中传递给网卡的数据量,从而降低延迟。
尽管 PIO 能够改善延迟,但仍不及 CTPIO,条件允许的情况下,应用还是应当使用 CTPIO。
过滤器
过滤器决定了哪些包会被传递给虚拟接口,而其余的包将会被忽略并允许传递给系统内核(比如 POSIX socket)。
每个过滤器指定了筛选包的特征,这些特征通常是包头里的一些字段:以太 MAC 地址、VLAN 标签、IP 地址、端口、协议类型等。
筛选出的报文可以被:
- 劫持:包传递至虚拟接口,不进入内核协议栈。
- 复制:副本传递至虚拟接口,同时也可以被传递给其他消费者。用于组播。
- 嗅探:数据包同时传递给虚拟接口与内核协议栈。
VLAN
ef_vi 对 VLAN 的支持较为有限。 因为 ef_vi 运作在以太帧层,而 VLAN 通常在更高层处理。
- 接收包时可通过 VLAN 进行过滤,但这需要网卡支持。用户可通过能力 API 获取支持情况。VLAN 相关过滤器在与其他过滤器同时使用时,存在一些限制,详见接口文档中的
ef_filter_spec_set_vlan。 - 发送包时用户需要手动在扩展头中添加 VLAN 标签。与校验和不同,ef_vi 不提供这部分的硬件 offload。
TX 备选
TX 备选是 SFN8000 与 X2 系列网卡支持的特性,它通过提供多个用于传输的备选队列来降低延迟。 用户可以将不同的预设响应包 push 到网卡的发送路径,并暂存于不同的备选队列等待传输。 当用户决定发送某个响应包时,可以调用接口,这个响应包就会从对应的备选队列中传输出去。 由于包都是被预先准备好并存储在接近物理端口的位置,发送延迟就能被显著降低。
尽管 TX 备选也能降低延迟,其效果仍不及 CTPIO,若条件允许,应用还是应当使用 CTPIO。
使用 ef_vi
编译与链接
ef_vi API 相关的所有组件都发布在 Onload 中,安装可参考 https://docs.amd.com/r/en-US/ug1586-onload-user/Onload-Distributions。
安装好 Onload 后,所需要的目录与文件可以在标准系统路径中找到(比如 /usr/include/etherfabirc)。
头文件路径是 /usr/include/etherfabric。
用户的应用必须静态链接 libciul1.a。
执行 Onload 的构建或安装脚本之后,用户可以在 build 目录中找到这个静态库。
初始化
- 调用
ef_driver_open接口获取驱动句柄。 - 调用以下任一接口分配保护域:
ef_pd_allocef_pd_alloc_by_nameef_pd_alloc_with_vport
- 调用
ef_vi_alloc_from_pd分配一个虚拟接口(VI),虚拟接口会被封装在ef_vi类型中。
创建包缓冲区
对于 X4 系列网卡的 Express 数据路径,包缓冲区由 ef_vi 库创建。 X3 系列网卡则是由内核驱动创建。 其他场景下,用户应当依据本节手动创建包缓冲区。
用作包缓冲区的内存使用标准方法分配,例如:
posix_memalignmmap分配大页(推荐)
包缓冲区的大小应当至少为 ef_vi_receive_buffer_len 接口的返回值。
缓冲区内存需要满足以下条件:
- 固定内存(禁止换页)
- 向网卡注册为 DMA 用途
上述条件可以通过调用 ef_memreg_alloc 接口来保证。
网卡通过特殊的地址空间标识这些缓冲区的位置,这类地址由 ef_addr 类型定义。
用户可以通过 ef_iovec 封装多个这样的缓冲区传递给 ef_vi 接口,其用法与 iovec 类似。
为了优化性能,包缓冲区的内存应当连续,且需要对齐:普通页应当至少按 4KB 边界对齐;大页应当按照 2MB 边界对齐。
缓冲区表
用户创建的包缓冲区内存将会通过一张缓冲区表进行映射。 具体实现可能有差异,但通常情况下:
- 缓冲区表是从可被用于其他用途的内存中分配的,因此大小是可变的
- 表中的条目被分为若干集合
- 每个条目会映射一块自然对齐的连续内存
- 每个包缓冲区占用 2KB
实际可用的包缓冲区总数可能会少于预期:
-
某些条目集合可能不会被完全利用
-
初始化虚拟接口会消耗缓冲区表的资源
在 X4 系列网卡 Enterprise 数据路径、X2 系列网卡、SFN8000 系列网卡的场景下,用于 TX 队列、RX 队列与事件队列的每页主机内存都会占用一个缓冲区表的条目来进行映射。 因此几百个虚拟接口可能会消耗大量条目,尤其是事件队列容量较大的情况下。
-
描述符缓存会消耗缓冲区表的资源
描述符缓存会消费网卡上的内存(这些内存原本是可以用于包缓冲区)。
传输包
通过 DMA 传输包
除了必须使用 CTPIO 的 X3 系列网卡,其他所有网卡都支持 DMA 传输。 X4 系列网卡必须使用 Enterprise 数据路径才能进行 DMA 传输。
DMA 传输的基本过程:
-
将包的内容(含头)写入一个或多个包缓冲区中。
包缓冲区的内存必须先注册到保护域上。
-
调用
ef_vi_transmit_init、ef_vi_transmit_push或ef_vi_transmit等接口将已填充数据包缓冲区的描述符提交到 TX 描述符环中。这个操作会通过“敲门铃”通知网卡传输环非空。 在传输环恰好为空的情况下,包缓冲区地址写入会跟“敲门铃”一次性完成,这个操作被称为 TX PUSH,能够降低延迟。 但 TX PUSH 可能需要在发送前轮询事件以确认 TX 描述符环是否为空,这在某些场景下会带来延迟与吞吐量的权衡。
调用这些接口提交描述符时,需要提供一个
dma_id参数。 这个dma_id是可以完全由上层应用指定的。 当传输任务完成后,ef_vi 会通过事件通知上层应用,此时也会把这个dma_id传递给上层应用。 如果我们直接把数据包缓冲区的索引值用作dma_id,那么当获取到传输完成的事件时,应用就可以通过dma_id索引到这次传输所使用的缓冲区,从而将该缓冲区用于后续的其他发送。 此外,也可以将高位用作一些 flag,用于指示本次发送的类型。 -
轮询事件队列以确认传输完成
传输场景下轮询事件队列可能不关键,但仍需执行。
EF_EVENT_TYPE_TX与EF_EVENT_TYPE_TX_WITH_TIMESTAMP事件用于通知传输完成;EF_EVENT_TYPE_TX_ERROR事件用于通知传输失败。 可以使用EF_EVENT_TX_Q_ID接口获取事件所对应的包缓冲区的 ID;或者使用ef_vi_transmit_unbundle接口获取提交描述符时提供的dma_id;或者也可以依赖 ef_vi 总会按照提交顺序传输包这一事实。 -
处理事件
通过事件获取并回收可重用的包缓冲区,比如维护它们的可用状态。
传输长包
一些长包虽然长度短于网卡的 MTU,但是长于包缓冲区的容量,这时就必须通过多个包缓冲区进行发送。
这种长包发送也只会触发一个事件(比如 EF_EVENT_TYPE_TX 或 EF_EVENT_TYPE_TX_ERROR)。
- 调用
ef_vi_transmitv接口将长包的多个片段连接起来 - MTU 不会被强制要求。如果需要保证传输的包长小于 MTU,应用程序需要在发送前自行检查长度。
- 片段的划分必须至少依据自然边界(4K 包),但如有必要也可以使用更短的片段。
通过 CTPIO 传输包
-
启用 CTPIO 特性
如果希望启用 CTPIO 特性,在通过
ef_vi_alloc_from_pd接口分配 VI 时,需要指定EF_VI_TX_CTPIO的 flag。 -
通过 CTPIO 传输以太帧
在主机内存上构造一个完整的以太帧,然后通过
ef_vi_transmit_ctpio或ef_vi_transmitv_ctpio接口发送以太帧。CTPIO 会绕过了网卡的主数据路径,因此不支持校验和 offload。 通过 CTPIO 发送的帧会被直接传输到物理链路上,不经过任何修改。 因此校验和(包括 L3/L4 校验和,但不包括 FCS)应当在发送前由上层软件计算并填充。
-
提供回退的 DMA 传输
CTPIO 可能因为欠载等原因失败,因此通过 CTPIO 传输以太帧之后还需要调用
ef_vi_transmit_ctpio_fallback或ef_vi_transmitv_ctpio_fallback接口将该以太帧提交到 TX 描述符环上。这些调用的用法与标准的 DMA 发送类似。 所以这个以太帧需要存储在由
ef_memreg接口注册的内存中,或者也可以直接拷贝过去。 可以参考 Onload 源码中ef_vi_transmitv_ctpio_copy的实现。如果 CTPIO 成功执行,提交用于回退的描述符不会影响延迟关键路径。 然而,在同一个 VI 上执行其他发送操作前,这个描述符也必须被提交。
通过 PIO 传输包
PIO 只能在 X2 与 SFN8000 系列网卡上使用。 它不能在 X3 与 X4 系列网卡上使用。
-
分配 PIO 缓冲区
PIO 支持更快的传输,尤其是短包,但是服务于 PIO 的硬件资源是有限的。 因此在使用 PIO 前,必须显式地通过
ef_pio_alloc接口分配 PIO 缓冲区并通过ef_pio_link_vi接口关联到一个 VI 上。 -
将以太帧拷贝到 PIO 缓冲区中
使用
ef_pio_memcpy接口把帧拷贝到 PIO 缓冲区中。 -
通过 PIO 传输以太帧
调用
ef_vi_transmit_pio接口把拷贝到 PIO 缓冲区中的帧发送出去。一个 PIO 缓冲区可以存储多个短包,上层应用可以通过接口的偏移量参数指定这些短包在 PIO 缓冲区中的存储位置。 这些短包都可以被独立传输,只要满足以下限制:
- 对齐
- 最小长度
- 最大长度
- 传输完成前,该短包在 PIO 缓冲区中占用的存储空间不应被使用
与标准的 DMA 发送类似,PIO 也在发送时也可以传入
dma_id参数(这里与 DMA 无关,应该只是命名习惯),我们可以通过dma_id区分不同的短包。 在轮询事件队列时,就可以通过事件携带的dma_id确认哪些短包的传输已经完成。 -
释放 PIO 缓冲区
当不再需要使用 PIO 缓冲区后,应当调用
ef_pio_unlink_vi接口解除关联,并调用ef_pio_free接口释放缓冲区。
尽管 PIO 能够优化延迟,它的效果仍不及 CTPIO,新的应用应当优先使用 CTPIO。