本文說明元件檢查檔案格式 (檢查格式)。
使用檢查格式設定格式的檔案稱為「檢查檔案」,通常會使用 .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
- 第 2 版允許值的名稱為 NAME 或 STRING_REFERENCE。
區塊
檢查檔案會分割為多個 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:此類型會儲存可能不符合單一區塊的長二進位資料。
名稱:這個類型會儲存可放入單一區塊的二進位資料,通常用於儲存值的名稱。
參照:此類型會保留單一標準值,其他區塊可參照此值。
每種類型對酬載的解讀方式各不相同,如下所示:
- 免費
- 保留
- HEADER
- 常見的 VALUE 欄位
- NODE_VALUE
- INT_VALUE
- UINT_VALUE
- DOUBLE_VALUE
- BUFFER_VALUE
- EXTENT
- 收件者姓名
- TOMBSTONE
- ARRAY_VALUE
- 連結
- BOOL_VALUE
- STRING_REFERENCE
免費
可用於分配的 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 | 將前兩個項目解讀為線性直方圖的 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 將樹狀結構拼接在一起,以便追蹤連結。
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 的一致快照:
- 旋轉鎖定,直到版本號碼為偶數 (沒有並行寫入) 為止
- 複製整個 VMO 緩衝區,並
- 檢查緩衝區的版本號碼是否與步驟 1 中的版本號碼相符。
只要版本號碼相符,用戶端就可以讀取本機副本來建構共用狀態。如果版本號碼不相符,用戶端可能會重試整個程序。
寫入鎖演算法
編寫者可透過下列操作,鎖定要修改的檢查 VMO:
atomically_increment(generation_counter, acquire_ordering);
這會將產生世代設為奇數,藉此鎖定檔案,防止並行讀取。取得排序可確保在變更前不會重新排序載入作業。
作家解鎖演算法
編寫者可透過以下步驟,在修改後解鎖檢查 VMO:
atomically_increment(generation_counter, release_ordering);
將世代設為新的偶數,即可解鎖檔案,允許並行讀取。釋出順序可確保在顯示世代計數更新之前,先顯示對檔案的寫入作業。