时钟

姓名

clock - 用于跟踪时间进度的内核对象。

概要

时钟是单调时钟参考时间轴的一维线性转换,可由时钟维护者以原子方式进行调整,并由客户端观察。

说明

属性

时钟的属性是在创建时钟时确定的,之后便无法更改。目前,定义了三个时钟属性。

ZX_CLOCK_OPT_MONOTONIC

设置后,时钟保证具有单调行为。也就是说,对时钟的任何观察序列都保证会产生一个时间序列,该序列始终大于或等于之前的观察结果。单调时钟从不会向后跳转,但可以向前跳转。正式:

给定时钟 C,设 C(x) 为从参考时间轴 C 的时间轴映射的函数。C(x) 是一个分段线性函数,由 C 的维护者确定的所有时间段内的所有线性转换片段组成。当且仅当满足以下条件时,C 才是单调函数:

对于所有 R1R2R2 >= R1

C(R2) >= C(R1)

ZX_CLOCK_OPT_CONTINUOUS

设置后,时钟将保证具有连续行为。也就是说,对时钟转换的任何更新都保证与上一个转换片段具有一阶连续性。正式:

Ci(x)C(x)i仿射转换片段。设 Ri 为参考时间轴上第一个定义了 Ci(x) 的时间点。时钟 C 连续的充分必要条件是:对于所有 i

Ci(Ri + 1) = Ci + 1(Ri + 1)

后备时间

时钟的后备时间表示时钟可以设置的最小值。由于时钟只能向前走,而不能向后走,因此时钟的观察器永远不可能收到低于时钟创建者配置的回退时间的值。

在创建时,可以通过 zx_create_args_v1_t 结构提供回退时间。否则,默认值为 0。

在时钟更新操作期间,如果尝试将时钟的值设置为小于后备时间的值,则会失败并抛出 ZX_ERR_INVALID_ARGS。未在初始设置时设置的时钟将始终报告为为时钟配置的后备时间。回退时间不得低于默认值零。

隐含属性

  • 系统中所有时钟对象的参考时钟都是单调时钟。
  • 所有时钟对象的标称单位均指定为纳秒。此属性不可配置。
  • 所有时钟对象的频率调整单位均指定为百万分之一,即 PPM。
  • 时钟对象的频率调整允许的最大范围被指定为 [-1000, +1000] PPM。此属性不可配置。

其他制作选项

ZX_CLOCK_OPT_AUTO_START

在创建时钟时使用此选项时,时钟会从已启动状态开始,而不是默认的未启动状态。如需了解详情,请参阅启动时钟

读取时钟

给定时钟句柄后,用户可以使用 zx_clock_read() 系统调用查询该时钟给出的当前时间。时钟会读取 ZX_RIGHT_READ 权限。保证所有观察器的计时器读取结果是一致的。也就是说,如果两个观察者在完全相同的参考时间 R 查询时钟,他们将始终看到相同的值 C(R)

参考时间轴、zx_ticks_get()zx_clock_get_monotonic()

如前所述,zx_clock_get_monotonic() 是所有用户创建的 Zircon 时钟的参考时间轴。这意味着,如果用户知道时钟实例的当前转换,那么给定时钟实例时间轴上的值,可以计算时钟单调时间轴上的相应点(反之亦然)。这也意味着,如果未对内核时钟进行速率调整,时钟单调和内核时钟将以完全相同的速率滴答。

除了时钟单调时间轴之外,zircon 内核还通过 zx_ticks_get()zx_ticks_per_second() 公开“滴答”时间轴。在内部,计数实际上是时钟单调的参考时间轴,并直接从可供内核访问的架构适用计时器单元读取。时钟单调实际上是对以纳秒单位归一化的计数器时间轴的线性转换。在内核启动时,这两个时间轴都会从零开始计时。

