RFC-0267:IOBuffer 環狀緩衝區紀律

RFC-0267:IOBuffer Ring Buffer Discipline
狀態已接受
區域
  • 診斷
說明

IOBuffer 的環形緩衝區規範。

問題
Gerrit 變更
作者
審查人員
提交日期 (年-月-日)2025-01-07
審查日期 (年-月-日)2025-02-20

問題陳述

我們希望採用更有效率的記錄機制,以改善記錄系統的 CPU 使用率,並減輕排程壓力。記憶體用量也是一個問題,但並非本 RFC 的主要重點。

摘要

導入 IO 緩衝區的主要用途之一是支援更有效率的記錄。這項 RFC 導入新的環狀緩衝區規範,目的是改善記錄系統的 CPU 使用率和排程壓力。

利害關係人

講師:jamesr@google.com

審查人員:adanis@google.com、eieio@google.com、gmtr@google.com、 miguelfrde@google.com

諮詢對象:核心和診斷團隊

社交:

這項提案已在 RFC 之前與核心和診斷團隊討論過。

需求條件

  • 封存者必須知道記錄元件的身分。
  • 在 Archivist 執行前,必須先支援記錄功能。
  • 這項 RFC 不應大幅增加記憶體總用量。

背景

記錄用戶端目前使用 fuchsia.logger.LogSink

    strict ConnectStructured(resource struct {
        socket zx.Handle:SOCKET;
    });

記錄訊息會以資料包訊息的形式,透過通訊端傳送。Archivist 服務 許多通訊端。針對每個通訊端,讀取訊息並寫入環形緩衝區:

  1. 用戶端寫入通訊端。
  2. 核心會將訊息複製到通訊端緩衝區。
  3. 檔案管理員醒來。
  4. 封存者會將核心插座緩衝區中的訊息複製到環形緩衝區。

設計

實作這項 RFC 後,記錄檔的寫入方式會變成:

  1. 用戶端對 IO 緩衝區執行中介寫入作業。
  2. 核心會將訊息複製到 IO 緩衝區區域。

系統將新增環形緩衝區規範:

#define ZX_IOB_DISCIPLINE_TYPE_MEDIATED_WRITER_RING_BUFFER ((zx_iob_discipline_type_t)(2u))
typedef struct zx_iob_discipline_mediated_writer_ring_buffer {
  uint64_t tag;
  uint64_t reserved[7];
} zx_iob_discipline_mediated_writer_ring_buffer_t;

這項機制一開始會支援並行核心調解寫入作業,以及單一使用者空間讀取器。系統不會支援使用者空間寫入和核心仲介讀取。這項學科一開始只能用於共用區域 (詳見下文)。保留欄位日後可能會用於設定行為,例如啟用或停用使用者空間寫入。

這個規範可指定核心將寫入環狀緩衝區的標記,並讓 Archivist 瞭解用戶端的 ID (可儲存從標記到元件 ID 的對應)。

使用這項學科的區域第一頁會包含標題,內含:

uint64_t head;
uint64_t tail;

其餘區域 (從第二頁開始) 會做為環形緩衝區,也就是說,這個區域至少要有兩頁,且環形緩衝區的大小必須是 2 的乘方。在頁面界線啟動環狀緩衝區,可讓用戶端在結尾對應環狀緩衝區的開頭,方便處理包裝作業。此外,這項功能也允許對頭尾值使用模數 2 算術,理論上效能會比其他方式更高。最後,如果日後需要額外中繼資料,這個結構也提供成長空間。缺點是會浪費一些空間。

系統會透過新的系統呼叫支援核心中介寫入:

// Performs a mediated write to the specified IO Buffer region.
//
// The maximum size of the data to be written is 65,535 bytes.
//
// |options| is reserved for future use and must be zero.
// |region_index| specifies which region to write to. It must use a discipline
//     that supports mediated writes.
//
// * Errors
//
// `ZX_ERR_ACCESS_DENIED`: Input handle does not have sufficient rights or |data| is
//     not readable.
// `ZX_ERR_BAD_STATE`: The ring buffer is in an invalid state (e.g. tail > head).
// `ZX_ERR_BAD_TYPE`: Input handle is not an IO Buffer.
// `ZX_ERR_INVALID_ARGS`: |options| or |region_index| are invalid, or there is an
//     attempt to write more than 65,535 bytes.
// `ZX_ERR_NO_SPACE`: There is no space in the ring buffer.
zx_status_t zx_iob_writev(
    zx_handle_t iob_handle, uint64_t options, uint32_t region_index,
    const zx_iovec_t* vectors,  size_t num_vectors);

