设备内存 TCP

简介

设备内存 TCP (devmem TCP) 允许直接将数据接收到设备内存 (dmabuf) 中。 该特性当前针对 TCP 套接字实现。

机会

大量数据传输以设备内存作为源和/或目标。 加速器极大地增加了此类传输的普遍性。 一些例子包括

  • 分布式训练,其中 ML 加速器(例如不同主机上的 GPU)交换数据。

  • 分布式原始块存储应用程序与远程 SSD 传输大量数据。 大部分数据不需要主机处理。

通常,网络中的设备到设备数据传输被实现为以下底层操作:设备到主机复制、主机到主机网络传输以及主机到设备复制。

涉及主机复制的流程是次优的,特别是对于批量数据传输,并且会对系统资源(例如主机内存带宽和 PCIe 带宽)造成重大压力。

Devmem TCP 通过实现套接字 API 来优化此用例,该 API 使使用者能够直接将传入的网络数据包接收到设备内存中。

数据包有效负载直接从 NIC 进入设备内存。

数据包标头进入主机内存并由 TCP/IP 协议栈正常处理。 NIC 必须支持标头拆分才能实现此目的。

优点

  • 与现有的网络传输 + 设备复制语义相比,减轻了主机内存带宽压力。

  • 通过将数据传输限制到 PCIe 树的最低级别,与通过根联合体发送数据的传统路径相比,减轻了 PCIe 带宽压力。

更多信息

RX 接口

示例

./tools/testing/selftests/drivers/net/hw/ncdevmem:do_server 显示了设置此 API 的 RX 路径的示例。

NIC 设置

标头拆分、流控制和 RSS 是 devmem TCP 的必需特性。

标头拆分用于将传入的数据包拆分为主机内存中的标头缓冲区和设备内存中的有效负载缓冲区。

流控制和 RSS 用于确保只有以 devmem 为目标的流才能落在绑定到 devmem 的 RX 队列上。

启用标头拆分和流控制

# enable header split
ethtool -G eth1 tcp-data-split on


# enable flow steering
ethtool -K eth1 ntuple on

配置 RSS 以将所有流量从目标 RX 队列(本示例中为队列 15)转向

ethtool --set-rxfh-indir eth1 equal 15

用户必须使用 netlink API 将 dmabuf 绑定到给定 NIC 上的任意数量的 RX 队列

/* Bind dmabuf to NIC RX queue 15 */
struct netdev_queue *queues;
queues = malloc(sizeof(*queues) * 1);

queues[0]._present.type = 1;
queues[0]._present.idx = 1;
queues[0].type = NETDEV_RX_QUEUE_TYPE_RX;
queues[0].idx = 15;

*ys = ynl_sock_create(&ynl_netdev_family, &yerr);

req = netdev_bind_rx_req_alloc();
netdev_bind_rx_req_set_ifindex(req, 1 /* ifindex */);
netdev_bind_rx_req_set_dmabuf_fd(req, dmabuf_fd);
__netdev_bind_rx_req_set_queues(req, queues, n_queue_index);

rsp = netdev_bind_rx(*ys, req);

dmabuf_id = rsp->dmabuf_id;

netlink API 返回一个 dmabuf_id:一个唯一的 ID,用于指代已绑定的此 dmabuf。

用户可以通过关闭建立绑定的 netlink 套接字来取消绑定 netdevice 中的 dmabuf。 我们这样做是为了即使用户空间进程崩溃,绑定也会自动取消绑定。

请注意,来自任何导出器的任何合理行为的 dmabuf 都应与 devmem TCP 一起使用,即使 dmabuf 实际上没有 devmem 支持。 这方面的一个例子是 udmabuf,它将用户内存(非 devmem)包装在 dmabuf 中。

套接字设置

必须将套接字流控制到 dmabuf 绑定的 RX 队列

ethtool -N eth1 flow-type tcp4 ... queue 15

接收数据

用户应用程序必须通过将 MSG_SOCK_DEVMEM 标志传递给 recvmsg 来向内核表明它能够接收 devmem 数据

ret = recvmsg(fd, &msg, MSG_SOCK_DEVMEM);

未指定 MSG_SOCK_DEVMEM 标志的应用程序将在 devmem 数据上收到 EFAULT。

Devmem 数据直接接收到在“NIC 设置”中绑定到 NIC 的 dmabuf 中,并且内核通过 SCM_DEVMEM_* cmsg 向用户发出此类信号

