本文档介绍了组件检查文件格式(检查格式)。
使用检查格式设置格式的文件称为检查文件,通常具有 .inspect
文件扩展名。
如需了解如何更改格式,请参阅扩展 Inspect 文件格式
概览
组件检查功能可让组件在运行时公开有关其状态的结构化分层信息。
组件使用检查格式托管映射的虚拟内存对象 (VMO),以公开包含此内部状态的检查层次结构。
检查层次结构由包含类型化属性的嵌套节点组成。
目标
本文档中介绍的检查格式具有以下目标:
对数据的低开销突变
借助“检查文件格式”,您可以就地更改数据。例如,递增整数的开销约为 2 次原子递增。
支持非静态层次结构
存储在检查文件中的层次结构可以在运行时进行修改。您可以随时在层次结构中添加或移除儿童。这样一来,层次结构就可以准确地表示组件工作集中的对象层次结构。
单写入者、多读取者并发,无需显式同步
与写入者同时运行的读取者会映射 VMO 并尝试获取数据快照。写入者通过代际计数器来指示自己处于临界区,该计数器不需要与读取者进行显式同步。读取器使用代际计数器来确定 VMO 的快照何时处于一致状态,可以安全地读取。
组件终止后,数据可能仍可供使用
即使写入组件终止,读取器也可能会保留对包含检查数据的 VMO 的句柄。
术语
本部分定义了本文档中使用的常用术语。
- 检查文件 - 使用本文档中所述格式的有界字节序列。
- 检查 VMO - 存储在虚拟内存对象 (VMO) 中的检查文件。
- 块 - Inspect 文件中具有大小的部分。块具有索引和顺序。
- 指数 - 特定块的唯一标识符。
byte_offset = index * 16
- Order - 以位移表示的块大小,相对于最小大小。
size_in_bytes = 16 << order
。按块的大小(2 的幂)将块分离到各个类中。 - 节点 - 层次结构中的一个命名值,其他值可以嵌套在该值下。在层次结构中,只有节点可以是父级。
- 属性 - 包含类型化数据(例如字符串、整数等)的命名值。
- 层次结构 - 一个节点树,从单个“根”节点开始向下延伸,每个节点可能都包含属性。一个检查文件包含一个层次结构。
本文档使用 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 位作为索引,但出于旧版原因,检查文件的大小最多为 128MiB。
block_header
由 8 个字节组成,如下所示:
每个块都有一个 order
,用于指定其大小。
如果最大块大小为 2048 字节,则有 8 种可能的块顺序 (NUM_ORDERS
),编号为 0...7,分别对应于大小为 16、32、64、128、256、512、1024 和 2048 字节的块。
每个块还具有一个类型,用于确定如何解释块中的其余字节。
好友分配
这种块布局允许使用伙伴分配高效分配块。伙伴分配是建议的分配策略,但不是使用检查格式的必要条件。
类型
所有受支持的类型均在 //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 |
参考文档 |
内部 - 这些类型用于实现块分配,读取器必须忽略它们。
标头 - 此类型允许读者检测检查文件并推断快照一致性。此块必须位于索引 0 处。
值 - 这些类型直接显示在层次结构中。值必须具有 Name 和父级(必须是
NODE_VALUE
)。Extent - 此类型用于存储可能无法放入单个块中的长二进制数据。
名称 - 此类型存储适合单个块的二进制数据,通常用于存储值的名称。
引用 - 此类型包含一个规范值,其他块可以引用该值。
每种类型对载荷的解读方式各不相同,如下所示:
- 免费
- RESERVED
- 标题
- 通用 VALUE 字段
- NODE_VALUE
- INT_VALUE
- UINT_VALUE
- DOUBLE_VALUE
- BUFFER_VALUE
- EXTENT
- NAME
- TOMBSTONE
- ARRAY_VALUE
- 链接
- BOOL_VALUE
- STRING_REFERENCE
免费
有 FREE
个块可供分配。重要的是,零值块(16 字节的 \0
)会被解读为 0 阶的 FREE
块,因此只需将缓冲区归零即可释放所有块。
写入器实现可以使用 FREE
块中 8..63 的未用位来实现任何目的。写入器实现必须将所有其他未使用的位设置为 0。
建议写入者使用上述位置来存储下一个相同顺序的空闲块的索引。使用此字段,空闲块可以创建每个大小的空闲块的单向链表,以实现快速分配。当 NextFreeBlock 指向不是 FREE
或不是相同顺序的位置(通常是索引 0 处的 Header 块)时,即表示已到达列表末尾。
RESERVED
RESERVED
块只是可以更改为其他类型。这是分配块和设置块类型之间的可选过渡状态,可用于检查实现的正确性(确保即将使用的块不会被视为空闲)。
HEADER
文件开头必须有一个 HEADER
代码块。它由魔数 (“INSP”)、版本(目前为 2)、用于并发控制的生成计数以及以字节为单位分配的 VMO 部分的大小组成。标头的第一字节不得是有效的 ASCII 字符。
如需了解如何使用代数实现并发控制,请参阅下一部分。
NODE_VALUE 和 TOMBSTONE
节点是进一步嵌套的锚点,并且值中的 ParentID
字段必须仅引用 NODE_VALUE
类型的块。
NODE_VALUE
块支持可选的引用计数和标记为已删除,以实现高效的实现,如下所示:
Refcount
字段可能包含引用给定 NODE_VALUE
作为其父级的值的数量。删除后,NODE_VALUE
会变为一种名为 TOMBSTONE
的新特殊类型。仅当 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
链。对于 kStringReference
的 format
值,裁判是 STRING_REFERENCE
。
如果 format
为 kStringReference
,则 total length
字段将归零。
格式标志用于指定应如何解读字节数据,如下所示:
枚举 | 值 | 含义 |
---|---|---|
kUtf8 | 0 | 字节数据可以解释为 UTF-8 字符串。 |
kBinary | 1 | 字节数据是任意二进制数据,可能无法打印。 |
kStringReference | 2 | 数据是一个 STRING_REFERENCE 块。 |
程度
EXTENT
块包含任意字节数据载荷和链中下一个 EXTENT
的索引。通过按顺序读取每个 EXTENT
,直到读取 总长度字节,即可检索 buffer_value 的字节数据。
载荷是字节数据,最多可到块的末尾。大小取决于订单。
姓名
NAME
块为对象和值提供直观易懂的标识符。它们由完全适合给定块的 UTF-8 载荷组成。载荷是名称的内容。大小取决于订单。
STRING_REFERENCE
STRING_REFERENCE
块用于在 VMO 中实现具有引用语义的字符串。它们是 EXTENT
的关联列表的开头,这意味着它们的值不受大小限制。
在需要使用 NAME
的位置,可以使用 STRING_REFERENCE
块。
注意:
- 总长度是指载荷的大小(以字节为单位)。如果“总长度 > (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
块,其内容是引用另一个检查文件的唯一标识符。读者应获取 (Identifier, File)
对的捆绑包(通过目录读取或其他接口),并且可以尝试使用存储的标识符将树拼接在一起,从而跟踪链接。
处置标志会指示读者如何拼接树,如下所示:
枚举 | 值 | 说明 |
---|---|---|
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 步中的版本号相同。
只要版本号匹配,客户端就可以读取其本地副本来构建共享状态。 如果版本号不匹配,客户端可能会重试整个流程。
写入器锁定算法
写入者通过执行以下操作来锁定 Inspect VMO 以进行修改:
atomically_increment(generation_counter, acquire_ordering);
这会通过将代数设置为奇数来锁定文件,以防止并发读取。获取顺序可确保在此更改之前不会重新排序加载。
撰稿人解锁算法
写入者通过执行以下操作在修改后解锁 Inspect VMO:
atomically_increment(generation_counter, release_ordering);
将世代设置为新的偶数,以解锁文件并允许并发读取。发布顺序可确保在生成计数更新可见之前,对文件的写入是可见的。
常见问题解答
我的字符串需要多少个字节?
字符串存储在 STRING_REFERENCE
块中。因此,如果字符串长度为 N
字节,则在检查 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
块来存储剩余数据。