由于时钟单调性是基于滴答的静态转换,并且所有内核时钟都是基于时钟单调性的转换,因此除了时钟单调性之外,滴答还可以用作内核时钟的参考时钟。

提取时钟的详细信息

除了简单读取时钟的当前值之外,拥有 ZX_RIGHT_READ 权限的高级用户还可以使用 zx_clock_get_details() 读取时钟并在过程中获取详细信息。调用成功后,返回给调用方的详细信息结构将包含:

  • 当前时钟单调到时钟转换。
  • 当前的秒针到时钟转换。
  • 时钟的当前对称误差边界估算值(如果有)。
  • 时钟上次更新的时间,由时钟单调参考时间轴定义。
  • 对系统滴答计数器的观察,是在观察时钟期间进行的。
  • 在创建时定义的时钟的所有静态属性。
  • 每次更新时钟的底层转换时,其值都会发生变化的生成 nonce。

高级用户不仅可以使用这些详细信息计算时钟的近期 now 值(通过使用“ticks-to-clock”转换来转换报告的 ticks-now 观察结果,这两项均由“get details”操作报告),还可以:

  • 了解自上次 zx_clock_get_details() 操作(使用生成 Nonce)后时钟转换是否发生了更改。请注意,时钟的生成 Nonce 不保证从任何给定值开始,也不保证在每次更新时以任何特定方式(例如按固定值递增)进行更改。而是,在每次更新时,生成 Nonce 都会更改为一个值,该值保证与更新前立即的值不同。
  • 将时钟转换与其他时钟的转换组合,以推理两个时钟之间的关系。
  • 了解时钟维护者对误差边界的最佳估算值。
  • 根据上次校正时间、当前转换和时钟的最大允许校正系数,推理时钟相对于参考时钟的可能未来值的范围(请参阅上文“隐含属性”部分中所述的频率调整的最大允许范围)。

启动时钟和时钟信号

创建后,计时器尚未开始。所有尝试读取时钟的操作都会返回时钟的配置后备时间,如果在创建时未指定,则默认为 0。

在时钟的维护者执行首次更新操作(必须包含设置值操作)后,时钟便会开始运行。时钟将在此时开始运行,其速率等于参考时钟加上维护者指定的与标称值的偏差。

时钟还有一个 ZX_CLOCK_STARTED 信号,用户可以使用该信号来了解时钟何时实际启动。最初,此信号未设置,但在首次成功执行更新操作后会设置。一旦启动,时钟就不会停止,并且 ZX_CLOCK_STARTED 信号始终会被断言。

最初,时钟是单调时钟的克隆,这使得单调时钟时间轴和合成时间轴之间的转换成为恒等函数。创建后,此时钟可能仍会维护,具体取决于权限、ZX_CLOCK_OPT_MONOTONICZX_CLOCK_OPT_CONTINUOUS 属性以及配置的后备时间所施加的限制。

如果使用 ZX_CLOCK_OPT_AUTO_START 选项创建时钟,则无法将其配置为具有大于当前时钟单调时间的回退时间。如果允许这样做,则会导致时钟的当前时间被设为其回退时间之前的时间。

维护时钟

拥有时钟对象的 ZX_RIGHT_WRITE 权限的用户可以使用 zx_clock_update() 系统调用充当时钟的维护者。在每次调用 zx_clock_update() 时,都可以调整时钟的三个参数,但每次都不需要调整这三个参数。这些值如下:

  • 时钟的绝对值。
  • 时钟的频率调整(与标称值的偏差,以 ppm 表示)
  • 时钟的绝对误差边界估算值(以纳秒为单位)

对时钟转换的更改会在系统调用本身期间发生。用户可能无法指定调整的具体参考时间。

对设置了 ZX_CLOCK_OPT_MONOTONIC 属性且会导致非单调行为的时钟的绝对值所做的任何更改都会失败,并返回 ZX_ERR_INVALID_ARGS 代码。

第一个更新操作会启动计时器,并且必须包含设置值操作。

