RFC-0267:IOBuffer 環狀緩衝區紀律 | |
---|---|
狀態 | 已接受 |
區域 |
|
說明 | IOBuffers 的環狀緩衝區規則。 |
問題 | |
Gerrit 變更 | |
作者 | |
審查人員 | |
提交日期 (年-月-日) | 2025-01-07 |
審查日期 (年-月-日) | 2025-02-20 |
問題陳述
我們希望能提供更有效率的記錄機制,以便改善記錄系統的 CPU 使用率,並改善排程壓力。記憶體使用量也是一個問題,但不是本 RFC 的主要重點。
摘要
引入 I/O 緩衝區的主要用途之一,是為了支援更有效率的記錄功能。這份 RFC 會引入新的環狀緩衝區規則,目的是改善記錄系統的 CPU 使用率和排程壓力。
利害關係人
講師:jamesr@google.com
審查者:adanis@google.com、eieio@google.com、gmtr@google.com、miguelfrde@google.com
諮詢對象:核心和診斷團隊
社會化:
在本 RFC 之前,我們已與核心和診斷團隊討論過這項提案。
需求條件
- Archivist 必須知道記錄元件的身分。
- 必須先支援記錄功能,Archivist 才能執行。
- 這項 RFC 不應大幅增加記憶體總用量。
背景
Logging 用戶端目前使用 fuchsia.logger.LogSink
:
strict ConnectStructured(resource struct {
socket zx.Handle:SOCKET;
});
記錄訊息會以資料包格式訊息傳送至 Socket。Archivist 服務會使用許多通訊端。針對每個 Socket,它會讀取訊息並將其寫入環形緩衝區:
- 用戶端寫入通訊端。
- 核心會將訊息複製到通訊端緩衝區。
- Archivist 喚醒。
- Archivist 會將核心 Socket 緩衝區的訊息複製到環狀緩衝區。
設計
實作這項 RFC 後,寫入記錄的動作會變成:
- 用戶端會對 IO 緩衝區執行中介寫入作業。
- 核心會將訊息複製到 I/O 緩衝區區域。
我們將新增新的環狀緩衝區規則:
#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 瞭解用戶端的身份 (可儲存從標記到元件身分的對應)。
使用此分類的區域的第一頁會包含標題,其中包含:
uint64_t head;
uint64_t tail;
區域的其餘部分 (從第二頁開始) 會用作環形緩衝區,也就是說,區域必須有至少兩個頁面,且環形緩衝區的大小為 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 位元組的倍數。
head
和 tail
指標會以原子方式更新,並設有適當的屏障。一開始,我們會透過在核心內使用鎖定機制來支援並行寫入作業,因為我們只支援經過調解的寫入作業。head
和 tail
指標只會遞增;由於它們是 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);
您可以使用這個共用區域建立許多 I/O 緩衝區。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 就能知道何時有新訊息寫入共用區域 (如果需要,例如有下游記錄檔讀取器)。
Archivist 可監控記錄用戶何時離開,方法是在每個用戶端的端點上監控信號 ZX_IOB_PEER_CLOSED
。
讀取門檻
與 ZX_SOCKET_READ_THRESHOLD
類似,您可以設定屬性 ZX_IOB_SHARED_REGION_READ_THRESHOLD
,讓系統在寫入後超過閾值時,以頻閃方式傳送 ZX_IOB_SHARED_REGION_READ_THRESHOLD
信號。請注意,信號只能閃爍,不能斷言,因為核心無法得知何時要斷言信號。
記憶體順序和 Writer 同步
以下是原子作業的最低記憶體順序要求:
初始化
首次初始化環狀緩衝區時,不需要進行同步處理。環緩衝區一開始會將所有標頭欄位設為零。不需要重新啟動支援。
寫入
並行寫入作業會使用核心中的鎖定機制進行同步處理。
- 使用放寬語意讀取頭部,並使用取得語意讀取尾部。
- 執行驗證和空間檢查。
- 使用一般寫入作業寫入訊息。這會以機會主義副本的形式執行。如果發生錯誤,系統會放棄鎖定,處理錯誤,然後從第一個步驟重試作業。
- 使用版本語意增加頭部。
如果不當的用戶端將訊息的記憶體來源設為不當的分頁器來源,則可能會導致所有用戶端的寫入作業停滯。請參閱下方的「不當行為的用戶端」一節。
閱讀
- 使用放寬語意讀取尾端,並使用取得語意讀取頭端。
- 對首尾執行檢查。發生錯誤或沒有訊息時會失敗。
- 以一般讀取方式朗讀訊息。
- 使用版本語意增加尾端。
用戶端緩衝
目前部分元件會在 Archivist 執行前啟動。如果 Archivist 負責建立 IOBuffer 組,為了避免發生死結,用戶端端需要將記錄緩衝,直到收到 IOBuffer 為止。這可能會影響在 Archivist 啟動前啟動及停止的短效元件 (因為如果不加以處理,記錄訊息可能會遭到捨棄)。
或者,元件管理服務需要負責建立 IOBuffer 物件。
您必須採用這兩種方法之一才能採用此 RFC,但選擇哪一種方法不在本 RFC 的範圍內。
這項假設是指我們會繼續使用單一常見環形緩衝區,就像目前的做法一樣,但我們可以考慮使用多個環形緩衝區,這些緩衝區是由不同的元件建立及使用 (例如,用於解決隔離、配置和歸因問題)。這超出本 RFC 的討論範圍,但我們想強調的是,這類解決方案不應受到禁止。
惡意客戶
行為不當的用戶端可能會寫入大量記錄,導致其他用戶端中斷,例如,從用戶端拒絕提供備援的記憶體中寫入記錄。
這些問題在某種程度上已經存在:元件可以傳送大量記錄,導致其他記錄遭到捨棄,而且會佔用 CPU 和記憶體,導致系統其他部分效能降低。
我們建議以目前的方式處理這些問題:視為需要修正的錯誤,或使用者必須終止的惡意應用程式 (如果產品允許這麼做)。
實作
zx_iob_writev
需要穩定下來,我們才能使用此 RFC 的任何部分進行記錄。這是用戶端需要使用的唯一新 API,因此所有其他建議的變更一開始都會是 Zircon 核心的 NEXT vdso 的一部分 (可供 Archivist 使用)。這樣一來,我們就能在必要時重複設計。
為了支援這項變更,需要變更記錄通訊協定,但這不在本 RFC 的範圍內。
成效
這應該可提高記錄效率:訊息可直接寫入環緩衝區,而無須喚醒 Archivist。系統會監控現有的 Archivist 和系統基準。
安全性考量
無
隱私權注意事項
無
測試
我們會新增單元測試。Archivist 提供適當的整合和端對端測試。
說明文件
核心系統呼叫說明文件將會更新。
未來的可能性
我們可能需要考慮支援阻斷寫入作業。我們認為目前不需要這麼做,但如果需要,我們可以提供
zx_iob_write
的選項,讓您進行寫入封鎖。目前的記錄格式包含用戶端產生的時間戳記。我們可以新增選項,讓核心產生時間戳記,讓接收端信任時間戳記,但代價是犧牲部分準確度。您也可以保證某些排序,但這可能會進一步降低準確度 (因為鎖定競爭和需要在寫入器中重試)。
為瞭解決隔離問題,我們可以考慮為不同的子系統支援多個環狀緩衝區。
我們日後可能會新增選項,支援核心產生的 ID,例如程序 ID、執行緒 ID,或不會將這類詳細資料洩漏給無權使用者的類似項目。
缺點、替代方案和未知因素
在本 RFC 之前,我們討論了許多替代方案,包括:
我們可以使用單一 IO 緩衝區,搭配新的標記寫入器核心物件,也就是多對一安排。這麼做有一定的優點,但我們不採用這項做法,因為這會導致追蹤代碼的配置方式變得複雜。Archivist 需要知道記錄器何時消失:這個 RFC 中的提案會依賴核心現有的對等分散處理器模型,而具有標記寫入器的單一 IO 緩衝區則需要其他方式。
我們可以從呼叫執行緒的屬性中衍生標記。標記會從程序和工作繼承。這個方法已遭到淘汰,因為執行程式不一定需要在個別執行緒中執行元件。
我們曾考慮使用串流的做法,但因擔心鎖定爭用問題而放棄,因為串流會在分頁記憶體中運作,而 IO 緩衝區則可在已釘選的記憶體中運作。
我們可以將標記與句柄建立關聯,這表示我們可以使用單一 IO 緩衝區,然後將不同的句柄傳遞至不同的寫入器。這需要大幅重構 Zircon 核心才能支援,但 API 的品質可能會因此變差。
我們可以讓核心使用 IOBuffer koid 做為標記。這會使 Archivist 需要進行的追蹤變得複雜,因為它需要追蹤每個 IOBuffer 的訊息計數,而不是每個元件的訊息計數。