检查 VMO 文件格式

本文档介绍了组件检查文件格式(检查格式)。

使用检查格式设置格式的文件称为检查文件,通常具有 .inspect 文件扩展名。

如需了解如何更改格式,请参阅扩展 Inspect 文件格式

概览

组件检查功能可让组件在运行时公开有关其状态的结构化分层信息。

组件使用检查格式托管映射的虚拟内存对象 (VMO),以公开包含此内部状态的检查层次结构

检查层次结构由包含类型化属性的嵌套节点组成。

目标

本文档中介绍的检查格式具有以下目标:

  • 对数据的低开销突变

    借助“检查文件格式”,您可以就地更改数据。例如,递增整数的开销约为 2 次原子递增。

  • 支持非静态层次结构

    存储在检查文件中的层次结构可以在运行时进行修改。您可以随时在层次结构中添加或移除儿童。这样一来,层次结构就可以准确地表示组件工作集中的对象层次结构。

  • 单写入者、多读取者并发,无需显式同步

    与写入者同时运行的读取者会映射 VMO 并尝试获取数据快照。写入者通过代际计数器来指示自己处于临界区,该计数器不需要与读取者进行显式同步。读取器使用代际计数器来确定 VMO 的快照何时处于一致状态,可以安全地读取。

  • 组件终止后,数据可能仍可供使用

    即使写入组件终止,读取器也可能会保留对包含检查数据的 VMO 的句柄。

术语

本部分定义了本文档中使用的常用术语。

  • 检查文件 - 使用本文档中所述格式的有界字节序列。
  • 检查 VMO - 存储在虚拟内存对象 (VMO) 中的检查文件。
  • 块 - Inspect 文件中具有大小的部分。块具有索引和顺序。
  • 指数 - 特定块的唯一标识符。byte_offset = index * 16
  • Order - 以位移表示的块大小,相对于最小大小。size_in_bytes = 16 << order。按块的大小(2 的幂)将块分离到各个类中。
  • 节点 - 层次结构中的一个命名值,其他值可以嵌套在该值下。在层次结构中,只有节点可以是父级。
  • 属性 - 包含类型化数据(例如字符串、整数等)的命名值。
  • 层次结构 - 一个节点树,从单个“根”节点开始向下延伸,每个节点可能都包含属性。一个检查文件包含一个层次结构。

本文档使用 RFC 2119 中定义的“必须”“应/建议”和“可以”关键字

所有位字段图均以小端字节序存储。

版本

当前版本:2

文本块数

检查文件会拆分为多个 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 - 此类型用于存储可能无法放入单个块中的长二进制数据。

  • 名称 - 此类型存储适合单个块的二进制数据,通常用于存储值的名称。

  • 引用 - 此类型包含一个规范值,其他块可以引用该值。

每种类型对载荷的解读方式各不相同,如下所示:

免费

FREE 个块可供分配。重要的是,零值块(16 字节的 \0)会被解读为 0 阶的 FREE 块,因此只需将缓冲区归零即可释放所有块。

写入器实现可以使用 FREE 块中 8..63 的未用位来实现任何目的。写入器实现必须将所有其他未使用的位设置为 0。

建议写入者使用上述位置来存储下一个相同顺序的空闲块的索引。使用此字段,空闲块可以创建每个大小的空闲块的单向链表,以实现快速分配。当 NextFreeBlock 指向不是 FREE 或不是相同顺序的位置(通常是索引 0 处的 Header 块)时,即表示已到达列表末尾。

RESERVED

RESERVED 块只是可以更改为其他类型。这是分配块和设置块类型之间的可选过渡状态,可用于检查实现的正确性(确保即将使用的块不会被视为空闲)。

文件开头必须有一个 HEADER 代码块。它由魔数 (“INSP”)、版本(目前为 2)、用于并发控制的生成计数以及以字节为单位分配的 VMO 部分的大小组成。标头的第一字节不得是有效的 ASCII 字符。

如需了解如何使用代数实现并发控制,请参阅下一部分

NODE_VALUE 和 TOMBSTONE

节点是进一步嵌套的锚点,并且值中的 ParentID 字段必须仅引用 NODE_VALUE 类型的块。

NODE_VALUE 块支持可选的引用计数标记为已删除,以实现高效的实现,如下所示:

Refcount 字段可能包含引用给定 NODE_VALUE 作为其父级的值的数量。删除后,NODE_VALUE 会变为一种名为 TOMBSTONE 的新特殊类型。仅当 TOMBSTONERefcount 为 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 链。对于 kStringReferenceformat 值,裁判是 STRING_REFERENCE

如果 formatkStringReference,则 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_VALUEPayload 的格式取决于存储的值类型 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 块,其内容是引用另一个检查文件的唯一标识符。读者应获取 (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 的一致性快照:

  1. 自旋锁,直到版本号为偶数(无并发写入),
  2. 复制整个 VMO 缓冲区,并
  3. 检查缓冲区中的版本号是否与第 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 块来存储剩余数据。