MSG_ZEROCOPY¶
簡介¶
MSG_ZEROCOPY 標誌允許對套接字傳送呼叫避免複製。 該功能當前已為 TCP、UDP 和 VSOCK(使用 virtio 傳輸)套接字實現。
機遇和注意事項¶
在使用者程序和核心之間複製大型緩衝區可能代價高昂。 Linux 支援各種避免複製的介面,例如 sendfile 和 splice。 MSG_ZEROCOPY 標誌將底層複製避免機制擴充套件到常見的套接字傳送呼叫。
避免複製不是免費的午餐。 按照目前的實現,使用頁面鎖定,它將每個位元組的複製成本替換為頁面記賬和完成通知開銷。 因此,MSG_ZEROCOPY 通常僅在超過 10 KB 的寫入時有效。
頁面鎖定還會更改系統呼叫語義。 它暫時在程序和網路堆疊之間共享緩衝區。 與複製不同,程序不能在系統呼叫返回後立即覆蓋緩衝區,否則可能會修改傳輸中的資料。 核心完整性不受影響,但有缺陷的程式可能會損壞其自身的資料流。
核心會在可以安全地修改資料時返回通知。 然後,將現有應用程式轉換為 MSG_ZEROCOPY 並不總是像傳遞標誌那麼簡單。
更多資訊¶
本文件的大部分內容來自 netdev 2.1 上發表的一篇較長的論文。 有關更深入的資訊,請參閱該論文和演講,LWN.net 上出色的報告或閱讀原始程式碼。
- 論文、幻燈片、影片
- LWN 文章
- 補丁集
[PATCH net-next v4 0/9] socket sendmsg MSG_ZEROCOPY https://lore.kernel.org/netdev/20170803202945.70750-1-willemdebruijn.kernel@gmail.com
介面¶
傳遞 MSG_ZEROCOPY 標誌是啟用複製避免的最明顯的步驟,但不是唯一的步驟。
套接字設定¶
當應用程式將未定義的標誌傳遞給傳送系統呼叫時,核心是允許的。 預設情況下,它只是忽略這些。 為了避免為意外已經傳遞此標誌的舊程序啟用複製避免模式,程序必須首先透過設定套接字選項來發出意圖訊號
if (setsockopt(fd, SOL_SOCKET, SO_ZEROCOPY, &one, sizeof(one)))
error(1, errno, "setsockopt zerocopy");
傳輸¶
傳送(或 sendto、sendmsg、sendmmsg)本身的更改是微不足道的。 傳遞新標誌。
ret = send(fd, buf, sizeof(buf), MSG_ZEROCOPY);
如果套接字超出其 optmem 限制或使用者超出其鎖定頁面的 ulimit,則 zerocopy 失敗將返回 -1,錯誤程式碼為 ENOBUFS。
混合避免複製和複製¶
許多工作負載混合了大型和小型緩衝區。 因為對於小資料包,避免複製比複製更昂貴,所以該功能實現為一個標誌。 可以安全地混合使用帶有標誌和不帶標誌的呼叫。
通知¶
核心必須在可以安全地重用先前傳遞的緩衝區時通知程序。 它在套接字錯誤佇列上排隊完成通知,類似於傳輸時間戳介面。
通知本身是一個簡單的標量值。 每個套接字維護一個內部無符號 32 位計數器。 每次使用 MSG_ZEROCOPY 成功傳送資料的傳送呼叫都會增加計數器。 如果呼叫失敗或長度為零,則計數器不會遞增。 計數器計算系統呼叫呼叫,而不是位元組。 它在 UINT_MAX 呼叫後迴繞。
通知接收¶
下面的程式碼片段演示了 API。 在最簡單的情況下,每個傳送系統呼叫之後是輪詢和接收錯誤佇列上的 recvmsg。
從錯誤佇列讀取始終是非阻塞操作。 輪詢呼叫用於阻塞,直到出現錯誤。 它將在其輸出標誌中設定 POLLERR。 該標誌不必在 events 欄位中設定。 錯誤是無條件地發出訊號。
pfd.fd = fd;
pfd.events = 0;
if (poll(&pfd, 1, -1) != 1 || pfd.revents & POLLERR == 0)
error(1, errno, "poll");
ret = recvmsg(fd, &msg, MSG_ERRQUEUE);
if (ret == -1)
error(1, errno, "recvmsg");
read_notification(msg);
該示例僅用於演示目的。 實際上,不等待通知,而是每隔幾次傳送呼叫進行非阻塞讀取更有效。
通知可以與其他套接字上的操作亂序處理。 具有排隊錯誤的套接字通常會阻止其他操作,直到讀取錯誤。 但是,Zerocopy 通知具有零錯誤程式碼,以不阻止傳送和接收呼叫。
通知批處理¶
可以使用 recvmmsg 呼叫一次讀取多個未完成的資料包。 這通常不是必需的。 在每個訊息中,核心返回的不是單個值,而是一個範圍。 它在錯誤佇列上合併連續的通知,同時為一個通知正在接收。
當要排隊新的通知時,它會檢查新值是否擴充套件了佇列尾部通知的範圍。 如果是這樣,它會刪除新的通知資料包,而是增加未完成通知的範圍上限值。
對於按順序確認資料的協議(如 TCP),每個通知都可以壓縮到前一個通知中,因此在任何一個點都不會有多個通知未完成。
按順序傳遞是常見情況,但不能保證。 通知可能會在重傳和套接字拆卸時亂序到達。
通知解析¶
下面的程式碼片段演示瞭如何解析控制訊息:上一個程式碼片段中的 read_notification() 呼叫。 通知以標準錯誤格式 sock_extended_err 編碼。
控制資料中的 level 和 type 欄位是協議族特定的,IP_RECVERR 或 IPV6_RECVERR(對於 TCP 或 UDP 套接字)。 對於 VSOCK 套接字,cmsg_level 將是 SOL_VSOCK,cmsg_type 將是 VSOCK_RECVERR。
錯誤來源是新型別 SO_EE_ORIGIN_ZEROCOPY。 ee_errno 為零,如前所述,以避免阻止套接字上的讀取和寫入系統呼叫。
32 位通知範圍編碼為 [ee_info, ee_data]。 此範圍是包含的。 結構中的其他欄位必須被視為未定義,ee_code 除外,如下所述。
struct sock_extended_err *serr;
struct cmsghdr *cm;
cm = CMSG_FIRSTHDR(msg);
if (cm->cmsg_level != SOL_IP &&
cm->cmsg_type != IP_RECVERR)
error(1, 0, "cmsg");
serr = (void *) CMSG_DATA(cm);
if (serr->ee_errno != 0 ||
serr->ee_origin != SO_EE_ORIGIN_ZEROCOPY)
error(1, 0, "serr");
printf("completed: %u..%u\n", serr->ee_info, serr->ee_data);
延遲複製¶
傳遞標誌 MSG_ZEROCOPY 是核心應用複製避免的提示,以及核心將排隊完成通知的合同。 它不能保證複製被省略。
避免複製並非總是可行的。 不支援分散-聚集 I/O 的裝置無法傳送由核心生成的協議頭加上 zerocopy 使用者資料組成的資料包。 可能需要將資料包轉換為堆疊深處的私有資料副本,例如計算校驗和。
在所有這些情況下,核心會在釋放對共享頁面的控制時返回完成通知。 該通知可能會在(複製的)資料完全傳輸之前到達。 因此,zerocopy 完成通知不是傳輸完成通知。
如果資料在快取中不再是熱資料,則延遲複製可能比系統呼叫中立即進行的複製更昂貴。 該過程還會產生通知處理成本,而沒有任何好處。 因此,核心會透過在返回時在欄位 ee_code 中設定標誌 SO_EE_CODE_ZEROCOPY_COPIED 來指示資料是否已透過複製完成。 程序可以使用此訊號來停止在同一套接字的後續請求上傳遞標誌 MSG_ZEROCOPY。
實現¶
環回¶
對於 TCP 和 UDP:如果接收程序不讀取其套接字,則傳送到本地套接字的資料可以無限期地排隊。 無界通知延遲是不可接受的。 因此,所有使用 MSG_ZEROCOPY 生成並迴圈到本地套接字的資料包都將產生延遲複製。 這包括迴圈到資料包套接字(例如,tcpdump)和 tun 裝置。
對於 VSOCK:傳送到本地套接字的資料路徑與非本地套接字相同。
測試¶
更真實的示例程式碼可以在核心原始碼的 tools/testing/selftests/net/msg_zerocopy.c 中找到。
請注意環回約束。 測試可以在一對主機之間執行。 但是,如果在本地一對程序之間執行,例如在使用 msg_zerocopy.sh 在名稱空間之間的一對 veth 之間執行時,測試將不會顯示任何改進。 對於測試,可以透過使 skb_orphan_frags_rx 與 skb_orphan_frags 相同來暫時放寬環回限制。
對於 VSOCK 型別的套接字,示例可以在 tools/testing/vsock/vsock_test_zerocopy.c 中找到。