除了首次设置值操作外,所有尝试设置已设置 ZX_CLOCK_OPT_CONTINUOUS 属性的时钟的绝对值的操作都会失败,并返回 ZX_ERR_INVALID_ARGS

关于时钟误差边界估算值的说明

zx_clock_get_details() 系统调用可向用户提供有关时钟的许多精细详细信息,包括“误差边界估算值”。此值以纳秒为单位,表示时钟维护人员目前对时钟相对于维护人员使用的参考时钟的误差的最佳估算值。例如,如果用户提取的时间 X 的误差边界估计值为 E,则时钟维护者尝试表明,它认为时钟的实际值位于 [ X-E, X+E ] 范围内。

内核 API 指定此估算值的置信度。有些时钟维护者可能使用严格的边界,有些则使用无法证明但提供“高置信度”的边界,而有些可能对其估算结果仍不太有信心或完全没有信心。

如果用户需要了解其访问的错误估算值的客观质量(例如,强制执行证书有效日期或 DRM 许可到期),则应了解系统中哪个组件在维护其时钟,以及维护者在其发布的错误边界估算值的置信度方面提供了哪些保证。

可映射的时钟

除了使用标准系统调用 (zx_clock_read()zx_clock_get_details()) 读取时钟或获取其详细信息之外,Zircon 内核时钟还提供了另一种方法,这种方法可以通过(几乎总是)跳过进入 Zircon 内核以观察时钟的步骤来减少观察开销。这里的主要思想是,时钟在本质上只不过是应用于所选参考时钟的当前值的仿射转换。如果用户模式可以通过共享内存访问转换状态,并且底层参考时钟无需进入 Zircon 内核即可读取,那么无需进入内核即可观察合成时钟。输入“可映射”时钟。

创建

您可以通过在创建时调用 zx_clock_create() 并传递 ZX_CLOCK_OPT_MAPPABLE 来创建可映射的内核时钟。新创建的时钟句柄将包含 ZX_RIGHT_MAP 权限以及所有其他默认权限。此外,通过 get-details 操作报告的 options 字段将包含 ZX_CLOCK_OPT_MAPPABLE 标志,反映出这是可映射的时钟。

映射

若要使用共享内存观察时钟的状态,必须先完成以下两项操作。

1) 必须确定时钟状态的映射大小。2) 时钟的共享状态必须映射到用户的地址空间。

如需提取时钟的映射大小,用户必须使用 ZX_INFO_CLOCK_MAPPED_SIZE 主题调用 zx_object_get_info(),并传递大小为 uint64_t 的缓冲区,以接收时钟的映射大小。

知道大小后,用户可以调用 zx_vmar_map_clock(),以便实际将时钟的状态映射到其地址空间。此调用与 zx_vmap_map_vmo() 的运作方式非常相似,只是增加了一些额外的内置限制。值得注意的是:

1) 仅允许使用 ZX_VM_PERM_READ 权限,无论用户拥有哪些句柄权限,都明确禁止使用所有其他 ZX_VM_PERM 标志。映射的时钟数据始终必须是只读的。2) 传递给系统调用的 len 参数必须与从 get-info 调用检索的值一致。3) 在映射时钟状态时无法指定 vmo_offset。 必须始终映射整个时钟状态区域。

若要映射时钟,需要同时拥有时钟句柄的 ZX_RIGHT_READZX_RIGHT_MAP 权限。

成功的映射操作将返回时钟对象的状态在指定 VMAR 中映射到的虚拟地址。

取消映射

取消映射时钟的过程与取消映射 VMO 或 IO 缓冲区区域的过程相同。用户只需对用于映射时钟的 VMAR 调用 zx_vmar_unmap(),传递从原始 get-info 调用收到的长度以及从 zx_clock_map() 调用收到的虚拟地址。