這個系統呼叫一開始只適用於新的環狀緩衝區規範。所有寫入環形緩衝區的內容都會包含 8 位元組的標頭,其中包含 IO 緩衝區的標記 (8 位元組),後方則為 8 位元組的長度,表示後續的位元組數。所有寫入作業都會經過填補,以維持 8 位元組對齊,但長度不一定要是 8 位元組的倍數。

headtail 指標會以適當的障礙原子方式更新。一開始,系統會使用核心內的鎖定功能支援並行寫入,這是因為系統只支援中介寫入。headtail 指標只會遞增,由於指標為 64 位元,因此在我們的生命週期內不會包裝。環形緩衝區偏移量會使用模數算術決定。

只有核心會遞增 head。只有使用者空間會遞增 tail。 使用者空間會負責在環狀緩衝區內維持足夠空間。如果空間不足,zx_iob_writev 會失敗並顯示 ZX_ERR_NO_SPACE。對 Archivist 而言,這項變更的影響是環形緩衝區必須比目前更大,因為 Archivist 必須維持足夠的可用空間,才能盡量避免捨棄記錄。

每個記錄用戶端都需要自己的 IO 緩衝區,並使用新的環形緩衝區規範共用區域。為支援這項功能,系統會導入新的 Kernel 物件來代表共用區域:

// Creates a shared region that can be used with an IO Buffer.
//
// |size| is the size of the shared region.
// |options| is reserved for future use and must be zero.
//
// * Errors
//
// `ZX_ERR_INVALID_ARGS`: The size is not a multiple of the page size.
zx_status_t zx_iob_create_shared_region(uint64_t options, uint64_t size, zx_handle_t* out);

您可以使用這個共用區域建立許多 IO 緩衝區。zx_iob_region_t將延長:

#define ZX_IOB_REGION_TYPE_SHARED ((zx_iob_region_type_t)(1u))

// If the type is ZX_IOB_REGION_TYPE_SHARED, the size comes from the shared region and
// |size| must be zero.
struct zx_iob_region_t {
  uint32_t type;
  uint32_t access;
  uint64_t size;
  zx_iob_discipline_t discipline;
  union {
    zx_iob_region_private_t private_region;
    zx_iob_region_shared_t shared_region;
    uint8_t max_extension[4 * 8];
  };
};

// |options| is reserved for future use and must be zero.
// |shared_region| is a handle (not consumed) referencing a shared region created with
// |zx_iob_create_shared_region|. |padding| must be zeroed.
struct zx_iob_shared_region_t {
  uint32_t options;
  zx_handle_t shared_region;
  uint64_t padding[3];
};

成功寫入環形緩衝區後,共用區域的 ZX_IOB_SHARED_REGION_UPDATED 信號會閃爍。這可讓 Archivist 瞭解何時有新訊息寫入共用區域 (例如需要有下游記錄讀取器時)。

封存管理員可以監控每個用戶端端點的信號,瞭解用戶端記錄何時消失。ZX_IOB_PEER_CLOSED

讀取門檻

ZX_SOCKET_READ_THRESHOLD 類似,系統會提供可設定的 ZX_IOB_SHARED_REGION_READ_THRESHOLD 屬性,在寫入後超過閾值時,導致 ZX_IOB_SHARED_REGION_READ_THRESHOLD 信號閃爍。請注意,信號只能閃爍,無法斷言,因為核心無法得知何時要取消斷言信號。

記憶體順序和寫入器同步

以下是原子作業的最低記憶體順序需求:

初始化

首次初始化環形緩衝區時,不需要同步處理。環形緩衝區一開始會將所有標頭欄位歸零。不需要支援重新初始化。

寫入

系統會使用核心中的鎖定機制,同步處理並行寫入作業。

  1. 以寬鬆語意讀取標頭,並以取得語意讀取尾部。
  2. 執行驗證和空間檢查。
  3. 使用一般寫入作業撰寫訊息。這項作業會以機會性複製的形式執行。如果發生錯誤,系統會捨棄鎖定、處理錯誤,然後從第一個步驟重試作業。
  4. 使用發布語意遞增 HEAD。

如果行為不當的用戶端將訊息的記憶體來源設為行為不當的呼叫器來源,可能會導致所有用戶端的寫入作業停滯。請參閱下方的「行為不當的用戶端」一節。

閱讀

  1. 以寬鬆語意讀取尾部,並以取得語意讀取頭部。
  2. 檢查頭部和尾部。如果發生錯誤或沒有訊息,就會失敗。
  3. 以一般朗讀方式讀取訊息。
  4. 使用發布語意遞增尾碼。

