本文說明元件檢查檔案格式 (檢查格式)。
使用檢查格式設定的檔案稱為「檢查檔案」,通常會採用 .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
- 第 2 版允許值名稱為 NAME 或 STRING_REFERENCE。
封鎖
檢查檔案會分割成多個 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
)。範圍 - 這類資料會儲存可能無法放入單一區塊的長二進位資料。
名稱:這類資料會儲存單一區塊中的二進位資料,通常用於儲存值的名稱。
參照:這個型別會保留單一標準值,其他區塊可以參照這個值。
各類型解讀酬載的方式不同,如下所示:
- 免費
- 已預訂
- 標頭
- 常見的 VALUE 欄位
- NODE_VALUE
- INT_VALUE
- UINT_VALUE
- DOUBLE_VALUE
- BUFFER_VALUE
- 範圍
- NAME
- TOMBSTONE
- ARRAY_VALUE
- 連結
- BOOL_VALUE
- STRING_REFERENCE
免費
可供分配的 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
值為 kUtf8
或 kBinary
,則仲裁者為 EXTENT
鏈結。對於 format
值,裁判是 STRING_REFERENCE
。kStringReference
如果 format
為 kStringReference
,則 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 | 將前兩個項目解讀為線性直方圖的 floor 和 step_size 參數,如下所示。 |
kExponentialHistogram | 2 | 將前三個項目解讀為指數直方圖的 floor 、initial_step 和 step_multiplier ,如下所示。 |
線性直方圖
這個陣列是線性直方圖,會內嵌儲存參數,並包含溢位和下溢值區。
前兩個元素分別是參數 floor
和 step_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)
指數直方圖
這個陣列是指數直方圖,會內嵌儲存參數,並包含溢位和下溢值區。
前三個元素分別是參數 floor
、initial_step
和 step_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
節點可透過 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 的一致性快照:
- 自旋鎖定,直到版本號碼為偶數 (無並行寫入),
- 複製整個 VMO 緩衝區,然後
- 確認緩衝區中的版本號碼與步驟 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
區塊儲存其餘資料。