for (cm = CMSG_FIRSTHDR(&msg); cm; cm = CMSG_NXTHDR(&msg, cm)) {
        if (cm->cmsg_level != SOL_SOCKET ||
                (cm->cmsg_type != SCM_DEVMEM_DMABUF &&
                 cm->cmsg_type != SCM_DEVMEM_LINEAR))
                continue;

        dmabuf_cmsg = (struct dmabuf_cmsg *)CMSG_DATA(cm);

        if (cm->cmsg_type == SCM_DEVMEM_DMABUF) {
                /* Frag landed in dmabuf.
                 *
                 * dmabuf_cmsg->dmabuf_id is the dmabuf the
                 * frag landed on.
                 *
                 * dmabuf_cmsg->frag_offset is the offset into
                 * the dmabuf where the frag starts.
                 *
                 * dmabuf_cmsg->frag_size is the size of the
                 * frag.
                 *
                 * dmabuf_cmsg->frag_token is a token used to
                 * refer to this frag for later freeing.
                 */

                struct dmabuf_token token;
                token.token_start = dmabuf_cmsg->frag_token;
                token.token_count = 1;
                continue;
        }

        if (cm->cmsg_type == SCM_DEVMEM_LINEAR)
                /* Frag landed in linear buffer.
                 *
                 * dmabuf_cmsg->frag_size is the size of the
                 * frag.
                 */
                continue;

}

应用程序可能会收到 2 个 cmsg

  • SCM_DEVMEM_DMABUF:这表明片段落入由 dmabuf_id 指示的 dmabuf 中。

  • SCM_DEVMEM_LINEAR:这表明片段落在线性缓冲区中。 当 NIC 无法在标头边界处拆分数据包时,通常会发生这种情况,从而导致部分(或全部)有效负载落入主机内存中。

应用程序可能不会收到 SO_DEVMEM_* cmsg。 这表示非 devmem 常规 TCP 数据落在一个未绑定到 dmabuf 的 RX 队列上。

释放片段

通过 SCM_DEVMEM_DMABUF 接收的片段在用户处理该片段时由内核固定。 用户必须通过 SO_DEVMEM_DONTNEED 将片段返回到内核

ret = setsockopt(client_fd, SOL_SOCKET, SO_DEVMEM_DONTNEED, &token,
                 sizeof(token));

用户必须确保将令牌及时返回到内核。 如果不这样做,将会耗尽绑定到 RX 队列的有限 dmabuf,并导致数据包丢失。

用户传递的令牌不能超过 128 个,所有令牌的 token->token_count 中的片段总数不能超过 1024 个。 如果用户提供的片段超过 1024 个,内核将释放最多 1024 个片段并提前返回。

内核返回实际释放的片段数。 在以下情况下,释放的片段数可能少于用户提供的令牌数:

  1. 内部内核泄漏错误。

  2. 用户传递的片段超过 1024 个。

TX 接口

示例

./tools/testing/selftests/drivers/net/hw/ncdevmem:do_client 显示了设置此 API 的 TX 路径的示例。

NIC 设置

用户必须使用 netlink API 将 TX dmabuf 绑定到给定的 NIC

struct netdev_bind_tx_req *req = NULL;
struct netdev_bind_tx_rsp *rsp = NULL;
struct ynl_error yerr;

*ys = ynl_sock_create(&ynl_netdev_family, &yerr);

req = netdev_bind_tx_req_alloc();
netdev_bind_tx_req_set_ifindex(req, ifindex);
netdev_bind_tx_req_set_fd(req, dmabuf_fd);

rsp = netdev_bind_tx(*ys, req);

tx_dmabuf_id = rsp->id;

netlink API 返回一个 dmabuf_id:一个唯一的 ID,用于指代已绑定的此 dmabuf。

用户可以通过关闭建立绑定的 netlink 套接字来取消绑定 netdevice 中的 dmabuf。 我们这样做是为了即使用户空间进程崩溃,绑定也会自动取消绑定。

请注意,来自任何导出器的任何合理行为的 dmabuf 都应与 devmem TCP 一起使用,即使 dmabuf 实际上没有 devmem 支持。 这方面的一个例子是 udmabuf,它将用户内存(非 devmem)包装在 dmabuf 中。

套接字设置

发送 devmem TCP 时,用户应用程序必须使用 MSG_ZEROCOPY 标志。 Devmem 无法由内核复制,因此 devmem TX 的语义类似于 MSG_ZEROCOPY 的语义

setsockopt(socket_fd, SOL_SOCKET, SO_ZEROCOPY, &opt, sizeof(opt));

还建议用户通过 SO_BINDTODEVICE 将 TX 套接字绑定到 dma-buf 已绑定的同一接口

