檢查 VMO 檔案格式

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

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

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

總覽

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

元件會使用檢查格式代管對應的虛擬記憶體物件 (VMO),以公開包含這個內部狀態的檢查階層

檢查階層包含巢狀 節點,其中含有已輸入的 屬性

目標

這份文件所述的檢查格式有下列目標:

  • 資料變動的負擔較低

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

  • 支援非靜態階層

    儲存在檢查檔案中的階層可在執行階段修改。您可以隨時在階層中新增或移除孩子。這樣一來,階層就能緊密代表元件工作集中的物件階層。

  • 單一寫入器、多個讀取器並行作業,無須明確同步處理

    與寫入者並行運作的讀取者會對應 VMO,並嘗試擷取資料快照。寫入器會透過產生計數器指出自己位於重要區段,不需要與讀取器明確同步。讀取器會使用生成計數器,判斷 VMO 快照是否一致,以及是否可安全讀取。

  • 元件終止後,資料可能仍可存取

    即使寫入元件終止,讀取器仍可保留含有檢查資料的 VMO 控制代碼。

術語

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

  • 檢查檔案 - 使用本文件所述格式的位元組有界序列。
  • 檢查 VMO - 儲存在虛擬記憶體物件 (VMO) 中的檢查檔案。
  • 區塊:檢查檔案中具有大小的部分。區塊有索引和順序。
  • 索引 - 特定區塊的專屬 ID。byte_offset = index * 16
  • 順序 - 以位元位移表示的區塊大小,以最小值為基準。size_in_bytes = 16 << order。依大小 (2 的次方) 將區塊分成不同類別。
  • 節點 - 階層中的具名值,其他值可能會巢狀內嵌在其中。只有節點可以做為階層中的父項。
  • 屬性:含有型別資料 (例如字串、整數等) 的具名值。
  • 階層 - 節點樹狀結構,從單一「根」節點開始遞減,每個節點可能包含屬性。檢查檔案包含單一階層。

本文使用 MUST、SHOULD/RECOMMENDED 和 MAY 關鍵字,這些字詞的定義請參閱 RFC 2119

所有位元欄位圖表都以小端排序儲存。

版本

目前版本:2

封鎖

檢查檔案會分割成多個 Blocks,大小必須是 2 的乘方。

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

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

我們使用 24 位元做為索引,但基於舊版原因,檢查檔案最多可能為 128 MiB。

block_header 由 8 個位元組組成,如下所示:

每個區塊都有 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 ,可分為下列類別:

enum 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 參考資料
  • 內部 - 這類類型用於實作區塊分配,讀取器必須忽略。

  • 標題 - 讀者可透過這類標題偵測「檢查檔案」並推斷快照一致性。這個區塊必須位於索引 0。

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

  • 範圍 - 這類資料會儲存可能無法放入單一區塊的長二進位資料。

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

  • 參照:這個型別會保留單一標準值,其他區塊可以參照這個值。

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

免費

可供分配的 FREE 區塊。重要事項:零值區塊 (16 位元組的 \0) 會解讀為順序 0 的 FREE 區塊,因此緩衝區可能只是歸零,以釋放所有區塊。

實作 Writer 時,可將 FREE 區塊的 8..63 未用位元用於任何用途。Writer 實作項目必須將所有其他未使用的位元設為 0。

建議寫入器使用上述位置,儲存相同順序的下一個可用區塊索引。使用這個欄位,空閒區塊可以為每個大小的空閒區塊建立單一連結清單,以利快速分配。當 NextFreeBlock 指向的位置不是 FREE 或不是相同順序 (通常是索引 0 的標頭區塊) 時,即表示已到達清單結尾。

RESERVED

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 區塊全都在區塊的第二個 8 位元組中,內含 64 位元數字類型。數值為小端序。

BUFFER_VALUE

BUFFER_VALUE 區塊可以指向鏈中的第一個 EXTENT 區塊,也可以指向 STRING_REFERENCE

如果 format 值為 kUtf8kBinary,則仲裁者為 EXTENT 鏈結。對於 format 值,裁判是 STRING_REFERENCEkStringReference

如果 formatkStringReference,則 total length 欄位會歸零。

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

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

EXTENT

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

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

名稱

