BPF 環形緩衝區

本文件描述了 BPF 環形緩衝區的設計、API 和實現細節。

動機

這項工作有兩個獨特的動機,現有的 perf 緩衝區無法滿足,因此促使建立了一個新的環形緩衝區實現。

  • 透過在 CPU 之間共享環形緩衝區,實現更高效的記憶體利用;

  • 保留事件按時間順序發生的順序,即使跨多個 CPU 也是如此(例如,任務的 fork/exec/exit 事件)。

這兩個問題是獨立的,但 perf 緩衝區未能同時滿足兩者。兩者都是由於選擇使用每個 CPU 的 perf 環形緩衝區而導致的。透過環形緩衝區的 MPSC 實現也可以解決這兩個問題。排序問題在技術上可以透過核心內的一些計數來解決 perf 緩衝區,但考慮到第一個問題需要一個 MPSC 緩衝區,同樣的解決方案將自動解決第二個問題。

語義和 API

單個環形緩衝區以 BPF_MAP_TYPE_RINGBUF 型別的 BPF 對映例項的形式呈現給 BPF 程式。另外兩種替代方案被考慮,但最終被拒絕。

一種方法是,類似於 BPF_MAP_TYPE_PERF_EVENT_ARRAY,讓 BPF_MAP_TYPE_RINGBUF 可以表示一個環形緩衝區陣列,但不強制執行“僅限同一 CPU”規則。這將是一個更熟悉的介面,與 BPF 中現有 perf 緩衝區的使用相容,但如果應用程式需要更高階的邏輯來透過任意鍵查詢環形緩衝區,則會失敗。BPF_MAP_TYPE_HASH_OF_MAPS 通過當前方法解決了這個問題。此外,考慮到 BPF 環形緩衝區的效能,許多用例會選擇所有 CPU 共享的簡單單個環形緩衝區,而當前方法對於這些用例來說將是多餘的。

另一種方法是引入一個新概念,與 BPF 對映並行,用於表示通用“容器”物件,該物件不一定具有帶查詢/更新/刪除操作的鍵/值介面。這種方法會增加大量需要為可觀察性和驗證器支援而構建的額外基礎設施。它還會增加 BPF 開發人員需要熟悉的新概念、libbpf 中的新語法等。但那樣的話,與使用對映的方法相比,它確實不會提供任何額外的好處。BPF_MAP_TYPE_RINGBUF 不支援查詢/更新/刪除操作,但其他一些對映型別(例如,佇列和棧;陣列不支援刪除等)也不支援。

所選方法具有以下優點:重用現有 BPF 對映基礎設施(核心中的內省 API、libbpf 支援等),概念熟悉(無需向用戶教授 BPF 程式中的新物件型別),並利用現有工具 (bpftool)。對於所有 CPU 使用單個環形緩衝區的常見場景,它與專用“容器”物件一樣簡單直接。另一方面,作為對映,它可以與 ARRAY_OF_MAPSHASH_OF_MAPS 對映中的對映結合使用,以實現各種拓撲結構,從每個 CPU 一個環形緩衝區(例如,作為 perf 緩衝區用例的替代),到複雜的應用程式環形緩衝區雜湊/分片(例如,擁有一個小型環形緩衝區池,其中雜湊的任務 tgid 作為查詢鍵以保留順序,但減少爭用)。

鍵和值大小被強制為零。max_entries 用於指定環形緩衝區的大小,並且必須是 2 的冪值。

perf 緩衝區 (BPF_MAP_TYPE_PERF_EVENT_ARRAY) 和新的 BPF 環形緩衝區語義之間有許多相似之處:

  • 可變長度記錄;

  • 如果環形緩衝區中沒有更多空間,則保留失敗,不阻塞;

  • 用於使用者空間應用程式的記憶體對映資料區域,以便於使用和實現高效能;

  • 新傳入資料的 epoll 通知;

  • 但在必要時仍能對新資料進行忙輪詢,以實現最低延遲。

BPF ringbuf 為 BPF 程式提供了兩套 API

  • bpf_ringbuf_output() 允許將資料從一個地方複製到環形緩衝區,類似於 bpf_perf_event_output()

  • bpf_ringbuf_reserve()/bpf_ringbuf_commit()/bpf_ringbuf_discard() API 將整個過程分為兩步。首先,保留固定數量的空間。如果成功,則返回一個指向環形緩衝區資料區域內資料的指標,BPF 程式可以像使用陣列/雜湊對映中的資料一樣使用它。一旦準備好,這塊記憶體要麼被提交,要麼被丟棄。丟棄類似於提交,但它使消費者忽略該記錄。

bpf_ringbuf_output() 的缺點是會產生額外的記憶體複製,因為記錄必須首先在其他地方準備好。但它允許提交驗證器事先不知道長度的記錄。它還與 bpf_perf_event_output() 緊密匹配,因此將大大簡化遷移。

bpf_ringbuf_reserve() 透過直接向環形緩衝區記憶體提供記憶體指標來避免額外的記憶體複製。在許多情況下,記錄大於 BPF 堆疊空間所允許的範圍,因此許多程式使用額外的每 CPU 陣列作為臨時堆來準備樣本。bpf_ringbuf_reserve() 完全避免了這種需求。但作為交換,它只允許保留已知常量大小的記憶體,以便驗證器可以驗證 BPF 程式無法訪問其保留記錄空間之外的記憶體。bpf_ringbuf_output() 雖然由於額外的記憶體複製而稍慢,但涵蓋了一些不適合 bpf_ringbuf_reserve() 的用例。

