MSG_ZEROCOPY

簡介

MSG_ZEROCOPY 標誌允許對套接字傳送呼叫避免複製。 該功能當前已為 TCP、UDP 和 VSOCK(使用 virtio 傳輸)套接字實現。

機遇和注意事項

在使用者程序和核心之間複製大型緩衝區可能代價高昂。 Linux 支援各種避免複製的介面,例如 sendfile 和 splice。 MSG_ZEROCOPY 標誌將底層複製避免機制擴充套件到常見的套接字傳送呼叫。

避免複製不是免費的午餐。 按照目前的實現,使用頁面鎖定,它將每個位元組的複製成本替換為頁面記賬和完成通知開銷。 因此,MSG_ZEROCOPY 通常僅在超過 10 KB 的寫入時有效。

頁面鎖定還會更改系統呼叫語義。 它暫時在程序和網路堆疊之間共享緩衝區。 與複製不同,程序不能在系統呼叫返回後立即覆蓋緩衝區,否則可能會修改傳輸中的資料。 核心完整性不受影響,但有缺陷的程式可能會損壞其自身的資料流。

核心會在可以安全地修改資料時返回通知。 然後,將現有應用程式轉換為 MSG_ZEROCOPY 並不總是像傳遞標誌那麼簡單。

更多資訊

本文件的大部分內容來自 netdev 2.1 上發表的一篇較長的論文。 有關更深入的資訊,請參閱該論文和演講,LWN.net 上出色的報告或閱讀原始程式碼。

介面

傳遞 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 中找到。