NAME 區塊會為物件和值提供使用者可讀取的 ID。這些酬載完全符合指定區塊的大小,且採用 UTF-8 編碼。酬載是名稱的內容。大小取決於訂單。

STRING_REFERENCE

STRING_REFERENCE 區塊用於在 VMO 中實作具有參照語意的字串。這些是 EXTENT 連結清單的開頭,因此值的大小不受限制。 在預期使用 NAME 的位置,可以使用 STRING_REFERENCE 區塊。

注意:

  • 總長度是指酬載的大小 (以位元組為單位)。如果「total length > (16 << order) - 12」,酬載就會溢位至「下一個範圍」。
  • 酬載是字串的標準例項。酬載大小取決於訂單。如果酬載大小 + 12 大於「16 << order」,則酬載過大,無法放入一個區塊,會溢位到下一個範圍。
  • 下一個範圍索引是第一個溢位 EXTENT 的索引,如果酬載未溢位,則為 0。

ARRAY_VALUE

ARRAY_VALUE 區塊 Payload 的格式取決於「儲存值類型」 T,解讀方式與「類型」欄位完全相同。其中 T ∊ {4, 5, 6} 為 64 位元數值,以位元組界線封裝。Payload其中 T ∊ {14}Payload 應由 32 位元值組成,代表 T 類型區塊的 24 位元索引,並沿著位元組界線封裝在一起。在此情況下,只允許使用一維陣列 F = 0

如果是 F = 0,則應預設執行個體化 ARRAY_VALUE。如果是數值,則應為相關聯的零值。如果是字串,則應為空字串,以特殊值 0 表示。

在區塊中位移 16 的位元組,會顯示確切的「計數」項目,以及指定的「儲存值類型」 (或其索引)。

「顯示格式」欄位用於影響陣列的顯示方式,解讀方式如下:

列舉 說明
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 將樹狀結構拼接在一起,藉此追蹤連結。

處置標記會指示讀取器如何接合樹狀結構,如下所示:

列舉 說明
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

如果節點與內嵌連結子項新增的值之間發生子項名稱衝突,則優先順序由讀取器定義。不過,對大多數讀者來說,連結值優先於原始值會比較實用,因為這樣他們就能覆寫原始值。

並行控制

寫入者必須使用全域版本計數器,讀取者才能偵測到讀取期間的修改內容,以及讀取之間的修改內容,而不需與寫入者通訊。這項功能支援單一寫入器和多個讀取器並行作業。

這項策略是讓寫入者在開始和結束寫入作業時,都遞增全域產生計數器

這項策略簡單易懂,但好處多多:寫入作業開始和結束時,寫入器可以遞增版本號碼,並對緩衝區執行任意數量的作業,不必考慮資料更新的原子性。

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

讀取器演算法

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

  1. 自旋鎖定,直到版本號碼為偶數 (無並行寫入),
  2. 複製整個 VMO 緩衝區,然後
  3. 確認緩衝區中的版本號碼與步驟 1 中的版本號碼相同。

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

寫入器鎖定演算法

寫入者會執行下列操作,鎖定 Inspect VMO 以進行修改:

atomically_increment(generation_counter, acquire_ordering);

這會將產生編號設為奇數,防止檔案遭到並行讀取。取得排序可確保系統不會在這個變更之前重新排序載入。

作家解鎖演算法

撰稿人修改 VMO 後,可透過下列方式解鎖檢查:

atomically_increment(generation_counter, release_ordering);

將產生編號設為新的偶數,即可解鎖檔案,允許並行讀取。發布順序可確保寫入檔案的內容會先顯示,然後才顯示生成次數更新。

常見問題

我的字串需要多少位元組?

字串會儲存在 STRING_REFERENCE 區塊中。因此,如果字串長度為 N 個位元組,在 Inspect VMO 中可能會使用超過 N 個位元組。下表說明特定長度的字串實際使用的位元組數:

字串長度 封鎖訂單 區塊大小 (位元組)
0 - 4 0 16
5 到 20 1 32
21 - 52 2 64
53 - 116 3 128
117 - 244 4 256
245 - 500 5 512
501 - 1012 6 1024
1013 - 2036 7 2048

如果字串長度超過 2036 個位元組,格式就會開始使用 EXTENT 區塊儲存其餘資料。