提交和丟棄之間的差異非常小。丟棄只是將記錄標記為已丟棄,消費者程式碼應該忽略此類記錄。丟棄對於一些高階用例很有用,例如確保全有或全無的多記錄提交,或在單個 BPF 程式呼叫中模擬臨時 malloc()/free()

每個保留的記錄都由驗證器透過現有的引用跟蹤邏輯進行跟蹤,類似於套接字引用跟蹤。因此,不可能保留記錄而忘記提交(或丟棄)它。

bpf_ringbuf_query() 輔助函式允許查詢環形緩衝區的各種屬性。目前支援 4 種:

  • BPF_RB_AVAIL_DATA 返回環形緩衝區中未消費的資料量;

  • BPF_RB_RING_SIZE 返回環形緩衝區的大小;

  • BPF_RB_CONS_POS/BPF_RB_PROD_POS 分別返回消費者/生產者的當前邏輯位置。

返回的值是環形緩衝區狀態的瞬時快照,並且在輔助函式返回時可能不準確,因此這應該僅用於除錯/報告目的或用於實現各種啟發式演算法,這些演算法考慮了其中一些特徵的高度可變性。

一種這樣的啟發式演算法可能涉及對環形緩衝區中新資料可用性的 poll/epoll 通知進行更細粒度的控制。與 output/commit/discard 輔助函式的 BPF_RB_NO_WAKEUP/BPF_RB_FORCE_WAKEUP 標誌結合使用,它允許 BPF 程式進行高度控制,例如,更高效的批次通知。然而,預設的自平衡策略應該足以滿足大多數應用程式的需求,並且已經可以可靠高效地工作。

設計與實現

這種預留/提交模式允許多個生產者(無論是位於不同 CPU 上還是甚至在同一 CPU/同一 BPF 程式中)以自然的方式預留獨立的記錄並處理它們,而不會阻塞其他生產者。這意味著如果 BPF 程式被另一個共享同一環形緩衝區的 BPF 程式中斷,它們都將獲得一個預留的記錄(如果空間足夠),並且可以獨立地處理並提交它。這也適用於 NMI 上下文,只是由於在預留期間使用了自旋鎖,在 NMI 上下文中,bpf_ringbuf_reserve() 可能會未能獲取鎖,在這種情況下,即使環形緩衝區未滿,預留也會失敗。

環形緩衝區本身在內部實現為大小為 2 的冪的迴圈緩衝區,帶有兩個邏輯且不斷增加的計數器(在 32 位架構上可能會迴繞,這不是問題)

  • 消費者計數器顯示消費者已消費資料到的邏輯位置;

  • 生產者計數器表示所有生產者預留的資料量。

每次預留記錄時,“擁有”該記錄的生產者將成功推進生產者計數器。然而,此時資料仍未準備好被消費。每條記錄都有一個 8 位元組的頭部,其中包含預留記錄的長度,以及兩個額外的位:一個忙碌位,表示記錄仍在處理中,以及一個丟棄位,如果記錄被丟棄,則在提交時可能會設定此位。在後一種情況下,消費者應該跳過該記錄並移到下一條。記錄頭部還編碼了記錄相對於環形緩衝區資料區域開頭(以頁為單位)的相對偏移量。這使得 bpf_ringbuf_commit()/bpf_ringbuf_discard() 只接受指向記錄本身的指標,而無需同時提供指向環形緩衝區本身的指標。環形緩衝區記憶體位置將從記錄元資料頭部恢復。這顯著簡化了驗證器,並提高了 API 的可用性。

生產者計數器增量在自旋鎖下序列化,因此預留之間存在嚴格的順序。另一方面,提交是完全無鎖且獨立的。所有記錄都按照預留的順序對消費者可用,但僅在所有先前的記錄都已提交之後。因此,慢速生產者可能會暫時阻止稍後預留的已提交記錄。

一個有趣的實現細節,它顯著簡化(也因此加速)了生產者和消費者實現,即資料區域如何在虛擬記憶體中連續兩次背靠背地對映。這使得對於在迴圈緩衝區資料區域末尾需要回繞的樣本,無需採取任何特殊措施,因為最後一個數據頁之後的下一頁將再次是第一個資料頁,因此樣本在虛擬記憶體中仍將顯得完全連續。請參閱 bpf_ringbuf_area_alloc() 中直觀顯示此內容的註釋和簡單的 ASCII 圖。

BPF 環形緩衝區與 perf 環形緩衝區的另一個區別是新資料可用性的自適應通知。bpf_ringbuf_commit() 實現只有在消費者已經趕上到正在提交的記錄時,才會在提交後傳送新記錄可用的通知。如果不是,消費者仍然需要趕上,因此無論如何都會看到新資料,而無需額外的輪詢通知。基準測試(參見 tools/testing/selftests/bpf/benchs/bench_ringbufs.c)表明,這允許實現非常高的吞吐量,而無需訴諸於諸如“僅每 N 個樣本通知一次”之類的技巧,這些技巧在 perf 緩衝區中是必需的。對於極端情況,當 BPF 程式需要更多手動控制通知時,commit/discard/output 輔助函式接受 BPF_RB_NO_WAKEUPBPF_RB_FORCE_WAKEUP 標誌,這些標誌提供對資料可用性通知的完全控制,但使用此 API 需要格外小心和謹慎。