裝置記憶體 TCP¶
簡介¶
裝置記憶體 TCP (devmem TCP) 允許直接將資料接收到裝置記憶體 (dmabuf) 中。 該特性當前針對 TCP 套接字實現。
機會¶
大量資料傳輸以裝置記憶體作為源和/或目標。 加速器極大地增加了此類傳輸的普遍性。 一些例子包括
分散式訓練,其中 ML 加速器(例如不同主機上的 GPU)交換資料。
分散式原始塊儲存應用程式與遠端 SSD 傳輸大量資料。 大部分資料不需要主機處理。
通常,網路中的裝置到裝置資料傳輸被實現為以下底層操作:裝置到主機複製、主機到主機網路傳輸以及主機到裝置複製。
涉及主機複製的流程是次優的,特別是對於批次資料傳輸,並且會對系統資源(例如主機記憶體頻寬和 PCIe 頻寬)造成重大壓力。
Devmem TCP 透過實現套接字 API 來最佳化此用例,該 API 使使用者能夠直接將傳入的網路資料包接收到裝置記憶體中。
資料包有效負載直接從 NIC 進入裝置記憶體。
資料包標頭進入主機記憶體並由 TCP/IP 協議棧正常處理。 NIC 必須支援標頭拆分才能實現此目的。
優點
與現有的網路傳輸 + 裝置複製語義相比,減輕了主機記憶體頻寬壓力。
透過將資料傳輸限制到 PCIe 樹的最低級別,與透過根聯合體傳送資料的傳統路徑相比,減輕了 PCIe 頻寬壓力。
更多資訊¶
- 幻燈片,影片
https://netdevconf.org/0x17/sessions/talk/device-memory-tcp.html
- 補丁集
[PATCH net-next v24 00/13] 裝置記憶體 TCP https://lore.kernel.org/netdev/20240831004313.3713467-1-almasrymina@google.com/
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 個片段並提前返回。
核心返回實際釋放的片段數。 在以下情況下,釋放的片段數可能少於使用者提供的令牌數:
內部核心洩漏錯誤。
使用者傳遞的片段超過 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