檢查 VMO 檔案格式

本文說明元件檢查檔案格式 (檢查格式)。

使用檢查格式設定格式的檔案稱為「檢查檔案」,通常會使用 .inspect 副檔名。

如要瞭解如何變更格式,請參閱「擴充檢查檔案格式

總覽

元件檢查可讓元件在執行階段公開其狀態的結構化階層資訊。

元件會使用檢查格式主控已對應的虛擬記憶體物件 (VMO),以便公開包含此內部狀態的檢查階層

檢查階層由巢狀的「節點」組成,其中包含類型化的「屬性」

目標

本文件所述的檢查格式有以下目標:

  • 對資料進行低負載突變

    檢查檔案格式可讓您在原地變更資料。舉例來說,整數遞增的額外負擔約為 2 個原子遞增。

  • 支援非靜態階層

    您可以在執行階段修改檢查檔案中儲存的階層。您隨時可以新增或移除階層中的子項。如此一來,階層就能精確呈現元件工作集合中的物件階層。

  • 單一寫入器、多個讀取器並行,且不需明確同步

    與寫入器同時運作的讀取器會對 VMO 進行對應,並嘗試擷取資料快照。寫入者會透過產生計數器表示自己位於關鍵區塊中,但不需要與讀者進行明確的同步處理。讀取器會使用世代計數器,判斷 VMO 快照是否一致,且可安全讀取。

  • 元件終止後,資料可能仍可供使用

    即使寫入元件已終止,讀取器仍可保留包含檢查資料的 VMO 句柄。

術語

本節定義本文件中使用的常見術語。

  • 檢查檔案:使用本文件所述格式的有界序列位元組。
  • 檢查 VMO:儲存在虛擬記憶體物件 (VMO) 中的檢查檔案。
  • 區塊:檢查檔案的大小區段。區塊有索引和順序。
  • Index - 特定區塊的專屬 ID。byte_offset = index * 16
  • 順序:區塊的大小,以位元位移方式從最小大小給出。size_in_bytes = 16 << order。根據區塊的大小 (二的冪),將區塊分割為不同的類別。
  • 節點:階層中可嵌套其他值的命名值。只有節點可在階層中設為父項。
  • 屬性:包含類型資料 (例如字串、整數等) 的命名值。
  • 階層 - 節點樹狀圖,從單一「根」節點開始往下,每個節點可能都包含屬性。檢查檔案包含單一階層。

本文件使用 RFC 2119 中定義的「MUST」、「SHOULD/RECOMMENDED」和「MAY」關鍵字

所有位元欄位圖表都會以位元組由小到大順序儲存。

版本

目前版本:2

區塊

檢查檔案會分割為多個 Blocks,且大小必須為 2 的冪次方。

區塊大小下限必須為 16 個位元組 (MIN_BLOCK_SIZE),區塊大小上限則必須是 16 個位元組的倍數。建議實作者指定的最大區塊大小小於頁面大小 (通常為 4096 個位元組)。在參考實作項目中,區塊大小上限為 2048 位元組 (MAX_BLOCK_SIZE)。

所有區塊都必須對齊 16 位元組邊界,且 VMO 內的位址是以索引為準,指定 16 位元組的偏移量 (offset = index * 16)。

我們會使用 24 位元做為索引,因此檢查檔案的大小最多為 256 MiB。

block_header 包含 16 個位元組,如下所示:

每個區塊都有 order,用於指定區塊大小。

如果區塊大小上限為 2048 個位元組,則有 8 個可能的區塊順序 (NUM_ORDERS),編號為 0...7,分別對應大小為 16、32、64、128、256、512、1024 和 2048 個位元組的區塊。

每個區塊也都有類型,用於判斷如何解讀區塊中的其餘位元組。

拍檔分配

這個區塊版面配置可透過好友分配功能,有效分配區塊。建議使用 Buddy 分配策略,但這並非使用檢查格式的必要條件。

類型

所有支援的類型皆定義於 //zircon/system/ulib/inspect/include/lib/inspect/cpp/vmo/block.h 中,並分為以下類別:

列舉 value 類型名稱 類別
kFree 0 FREE 內部
kReserved 1 RESERVED 內部
kHeader 2 HEADER 標頭
kNodeValue 3 NODE_VALUE
kIntValue 4 INT_VALUE
kUintValue 5 UINT_VALUE
kDoubleValue 6 DOUBLE_VALUE
kBufferValue 7 BUFFER_VALUE
kExtent 8 EXTENT 範圍
kName 9 NAME 名稱
kTombstone 10 TOMBSTONE
kArrayValue 11 ARRAY_VALUE
kLinkValue 12 LINK_VALUE
kBoolValue 13 BOOL_VALUE
kStringReference 14 STRING_REFERENCE 參考資料
  • Internal:這些類型是用於實作區塊分配,讀取器必須忽略這些類型。

  • Header:此類型可讓讀者偵測檢查檔案,並說明快照一致性的理由。這個區塊必須位於索引 0 處。

  • :這類型別會直接顯示在階層中。值必須具有名稱和父項 (必須是 NODE_VALUE)。

  • Extent:此類型會儲存可能不符合單一區塊的長二進位資料。

  • 名稱:這個類型會儲存可放入單一區塊的二進位資料,通常用於儲存值的名稱。

  • 參照:此類型會保留單一標準值,其他區塊可參照此值。