用戶端緩衝

目前部分元件會在 Archivist 執行前啟動。如果 Archivist 負責建立 IOBuffer 配對,為避免死結,用戶端需要緩衝處理記錄,直到收到 IOBuffer 為止。這可能會對在 Archivist 啟動前啟動及停止的短期元件造成影響 (因為如果沒有妥善處理,記錄訊息可能會遭到捨棄)。

或者,元件管理服務必須負責建立 IOBuffer 物件。

如要採用這項 RFC,必須採取這兩種方法之一,但選擇哪種方法不在本 RFC 的範圍內。

這項假設的前提是我們繼續使用單一的通用環形緩衝區 (目前就是這樣),但我們可能會考慮使用由不同元件建立及使用的多個環形緩衝區 (例如解決隔離、分配和歸因問題)。本 RFC 不會討論這類解決方案,但會說明不應禁止這類解決方案。

行為不當的用戶端

如果用戶端行為不當,可能會寫入大量記錄,導致其他記錄遭到捨棄,進而干擾其他用戶端。舉例來說,用戶端可能會從拒絕提供備份的頁面支援記憶體寫入資料。

在某種程度上,這些問題早已存在:元件可以傳送大量記錄,導致其他記錄遭到捨棄,而且元件可能會占用大量 CPU 和記憶體,導致系統其他部分效能降低。

我們建議繼續以現行方式處理這些問題:將其視為需要修正的錯誤,或是使用者必須終止的錯誤應用程式 (產品允許這麼做)。

實作

zx_iob_writev必須先穩定,我們才能使用這份 RFC 的任何部分進行記錄。這是用戶端唯一需要使用的新 API,因此所有其他提議的變更最初都會納入 Zircon 核心的 NEXT vdso (可供 Archivist 使用)。這樣一來,我們就能視需要疊代設計。

為支援這項變更,記錄通訊協定必須進行變更,但這些變更不在本 RFC 的範圍內。

效能

這應該能提升記錄效率:訊息可直接寫入環形緩衝區,不必喚醒 Archivist。系統會監控現有的 Archivist 和系統基準。

安全性考量

不適用

隱私權注意事項

不適用

測試

我們會新增單元測試。封存工具具備適當的整合功能和端對端測試。

說明文件

核心系統呼叫說明文件將會更新。

未來發展

  1. 我們可能需要支援封鎖寫入作業。我們認為目前不需要這麼做,但如果需要,我們可以提供選項,讓zx_iob_write進行寫入封鎖。

  2. 記錄格式目前包含用戶端產生的時間戳記。我們可以新增選項,讓核心產生時間戳記,這樣接收者就能信任時間戳記,但準確度會稍微降低。此外,您或許也能保證某種排序方式,但這可能會進一步降低準確度 (因為鎖定爭用,且需要在寫入器中重試)。

  3. 為解決隔離問題,我們可以考慮支援不同子系統的多個環形緩衝區。

  4. 我們可能會在日後新增選項,支援核心產生的 ID,例如程序 ID、執行緒 ID,或不會向無權存取的使用者洩漏這類詳細資料的同等項目。

缺點、替代方案和未知事項

在 RFC 之前,我們討論了許多替代方案,包括:

  1. 我們可能會使用單一 IO 緩衝區,搭配新的標記寫入器核心物件,也就是多對一的配置。這項做法有幾個優點,但由於會導致追蹤代碼的架構變得複雜,因此不建議使用。封存管理員需要知道記錄器何時會消失:本 RFC 中的提案依賴核心現有的對等調度器模型,而具有標記寫入器的單一 IO 緩衝區則需要其他模型。

  2. 我們可以從呼叫執行緒的屬性衍生標記。標記會從程序和工作繼承。由於執行器不一定需要以個別執行緒執行元件,因此這個方法已遭淘汰。

  3. 我們曾考慮使用串流,但由於串流會在分頁記憶體上運作,而 IO 緩衝區可在釘選記憶體上運作,因此我們擔心鎖定爭用問題,最後決定不採用串流。

  4. 我們可以將標記與控制代碼建立關聯,這表示我們可以只使用單一 IO 緩衝區,然後將不同的控制代碼傳遞給不同的寫入器。這需要大幅重組 Zircon 核心才能支援,但 API 可能會更糟。

  5. 我們可以讓核心使用 IOBuffer koid 做為標記。這會使 Archivist 需要執行的追蹤作業變得複雜,因為 Archivist 需要追蹤每個 IOBuffer 中的訊息數量,而不是每個元件中的訊息數量。