用户可以在映射时关闭时钟的句柄,但仍可读取时钟的状态。就像关闭 VMO 句柄不会销毁该 VMO 的任何映射一样,关闭时钟句柄也不会销毁其任何映射。请参阅 zx_vmar_unmap()zx_vmar_destroy()

观察映射的时钟

如需观察映射的时钟,用户可以调用 zx_clock_read_mapped()zx_clock_get_details_mapped(),并将映射时钟状态的虚拟地址作为第一个参数传递。所有其他内容保持不变,如需了解详情,请参阅 zx_clock_read()zx_clock_get_details() 的文档。

以下任何操作都会导致未定义的行为:

  • 尝试使用除原始 zx_clock_map() 调用返回的虚拟地址之外的任何虚拟地址调用映射的时钟观察系统调用。
  • 尝试使用已部分或完全取消映射的时钟状态调用已映射的时钟观察系统调用。

此外,用户不得尝试使用映射的时钟状态来推断时钟本身的详细信息。时钟状态的格式未指定,可能会随时更改。使用时钟的映射状态的唯一合法方式是与对 zx_clock_read_mapped() orzx_clock_get_details_mapped()` 的调用结合使用。

使用提供的系统调用进行正确访问时,时钟观察结果将具有“多观察器”一致性,这意味着,如果多个观察器进行的一组观察结果具有既定顺序,则单调性和连续性等属性在这些观察结果中将保持一致。如需了解详情,请点击此处

zx_info_maps_tzx_info_vmos_t 结构中的时钟映射表示

Zircon 目前提供两个用于提取有关当前有效映射的信息的诊断 zx_object_get_info() 主题,即 ZX_INFO_VMAR_MAPSZX_INFO_PROCESS_MAPS,这两个主题都会返回一个 zx_info_maps_t 结构数组,用于描述指定 VMAR(或进程)中当前有效的映射。

时钟映射将以与 VMO 映射相同的方式枚举,但存在以下差异。

1) 由于时钟目前不是可命名的对象,因此系统会将字符串“kernel-clock”作为映射的名称返回,而不是像通常所做的那样返回 VMO 的当前名称。2) 系统将使用时钟对象的 KOID 填充映射的 KOID 字段。

此外,程序可以使用 ZX_INFO_PROCESS_VMOS 主题提取一个 zx_info_vmos_t 结构数组,从而获取与进程关联的当前 VMO 的相关信息。如果某个进程将时钟映射到其地址空间,该时钟也会显示在此枚举中。与 zx_info_maps_t 情形一样,报告的 KOID 将是时钟对象的 KOID,报告的名称将是“kernel-clock”。此外,条目的 flags 字段将设置 ZX_INFO_VMO_VIA_MAPPING 位,以指示由于存在有效的映射,系统正在对列表中的对象进行枚举。

示例

以下是一个简化示例,展示了如何映射和读取时钟。

class MappedClockReader {
 public:
  MappedClockReader(const zx::clock& clock) {
    zx_status_t status = clock.get_info(ZX_INFO_CLOCK_MAPPED_SIZE, &mapped_clock_size_,
                                        sizeof(mapped_clock_size_), nullptr, nullptr));
    ZX_ASSERT(status == ZX_OK);

    status = zx::vmar::root_self()->map_clock(ZX_VM_PERM_READ | ZX_VM_MAP_RANGE, 0, clock,
                                              mapped_clock_size_, &clock_addr_);
    ZX_ASSERT(status == ZX_OK);
  }

  ~MappedClockReader() {
    if (clock_addr_ != 0) {
      zx::vmar::root_self()->unmap(clock_addr_, mapped_clock_size_);
    }
  }

  zx_time_t Read() {
    zx_time_t result{0};
    const zx_status_t status = zx::clock::read_mapped(clock_addr_, &result);
    ZX_ASSERT(status == ZX_OK);
    return result;
  }

 private:
  zx_vaddr_t clock_addr_{0};
  uint64_t mapped_clock_size_{0};
}

参考文献