setsockopt(socket_fd, SOL_SOCKET, SO_BINDTODEVICE, ifname, strlen(ifname) + 1);

发送数据

Devmem 数据使用 SCM_DEVMEM_DMABUF cmsg 发送。

用户应创建一个 msghdr,其中,

  • iov_base 设置为 dmabuf 的偏移量,从该偏移量开始发送

  • iov_len 设置为要从 dmabuf 发送的字节数

用户通过 dmabuf_tx_cmsg.dmabuf_id 传递要从中发送的 dma-buf id。

以下示例从偏移量 100 开始向 dmabuf 发送 1024 个字节,并从偏移量 2000 开始向 dmabuf 发送 2048 个字节。 要从中发送的 dmabuf 是 tx_dmabuf_id

char ctrl_data[CMSG_SPACE(sizeof(struct dmabuf_tx_cmsg))];
struct dmabuf_tx_cmsg ddmabuf;
struct msghdr msg = {};
struct cmsghdr *cmsg;
struct iovec iov[2];

iov[0].iov_base = (void*)100;
iov[0].iov_len = 1024;
iov[1].iov_base = (void*)2000;
iov[1].iov_len = 2048;

msg.msg_iov = iov;
msg.msg_iovlen = 2;

msg.msg_control = ctrl_data;
msg.msg_controllen = sizeof(ctrl_data);

cmsg = CMSG_FIRSTHDR(&msg);
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_DEVMEM_DMABUF;
cmsg->cmsg_len = CMSG_LEN(sizeof(struct dmabuf_tx_cmsg));

ddmabuf.dmabuf_id = tx_dmabuf_id;

*((struct dmabuf_tx_cmsg *)CMSG_DATA(cmsg)) = ddmabuf;

sendmsg(socket_fd, &msg, MSG_ZEROCOPY);

重用 TX dmabuf

与具有常规内存的 MSG_ZEROCOPY 类似,用户不应在发送操作进行时修改 dma-buf 的内容。 这是因为内核不会保留 dmabuf 内容的副本。 相反,内核将固定并从用户空间可用的缓冲区发送数据。

就像 MSG_ZEROCOPY 中一样,内核使用 MSG_ERRQUEUE 通知用户空间发送完成

int64_t tstop = gettimeofday_ms() + waittime_ms;
char control[CMSG_SPACE(100)] = {};
struct sock_extended_err *serr;
struct msghdr msg = {};
struct cmsghdr *cm;
int retries = 10;
__u32 hi, lo;

msg.msg_control = control;
msg.msg_controllen = sizeof(control);

while (gettimeofday_ms() < tstop) {
        if (!do_poll(fd)) continue;

        ret = recvmsg(fd, &msg, MSG_ERRQUEUE);

        for (cm = CMSG_FIRSTHDR(&msg); cm; cm = CMSG_NXTHDR(&msg, cm)) {
                serr = (void *)CMSG_DATA(cm);

                hi = serr->ee_data;
                lo = serr->ee_info;

                fprintf(stdout, "tx complete [%d,%d]\n", lo, hi);
        }
}

在关联的 sendmsg 完成后,用户空间可以重用 dmabuf。

实现和注意事项

不可读的 skb

Devmem 有效负载无法由处理数据包的内核访问。 这导致 devmem skb 的有效负载出现一些怪癖

  • 环回不起作用。 环回依赖于复制有效负载,这对于 devmem skb 是不可能的。

  • 软件校验和计算失败。

  • TCP Dump 和 bpf 无法访问 devmem 数据包有效负载。

测试

可以在内核源代码中的 tools/testing/selftests/drivers/net/hw/ncdevmem.c 中找到更真实的示例代码

ncdevmem 是 devmem TCP netcat。 它的工作方式与 netcat 非常相似,但直接将数据接收到 udmabuf 中。

要运行 ncdevmem,您需要在测试机器上的服务器上运行它,并且需要在对等方上运行 netcat 以提供 TX 数据。

ncdevmem 还有一种验证模式,它期望传入数据的重复模式并对其进行验证。 例如,您可以通过以下方式在服务器上启动 ncdevmem

ncdevmem -s <server IP> -c <client IP> -f <ifname> -l -p 5201 -v 7

在客户端,使用常规 netcat 将 TX 数据发送到服务器上的 ncdevmem 进程

yes $(echo -e \\x01\\x02\\x03\\x04\\x05\\x06) | \
        tr \\n \\0 | head -c 5G | nc <server IP> 5201 -p 5201