每種類型對酬載的解讀方式各不相同,如下所示:

免費

可用於分配的 FREE 區塊。重要的是,零值區塊 (16 位元組的 \0) 會解讀為序數為 0 的 FREE 區塊,因此緩衝區可以直接歸零,釋放所有區塊。

寫入器實作可能會將 FREE 區塊 8..63 的未使用位元用於任何用途。Writer 實作必須將所有未使用的位元設為 0。

建議寫入程式使用上述指定的位置,儲存相同順序中下一個空白區塊的索引。使用這個欄位時,空閒區塊可能會建立各個大小空閒區塊的單一連結清單,以便快速分配。當 NextFreeBlock 指向的位置不是 FREE,或不是相同的順序 (通常是索引 0 的標頭區塊) 時,就會到達清單結尾。

保留

RESERVED 區塊可輕鬆變更為其他類型。這是在分配區塊和設定區塊類型之間的選用過渡狀態,可用於檢查實作內容的正確性 (確保即將使用的區塊不會視為空白)。

檔案開頭必須有一個 HEADER 區塊。這項資訊包含「魔術數字」 ("INSP")、版本 (目前為 2)、用於並行控制的「產生計數器」,以及以位元組分配的 VMO 部分大小。標頭的第一個位元組不得是有效的 ASCII 字元。

如要瞭解如何使用世代計數來實作並行控制,請參閱下一節

NODE_VALUE 和 TOMBSTONE

節點是用於進一步巢狀結構的錨點,且值的 ParentID 欄位只能參照 NODE_VALUE 類型的區塊。

NODE_VALUE 區塊支援選用的參照計數暫存點,以便實作下列高效率的功能:

Refcount 欄位可能包含的值數量,是指以特定 NODE_VALUE 做為父項的值數量。刪除後,NODE_VALUE 會變成名為 TOMBSTONE 的新特殊類型。只有在 Refcount 為 0 時,系統才會刪除 TOMBSTONE

這可讓作者實作項目不必明確追蹤節點的子項,並可避免下列情況:

// "b" has a parent "a"
Index | Value
0     | HEADER
1     | NODE "a", parent 0
2     | NODE "b", parent 1

/* delete "a", allocate "c" as a child of "b" which reuses index 1 */

// "b"'s parent is now suddenly "c", introducing a cycle!
Index | Value
0     | HEADER
1     | NODE "c", parent 2
2     | NODE "b", parent 1

{INT,UINT,DOUBLE,BOOL}_VALUE

數字 VALUE 區塊都包含 64 位元數字類型,並內嵌在區塊的第二個 8 位元組中。數值為小端序。

BUFFER_VALUE

一般 BUFFER_VALUE 區塊會參照一或多個連結 EXTENT 區塊中的任意位元組資料。

BUFFER_VALUE 區塊包含第一個 EXTENT 區塊的索引,該區塊會保留二進位資料,並在所有區塊中以位元組為單位,包含資料的總長度。

格式標記會指定如何解讀位元組資料,如下所示:

列舉 意義
kUtf8 0 位元組資料可能會解讀為 UTF-8 字串。
kBinary 1 位元組資料是任意二進位資料,可能無法列印。

EXTENT

EXTENT 區塊包含任意位元組資料酬載,以及鏈條中下一個 EXTENT 的索引。系統會依序讀取每個 EXTENT,直到讀取到 Total Length 位元組為止,藉此擷取 buffer_value 的位元組資料。

酬載是位元組資料,最多可達區塊結尾。大小取決於訂單。

名稱

NAME 區塊會為物件和值提供人類可讀的 ID。這些內容包含 UTF-8 酬載,可完全符合指定區塊。酬載是名稱的內容。大小取決於訂單。

STRING_REFERENCE

STRING_REFERENCE 區塊可用於在 VMO 中實作具有參照語意的字串。它們是 EXTENT 連結清單的起點,也就是說,其值不受大小限制。STRING_REFERENCE 區塊可用於 NAME 預期的情況。

注意:

  • 總長度是酬載的大小 (以位元組為單位)。如果「總長度」>「(16 << 順序) - 12」,酬載就會溢位至「下一個邊界」。
  • 酬載是字串的標準例項。酬載大小取決於訂單。如果酬載大小 + 12 大於「16 << order",則表示酬載太大,無法放入一個區塊,並會溢出至下一個區塊。
  • 下一個邊界索引是第一個溢位 EXTENT 的索引,如果酬載未溢位,則為 0。

ARRAY_VALUE

ARRAY_VALUE 區塊 Payload 的格式取決於儲存值類型 T,解讀方式與類型欄位完全相同。如果 T ∊ {4, 5, 6}Payload 應為位元組邊界上封裝的 64 位元數值。如果 T ∊ {14}Payload 應由 32 位元值組成,代表 T 型別區塊的 24 位元索引,並沿位元組邊界一起封裝。在這種情況下,系統只允許使用 F = 0 (平面陣列)。

F = 0 時,ARRAY_VALUE 應預設為個體化。在數值情況下,這應為相關的零值。在字串情況下,這應為空字串,由特殊值 0 表示。

在區塊的位移 16 處的位元組中,會顯示指定儲存值類型 (或其索引) 的 Count 項目。

「顯示格式」欄位可用於影響陣列的顯示方式,並且會依下列方式解讀:

列舉 說明
kFlat 0 以排序的平面陣列顯示,不含任何額外格式。
kLinearHistogram 1 將前兩個項目解讀為線性直方圖的 floorstep_size 參數,如以下定義所示。
kExponentialHistogram 2 將前三個項目解讀為指數直方圖的 floorinitial_stepstep_multiplier,如下所定義。

線性直方圖

陣列是線性直方圖,可在內嵌方式儲存參數,並包含溢位和欠流桶。

前兩個元素分別是參數 floorstep_size (如以下定義)。

值區數量 (N) 會隱含為 Count - 4

其餘元素為值區:

2:     (-inf, floor),
3:     [floor, floor+step_size),
i+3:   [floor + step_size*i, floor + step_size*(i+1)),
...
N+3:   [floor+step_size*N, +inf)

指數直方圖

陣列是指數型直方圖,可在內文中儲存參數,並包含溢位和欠流桶。

前三個元素分別是參數 floorinitial_stepstep_multiplier (如以下定義)。

值區數量 (N) 是隱含的 Count - 5。

其餘的則是值區:

3:     (-inf, floor),
4:     [floor, floor+initial_step),
i+4:   [floor + initial_step * step_multiplier^i, floor + initial_step * step_multiplier^(i+1))
N+4:   [floor + initial_step * step_multiplier^N, +inf)

LINK_VALUE 區塊可讓節點支援在個別檢查檔案中顯示的子項。

「內容索引」會指定另一個 NAME 區塊,其內容是參照其他檢查檔案的專屬 ID。讀取器應會取得一組 (Identifier, File) 組合 (透過目錄讀取或其他介面),並嘗試使用儲存的 ID 將樹狀結構拼接在一起,以便追蹤連結。

Disposition Flags 會指示讀者如何拼接樹狀結構,如下所示:

列舉 說明
kChildDisposition 0 連結檔案中儲存的階層應為 LINK_VALUE 父項的子項。
kInlineDisposition 1 儲存在連結檔案中的根目錄子項和屬性,應加入至 LINK_VALUE 的父項。

例如:

// root.inspect
root:
  int_value = 10
  child = LINK("other.inspect")

// other.inspect
root:
  test = "Hello World"
  next:
    value = 0


// kChildDisposition produces:
root:
  int_value = 10
  child:
    test = "Hello World"
    next:
      value = 0

// kInlineDisposition produces:
root:
  int_value = 10
  test = "Hello World"
  next:
    value = 0

如果節點與其內嵌連結子項新增的值之間的子項名稱發生衝突,優先順序由讀取器定義。不過,大多數讀者會發現,讓連結值優先,以便覆寫原始值,會更實用。

並行控制

寫入者必須使用全域版本計數器,讓讀取者能夠在讀取之間偵測正在進行的修改,以及修改內容。這項功能支援單一寫入者多重讀取器並行作業。

策略是讓寫入者在開始和結束寫入作業時,都會遞增全域世代計數器

這項簡單的策略具有重大優點:在增加開始和結束寫入的版本號碼之間,寫入器可以在緩衝區執行任意次數的作業,而無須考量資料更新的封包性。

主要缺點是,由於寫入器會頻繁更新,讀取作業可能會無限期延遲,但讀取器實際上可以採取緩解措施。

閱讀器演算法

讀取器會使用下列演算法,取得檢查 VMO 的一致快照:

  1. 旋轉鎖定,直到版本號碼為偶數 (沒有並行寫入) 為止
  2. 複製整個 VMO 緩衝區,並
  3. 檢查緩衝區的版本號碼是否與步驟 1 中的版本號碼相符。

只要版本號碼相符,用戶端就可以讀取本機副本來建構共用狀態。如果版本號碼不相符,用戶端可能會重試整個程序。

寫入鎖演算法

編寫者可透過下列操作,鎖定要修改的檢查 VMO:

atomically_increment(generation_counter, acquire_ordering);

這會將產生世代設為奇數,藉此鎖定檔案,防止並行讀取。取得排序可確保在變更前不會重新排序載入作業。

作家解鎖演算法

編寫者可透過以下步驟,在修改後解鎖檢查 VMO:

atomically_increment(generation_counter, release_ordering);

將世代設為新的偶數,即可解鎖檔案,允許並行讀取。釋出順序可確保在顯示世代計數更新之前,先顯示對檔案的寫入作業。