RFC-0266:内存可映射内核时钟

RFC-0266:可映射到内存的内核时钟
状态已接受
区域
  • 内核
说明

提供了一种供用户模式进程读取内核时钟状态的方法,而无需进入 Zircon 内核。

问题
Gerrit 更改
作者
审核人
提交日期(年-月-日)2024-11-21
审核日期(年-月-日)2025-02-14

问题陈述

Starnix 需要一种方式来为其客户端程序提供时钟,该时钟的行为与 Linux 的 CLOCK_MONOTONIC 定义相符。此时钟需要表示单调的时间轴,该时间轴的速率调整与系统范围内的 UTC 时间轴相同,但当能够通过外部方式确定观测顺序的不同线程观测到该时间轴时,该时间轴也始终是单调且连续的。换句话说,这些属性需要在多个观测者进行的一系列有序观测中保持一致。

请注意,Zircon/Fuchsia 的 CLOCK_MONOTONIC 定义目前不符合所有这些要求,也没有任何计划要使其符合这些要求。虽然 Zircon 的版本始终是单调递增且连续的,但它始终以系统底层参考时钟硬件的速率运行,并且不会尝试与任何外部参考进行速率匹配。

预计 Starnix 的客户对 Linux CLOCK_MONOTONIC 时钟的使用率会很高,因此需要尽一切努力使任何解决方案的查询成本尽可能低。更复杂的是,Starnix 的客户端必须在受限模式下运行,这意味着它们不允许直接发出 Zircon 系统调用。在最好的情况下,Starnix 客户端可以退出受限模式并进入 Starnix 内核,从而可以进行 Zircon 系统调用。请注意,这是 Starnix 目前用于提供对系统级 UTC 时间轴的访问权限的策略,但此方法的性能成本已经相当高。理想的解决方案应能够为 Starnix 客户端提供对 Linux CLOCK_MONOTONIC 的访问权限,而无需客户端退出受限模式 进入 Zircon 内核,因为这种开销实际上已经变得难以承受。

摘要

Zircon 内核时钟对象已经能够满足 Linux 版 CLOCK_MONOTONIC大多数要求。它们提供了一种构建用户模式定义的时间轴的方法,该时间轴在被多个观察者观察时,保证(由内核)始终是单调递增且连续的,并且还可以委托给授权机构(在本例中为 Timekeeper)进行维护

此 RFC 提出了一种允许以无需进入 Zircon 内核的方式读取 Zircon 内核时钟的方法,从而使读取操作的开销非常低,前提是时钟对象的参考时钟也可以在无需进入 Zircon 内核的情况下读取。此外,这种读取时钟对象的方法将提供一种途径,让 Starnix 客户端无需退出受限模式即可读取时钟,从而使 Starnix 能够向其客户端发布 Linux CLOCK_MONOTONIC 和 UTC 两者,而无需退出受限模式。

利益相关方

  • Starnix 实现者
  • Timekeeper 维护人员
  • 内核维护人员
  • 媒体维护人员

辅导员

  • jamesr@google.com

审核者

  • rashaeqbal@google.com
  • rudymathu@google.com
  • jamesr@google.com
  • adanis@google.com
  • mpuryear@google.com

已咨询

  • maniscalco@google.com
  • mcgrathr@google.com
  • fmil@google.com

共同化

此 RFC 是在内部设计文档中提出的,并与利益相关者进行了讨论,直到被认为可以开始 RPC 流程。

要求

此提案的主要目的之一是为 Starnix 提供一种途径,使其能够向受限模式进程提供表示 Linux 版 CLOCK_MONOTONIC 的时钟。虽然能够对时钟进行内存映射也有次要好处,但此处所述的要求主要来自需要满足 Starnix 和 Linux 定义的要求。

内存映射时钟需要能够:

  • 满足 CLOCK_MONOTONIC 的 Linux 要求
  • 与系统范围内的 UTC 时钟一样,受到相同的速率调整。
  • 在多个观测者按顺序进行的一系列观测中保持单调性和连续性。
  • 可以从 Starnix 受限模式进程中查询,且“运行时成本可承受”。

CLOCK_MONOTONIC 的 Linux 要求

根据 Linux clock_gettime man 页面CLOCK_MONOTONIC 的要求如下。

A nonsettable system-wide clock that represents monotonic time sinceas
described by POSIX"some unspecified point in the past".  On Linux, that point
corresponds to the number of seconds that the system has been running since it
was booted.

The CLOCK_MONOTONIC clock is not affected by discontinuous jumps in the system
time (e.g., if the system administrator manually changes the clock), but is
affected by the incremental adjustments performed by adjtime(3) and NTP.  This
clock does not count time that the system is suspended.  All CLOCK_MONOTONIC
variants guarantee  that  the  time returned by consecutive calls will not go
backwards, but successive calls maydepending on the architecturereturn
identical (not-increased) time values.

可维护性

Linux 对 CLOCK_MONOTONIC 的定义要求,为保持系统范围内的 UTC 定义而进行的相同速率调整也应应用于 CLOCK_MONOTONIC 时间轴。目前,表示系统 UTC 时间轴的时钟由组件管理器创建,并通过进程启动协议以重复句柄(具有适当的权限)的形式分发给进程。一个名为 Timekeeper 的进程会收到对该时钟的具有写入权限的句柄。它负责使用可用的外部参考来维护 UTC 时钟,以持续修正 UTC 的本地表示。

鉴于上述 Linux 要求,系统中的某些内容需要将 Timekeeper 应用于系统范围的 UTC 时钟的相同速率调整应用于 Starnix 将提供给其客户的 Linux CLOCK_MONOTONIC 版本。

一致性

如 Linux 要求中所述,CLOCK_MONOTONIC 需要是单调递增且连续的。因此,观测时钟的线程永远不能看到时钟向后走或向前跳。换句话说,时钟观测结果必须与这些单调/连续属性保持一致。

虽然 Linux man 页面中未明确说明,但我们认为此要求还意味着时钟不仅要从单个观测者的角度来看保持一致,还必须在“多观测者”之间保持一致。换句话说,对于多个线程进行的一系列有序观测结果 (O1, O2, O3 ... On),该观测结果序列必须同时满足单调性和连续性属性。

只有当多个观测者之间存在互动,从而建立起这样的顺序时,多个观测者所做的观测序列才具有确定的顺序。 例如,如果两个线程(T1 和 T2)进行两次观测(O1 和 O2)并将观测结果发送给第三个线程,则没有明确的顺序。T1 和 T2 没有互动,因此无法确定哪个观测结果“先发生”,并且序列没有明确的顺序。

下面是一个生成有序时钟观测序列的代码示例:假设有一个受互斥锁保护的全局观测列表(可能是内部调试日志)。许多不同的线程会定期锁定互斥锁,进行观测,将其附加到全局列表,最后解锁互斥锁。该列表现在包含多个观测者所做的观测结果的有序序列,顺序由互斥锁的独占性质确定。此有序观测序列必须与时钟的单调性和连续性属性保持一致。

可从 Starnix 客户端以“经济实惠的运行时费用”读取。

虽然不是 Linux 要求,但仍是一项要求。Starnix 受限模式进程必须能够以“非常廉价”的方式观察 CLOCK_MONOTONIC 时钟。请注意,Starnix 客户端目前使用 Zircon 内核时钟对象向其客户端提供 UTC。它们最初会退出受限模式并进入 Starnix 内核,然后发出进入 Zircon 内核的 Zircon 系统调用。

既需要退出受限模式又需要进入 Zircon 内核,最终导致了过多的开销,因此系统经过优化,让 Starnix 尝试保留时钟对象转换的缓存副本,从而允许查询时钟,而无需为每个查询都进入 Zircon 内核。虽然这有所改进,但仍需要 Starnix 客户端退出受限模式,并且在维护时钟转换的缓存副本以及与非 Starnix 程序的观测结果进行比较时,会产生其他复杂情况,导致结果不一致。

设计

从高层来看,此处的设计是使用 Zircon 内核时钟对象来管理 Starnix 范围内的 Linux CLOCK_MONOTONIC 时间轴概念,并扩展对象 API,以便能够以非常低的成本观测时钟,并以一种使 Starnix 客户端甚至无需退出受限模式的方式来完成。

除了永不离开受限模式这一性能目标之外,Zircon 内核时钟已经满足上述要求。下面我们就来快速回顾一下每种方法。

可维护性

内核时钟是内核对象,通过带有权限的句柄进行管理,因此本质上是可委托的。这为 Starnix 提供了多种选项,以便维护表示 Linux CLOCK_MONOTONIC 定义的时钟。

1) 时钟可由组件管理器创建,并分发给 Timekeeper 进行维护,分发给 Starnix 供其使用。 2) 时钟可由 Timekeeper 创建和维护,并由 Starnix 通过 FIDL 接口从 Timekeeper 中提取以供使用。 3) 时钟可由 Starnix 创建,并通过 FIDL 接口发送到 Timekeeper 进行维护。 4) 完全不同的其他原因。

这里的主要优势在于,由于它是内核对象,因此可以将时钟维护工作委托给当前也在维护 UTC 时钟的 Timekeeper,从而相对简单地将对 Fuchsia UTC 时钟进行的相同速率调整应用于 Linux CLOCK_MONOTONIC 时钟。

一致性

Zircon 内核时钟已保证上述定义的多观察者一致性。内核将不允许任何可能产生不一致观测结果(由创建时钟时使用的属性定义)的更新操作发生。因此,无论维护者多么有缺陷或多么恶意,都无法导致观测者看到不一致的序列。

同样,内核中用于观测时钟的协议可保证观测结果的一致性。只要对 API 的任何添加都遵循相同的协议,就可以保证一致性。

可接受的观测成本

这使我们能够以可接受的性能代价来观察内核时钟对象,从而进入设计的最重要部分。这里的主要目标是设计一种读取时钟的方法,该方法不需要进入 Zircon 内核(从而消除开销),并且还可以以一种不需要受限模式进程退出受限模式的方式来实现(无论该 syscall 是否进入 Zircon 内核,受限模式进程都需要退出受限模式才能发出 Zircon syscall)。

可映射状态

了解内核时钟对象如何执行其功能可能会有所帮助,详细信息请参阅此处的公开文档。

不过,最重要的是要了解,内核时钟的状态实际上只是一个仿射函数,它将所选的参考时钟(Zircon 单调时钟或启动时钟)转换为时钟所代表的合成时间轴的当前值。要对时钟对象进行一致的观测,需要成功执行“读取事务”,即记录转换的当前状态以及参考时钟的当前值,并确保在事务期间未对合成时钟的转换状态进行任何更改。

截至今天,时钟的状态存储在内核内存中,无法从用户模式直接访问。用户模式可能会使用 zx_clock_get_details 系统调用来获取时钟状态的最新版本,但这不仅需要系统调用,而且信息在系统调用返回时可能已经过时,从而导致观测序列不一致。

不过,此状态并没有什么特别之处。对时钟具有读取权限的用户已经可以获取时钟的有效转换的详细信息。应该可以消除程序进入 Zircon 内核来观察时钟详细信息的需要,我们只需要扩展时钟对象实现,以允许将时钟的状态存储在可以映射(只读)到用户模式进程的 RAM 页面中。采用这种方法,用户模式可以将时钟状态映射到其进程中,并且只要遵循观测协议,并且只要可以直接访问参考时钟而无需进入 Zircon 内核,就可以进行一致的观测,而无需进入内核的额外开销。

在用户模式下观察时钟

在用户模式下观察内存映射时钟需要遵循非常具体的协议,该协议必须始终与内核的行为保持一致。此外,遵循该协议还需要了解映射页面中时钟状态数据的具体布局。Zircon 必须能够更改观测协议或共享内存布局,而无需与客户端程序协调,也无需经历完整的正式弃用周期。

因此,用户必须将指向映射时钟状态的指针视为指向不透明 blob 的指针,并将其传递给 Zircon vDSO 中实现的 Zircon 系统调用。请注意,这些系统调用几乎永远不需要实际进入 Zircon 内核才能读取时钟。唯一需要进入内核的时间是时钟对象的参考时钟只能通过进入内核来读取时(这种情况目前极为罕见)。故意使共享数据格式不透明,并将读取实现放入 Zircon vDSO 中实现的 Zircon 系统调用中,这样一来,用户无需进入 Zircon 内核即可获得性能优势,同时还能够更改结构和读取协议的详细信息,而无需更改 ABI 合约。

不过(如前所述),Starnix 客户端以受限模式运行,因此无法发出 Zircon 系统调用,即使是不会进入 Zircon 内核的系统调用也不行。请注意,对于想要访问现有 Zircon 系统调用的受限模式客户端来说,这已经是一个问题,因为这些系统调用通常永远不会进入 Zircon 内核,最明显的例子是 zx_clock_get_monotonic()

目前,Starnix 针对这些情况使用了一种特殊的解决方法。创建了一个通用库 (libfasttime),用于实现 get_monotonic 的逻辑已移至该库。Zircon vDSO 在提取单调引用不需要进入内核时,使用此库实现 zx_clock_get_monotonic(),否则会回退到完整的内核条目。

Starnix 对其受限模式客户端执行类似操作,以便让它们访问 Zircon 单调引用,使用 libfasttime 中的代码在受限模式下实现实际操作,而无需退出受限模式来实现 zx_clock_get_monotonic() 的功能。此处的计划是简单地扩展此技术,将内存映射的时钟读取操作放入 libfasttime 中,然后在所有位置使用此实现。

可以理解的是,这意味着 Starnix 与 Zircon 内核之间存在依赖关系。具体而言,每次 libfasttime 中的内容发生更改时,都需要同时重新构建和部署 Starnix 和 Zircon。我们正在努力从长远角度消除这种依赖关系,新的时钟读取操作将在最终实现这些努力时加以利用。扩展现有技术不会使情况比现在更糟,因此被认为是可接受的方法。

时钟读取协议详细信息。

下文介绍了目前如何以保证一致性的方式观测内核时钟,以及如何在 libfasttime 中初步实现这种观测方式。包含此信息是为了帮助您直观地了解这一切是如何在“幕后”运作的,但不应将其视为内核 ABI 合约的一部分。

在内部,内核时钟对象使用一种称为 SeqLock 的特殊类型的锁来保护其状态。在实现无系统调用的时钟观测方式时,这些锁具有一些有用的属性。值得注意的是:

1) SeqLock 允许并发读取,但读取事务永远无法阻止写入事务。因此,用户模式程序无法阻止其他用户模式程序读取时钟,也无法阻止内核中正在更新时钟的代码。 2) SeqLock 读取事务不需要改变状态。因此,如果锁定状态是映射时钟状态的一部分,即使该状态映射为只读,也可以在用户模式下使用锁定。 3) SeqLock 读取事务绝不会阻塞读取器线程,这意味着用户模式下不需要任何 futex 系统调用。此外,它们无法阻止读取或写入事务同时进行,因此无需停用中断(在用户模式下无法执行此操作)。

从宏观层面来看,正确观测时钟非常简单。序列将如下所示:

1) 使用 SeqLock 开始读取事务,以保护映射的时钟状态。 2) 将时钟状态复制到本地堆栈分配的缓冲区。 3) 读取参考时钟值。 4) 结束读取事务,如果事务被中断,则重新尝试,直到成功为止。 5) 使用保存的参考时钟值和时钟状态计算合成时间值。

不过,深入挖掘后会发现,情况并非如此简单。SeqLock 事务需要对锁的状态和受保护的载荷的状态使用原子加载,并且可能需要显式栅栏指令,具体取决于载荷的处理方式(AcqRel 原子与 Relaxed 原子)。更复杂的是,作为参考时钟基础的硬件计数器寄存器不是内存,并且在排序和流水线执行方面不遵循 C++ 内存模型。需要使用特殊的架构和特定于计时器的指令,以防止此寄存器的读取“滑出”事务。最后,将参考时钟值转换为适当的合成时钟值所用的方法需要在所有用户之间保持一致,否则,舍入误差可能会导致使用不同转换技术的观测者观测到的序列出现不一致的情况。

为了让所有内容都显示在同一页面上,此设计要求对代码进行重构,如下所示:

1) zx_clock_readzx_clock_get_details 操作的核心将从内核代码移至 libfasttime,并且内核代码将重构为使用 libfasttime 实现。 2) 将在时钟状态(已映射到本地地址空间)上运行的 zx_clock_readzx_clock_get_details 版本作为 Zircon 系统调用添加到 Zircon vDSO。这些 vDSO 调用的实现将基于 libfasttime 实现,确保每个人都使用正确匹配的实现。

API

支持可映射时钟的用户模式 API 包含三个新的系统调用,其中两个通常永远不会进入 Zircon 内核。此外,我们还将推出一个新选项,在创建时钟时需要传递该选项;同时还将推出一个新的 zx_object_get_info() 主题,以便用户了解时钟实例的映射大小。

时钟创建

创建可映射的时钟需要用户将新的 ZX_CLOCK_OPT_MAPPABLE 标志传递给 zx_clock_create 的 options 字段。当创建可映射时钟的调用成功时,时钟对象将在内部分配一个 VMO,并使用此 VMO 来存储其状态,而不是使用内部内核内存。此外,此成功调用返回的句柄将包含 ZX_RIGHT_MAP 以及默认的时钟权限。在未指定此选项的情况下创建的时钟的初始句柄将不包含相应权限。

确定映射时钟所需的空间

在此 RFC 草稿发布时,内核时钟状态的当前大小(包括对齐填充)为 160 字节。这可以轻松容纳在单个 4KB 页面中,并为未来的增长留出充足的空间。

不过,简单地假设映射的时钟的大小现在和将来始终正好是一个 4KB 页面是不明智的,原因有很多。

1) 系统的底层页面大小可能会在将来发生变化,例如从 4KB 变为 16KB 甚至 64KB。 2) 时钟对象的状态可能需要大幅增加,甚至可能超过单个页面(可根据要求提供理论示例)。 3) 开发者可能需要临时扩展映射时钟表示的大小,作为调试工作的一部分,这需要维护更多状态。即使较大的时钟表示形式从未向任何最终用户发布,但作为实验的一部分,增大时钟大小最好不会破坏现有代码。

因此,映射时钟的大小不一定恒定,希望将时钟状态映射到其地址空间的用户需要知道时钟状态的大小,以便正确管理其地址空间。

为了解决这个问题,我们将添加一个新的信息主题 (ZX_INFO_CLOCK_MAPPED_SIZE)。如果与 zx_clock_get_info() 结合使用,且时钟已使用 ZX_CLOCK_OPT_MAPPABLE 标志成功创建,则映射的时钟状态的大小(以字节为单位)将以单个 uint64_t 返回。

尝试提取不可映射的时钟对象的时钟映射大小会被视为错误,并将返回 ZX_ERR_INVALID_ARGS;而尝试提取非时钟类型的对象的映射时钟大小将返回 ZX_ERR_WRONG_TYPE

映射和取消映射时钟

将添加一个新的系统调用 (zx_vmar_map_clock),允许用户将内部 VMO 实际映射到其地址空间,以便将其用于 readget_details 操作。

zx_status_t zx_vmar_map_clock(zx_handle_t handle,
                              zx_vm_option_t options,
                              uint64_t vmar_offset,
                              zx_handle_t clock_handle,
                              uint64_t len,
                              user_out_ptr<zx_vaddr_t> mapped_addr);

此操作与 zx_vmar_map()zx_vmar_map_iob() 几乎完全相同,但有几个重要区别。

1) 允许使用大多数(但并非全部)可与 zx_vmar_map() 搭配使用的选项。特别是,用户不得指定 ZX_VM_PERM_WRITEZX_VM_PERM_EXECUTEZX_VM_PERM_READ_IF_XOM_UNSUPPORTED。时钟的状态不能允许用户模式客户端更改(因此,没有 WRITE),当然也不是代码(因此没有 EXECUTE)。任何使用这些选项的尝试都会导致 ZX_ERR_INVALID_ARGS 错误。所有其他选项,尤其是与 VMAR 中地图定位相关的选项,均受支持。 2) 需要 len(与其他 VMAR 映射操作一样),但要映射的量不是任意的,也不受用户模式控制。给定时钟实例的映射长度必须与使用新引入的 ZX_INFO_CLOCK_MAPPED_SIZE 主题提取信息时报告的大小相匹配。为 len 传递的任何其他值都被视为错误。3) 未提供 vmo_offset 参数。用户必须始终映射所有时钟状态。不允许从 VMO 的偏移量开始的部分时钟状态映射,这使得 vmo_offset 无用。4) 未提供 region_index 参数,与 zx_vmar_map_iob() 相同。 IO 环形缓冲区对象可能具有多个可独立映射的区域,但时钟对象没有,这意味着索引参数将毫无用处。

将时钟的内部 VMO 保留在内核内部意味着,我们不必指定或测试时钟的 VMO 在传递给任何其他 VMO 系统调用时的行为。

取消映射时钟状态与取消映射用户模式所做的任何其他映射相同。用户只需调用 zx_vmar_unmap(),并传递其映射操作返回的虚拟地址以及原始映射中使用的 len

用户可以在映射时钟句柄后随意关闭该句柄。映射将保留,并且仍可用于读取时钟的状态。如果映射时钟的所有句柄都已关闭,并且时钟对象已销毁,则内部 VMO 和任何剩余的映射都将保留。不过,此时,任何程序都无法继续“维护”时钟的转换,因为该对象已被销毁,现在没有任何方法可以更新转换。从概念上讲,时钟将继续存在,并在之后永远漂移。请注意,这与时钟维护者关闭其负责维护的时钟的最后一个句柄,而时钟的消费者保留其句柄的情况并无不同。

观察时钟。

zx_status_t zx_clock_read_mapped(const void* clock_addr, zx_time_t* now);

zx_status_t zx_clock_get_details_mapped(const void* clock_addr,
                                        uint64_t options,
                                        void* details);

为了执行时钟观测,用户可以调用相应的新系统调用,即 zx_clock_read_mappedzx_clock_get_details_mapped

这些系统调用将在 Zircon vDSO 中实现,并且只有在访问底层参考时钟需要时才会进入 Zircon 内核。

这些函数本身的操作方式与未映射的对应部分(zx_clock_readzx_clock_get_details)相同,但有以下例外情况。

1) 用户必须传递时钟状态在其当前地址空间中映射到的地址,而不是将时钟句柄作为第一个实参传递,同时该状态仍处于映射状态。传递任何其他值,或传递已取消映射(完全或部分)的时钟状态的地址,都会导致未定义的行为。 2) 调用这两个例程中的任何一个时,都不会进行权限检查。用户自然而然地拥有读取映射时钟的权限,因为其状态已成功映射到用户的地址空间。

如前所述,Starnix 客户端在受限模式下运行,无法直接使用这些系统调用。为了避免需要退出受限模式,Starnix 可能会以类似于当前使用 libfasttime 的方式来使用它,以实现相当于 zx_clock_get_monotonic() 的功能,直到开发出一种更好的技术来避免 Starnix 依赖于 libfasttime

zx_info_maps_t 结构中的时钟映射表示

Zircon 目前为开发者提供了几种不同的方式来获取有关当前有效映射的信息,以用于诊断目的。两者均使用 zx_object_get_info() 主题进行访问:

++ ZX_INFO_VMAR_MAPS ++ ZX_INFO_PROCESS_MAPS

第一个主题列举了给定 VMAR 中的一组有效映射,而第二个主题则针对进程对象执行相同的操作。无论哪种情况,结果都会以 zx_info_maps_t 结构数组的形式返回。现在,时钟对象本身可以创建映射,我们需要能够回答一些问题,即映射时钟的枚举记录中将包含哪些信息。

1) 名称字段中应填写什么内容?内核时钟目前没有可分配的名称,因此暂时在枚举时钟映射时,名称将设置为常量字符串“kernel-clock”,以帮助查看诊断数据的人员。如果(将来)内核时钟扩展为允许用户配置名称,那么显而易见的下一步就是报告用户分配的名称,而不是常量名称。 2) 记录中联合的“类型”是什么?按照映射的 IOB 区域的示例,类型将为 ZX_INFO_MAPS_TYPE_MAPPING,并且将填充 u.mapping 的成员。 3) u.mapping.vmo_koid 字段中报告的值是多少? 虽然内核时钟在内部使用 VMO 来获取通过映射与进程共享时钟状态所需的页面,但此 VMO 未封装在 VmObjectDispatcher 中,因此未分配 KOID。因此,系统会报告时钟对象本身的 KOID。

实现

此功能的实现将以一系列 Gerrit CL 的形式提交,主要是为了使代码审核规模合理且易于管理。要提交的 CL 如下所示。

1) 将为上述 3 个调用向 Zircon vDSO 添加 API 桩。 这些桩的初始实现将仅返回 ZX_ERR_NOT_SUPPORTED。它们最初会添加到 @next API 下。 2) readget_details 实现将移至 libfasttime,并且内核代码将更改为使用这些实现,而不是使用自己的实现。 3) zx_clock_create 的更新行为、zx_clock_get_vmo 的实际实现以及对 ZX_INFO_CLOCK_MAPPED_SIZE 主题的支持将添加到内核中。4) zx_clock_read_mappedzx_clock_get_details_mapped 的实际实现将添加到 Zircon vDSO 中。 5) 将 C++ 特有的封装容器添加到 libzx。 6) 将针对整个功能的测试添加到现有的 Zircon 内核时钟测试中。有关该功能和新系统调用的文档将添加到现有的公开文档中。

至此,实现已完成。现在,可以向 Rust 添加更多特定于语言的绑定,并且 Starnix 现在可以利用 libfasttime 中的功能和代码,为其受限模式客户端提供对 Zircon 时钟的高效访问。

经过一段时间的使用和稳定后(在此期间可能会对 API 进行适当的更改),该 API 将从 @next 中移除,并升级为当前 Zircon Kernel API 的成员。

性能

从 Zircon 进程访问内存映射的内核时钟的成本预计会比通过系统调用访问的成本略低,因为进入 Zircon 内核和执行句柄验证的所有成本都已消除。由于在句柄验证期间不再需要锁定进程句柄表以进行读取,因此这也会在一定程度上减轻进程句柄表的压力。

这样做的代价是内存使用量会略有增加,因为我们现在需要一个内存页来存储时钟状态,而不是像现在这样使用大约 160 字节的内核堆内存来跟踪时钟状态。此外,还需要少量内核簿记来记录 VMO 对象的内核表示,以及映射选择这样做的进程中的时钟所需的页表开销。

预计对 Starnix 受限模式进程的影响会更大。客户端不再需要退出受限模式并进入 Starnix 内核才能访问时钟对象。此外,Starnix 内核将不再需要监控系统范围的 UTC 是否发生变化,并更新其维护的内部缓存版本,以避免发出系统调用来读取时钟。相反,受限模式客户端可以直接读取时钟,而无需退出当前上下文并进入 Starnix 或 Zircon 内核。

工效学设计

总体 API 人体工程学设计应与现有 API 几乎完全相同。只需少量设置(获取和映射 VMO),用户现在就可以像以前一样读取 Zircon 内核时钟,只需将时钟状态指针替换为他们本应传递给原始读取例程的时钟句柄即可。

下面是一个小示例,用于演示 API 差异。

// An object which creates a non-mappable kernel clock and allows users to read it.
class BasicClock {
 public:
  BasicClock() {
    const zx_status_t status = zx::clock::create(ZX_CLOCK_OPT_AUTO_START,
                                                 nullptr,
                                                 &clock_);
    ZX_ASSERT(status == ZX_OK);
  }

  zx_time_t Read() const {
    zx_time_t val{0};
    const zx_status_t status = clock_.read(&val);
    ZX_ASSERT(status == ZX_OK);
    return val;
  }

 private:
  zx::clock clock_;
};

// And now the same thing, but with a mappable clock.  There are a few extra
// setup and teardown steps, but the read operation is virtually identical.
// An object which creates a non-mappable kernel clock and allows users to read it.
class MappedClock {
 public:
  MappedClock() {
    // Make the clock
    zx_status_t status = zx::clock::create(ZX_CLOCK_OPT_AUTO_START | ZX_CLOCK_OPT_MAPPABLE,
                                           nullptr,
                                           &clock_);
    ZX_ASSERT(status == ZX_OK);

    // Get the mapped size.
    status = clock_.get_info(ZX_INFO_CLOCK_MAPPED_SIZE, &mapped_clock_size_,
                             sizeof(mapped_clock_size_), nullptr, nullptr);
    ZX_ASSERT(status == ZX_OK);

    // Map it.  We can close the clock now if we want to, just need to remember to un-map our
    // region when we destruct.
    constexpr zx_vm_option_t opts = ZX_VM_PERM_READ | ZX_VM_MAP_RANGE;
    status = zx::vmar::root_self()->map(opts, 0, clock_vmo, 0, mapped_clock_size_, &clock_addr_);
    ZX_ASSERT(status == ZX_OK);
  }

  ~MappedClock() {
    // Un-map our clock.
    zx::vmar::root_self()->unmap(clock_addr_, mapped_clock_size_);
  }

  // The read is basically the same, just pass the mapped clock address instead of using the handle.
  zx_time_t Read() const {
    zx_time_t val{0};
    const zx_status_t status = zx::clock::read_mapped(reinterpret_cast<const void*>(clock_addr_),
                                                      &val);
    ZX_ASSERT(status == ZX_OK);
    return val;
  }

 private:
  zx::clock clock_;
  zx_vaddr_t clock_addr_{0};
  size_t mapped_clock_size_{0};
};

向后兼容性

对于可以直接进行 Zircon 系统调用的程序,无需考虑向后兼容性问题。现有时钟功能不会发生任何变化,并且 Zircon vDSO 的性质使我们无需担心对状态结构的布局或所需的时钟观测协议做出任何长期承诺。

不过,Starnix 将依赖于内核(或更准确地说,依赖于 libfasttime)。对结构布局或观测协议所做的任何更改都将在 libfasttime 库中更新,并且需要重新编译 Starnix。目前,我们认为这并不是一个严重的问题,原因有以下两点。

1) 鉴于 Starnix 客户端在受限模式下获得 Zircon 单调时间和启动时间轴的无系统调用访问权限的方式,这已经是当前的情况。换句话说,这并不是新的依赖项,只是对已在执行的操作的延续。 2) 从长远来看,我们正在考虑如何消除预先存在的依赖关系。如果这些想法最终实现,那么这个新 API 将像 zx_clock_get_monotonic 一样受益于这些改进。

安全注意事项

允许用户将时钟状态(只读)映射到其地址空间中,不会带来已知的安全隐患。用于观测时钟的协议不允许用户模式独占锁定时钟(这会使我们面临各种 DoS 攻击)或修改时钟状态(这可能会使内核保证的时钟属性失效)。时钟状态中包含的所有信息都可以通过调用 zx_clock_get_details() 从用户模式访问,此新功能只是对我们获取这些信息的方式进行优化。

添加直接访问内核时钟状态的功能不会引入访问高分辨率计时器的额外能力。时钟只是对现有参考的转换,因此进程可用的底层参考时钟分辨率的任何潜在未来限制也必然会影响从这些参考派生的合成时钟。

隐私注意事项

此提案没有已知的隐私权影响。此处不涉及任何用户数据。

测试

我们将通过以下两种方式扩展 Zircon 时钟单元测试,以实现核心功能测试。

1) 将添加测试,以确保强制执行涉及创建、映射和取消映射可映射时钟的规则(如上所述)。 2) 针对 zx_clock_readzx_clock_get_details 的现有测试将扩展为同时测试这些调用的 mapped 版本。它们的行为应该几乎完全相同,因此测试结果也应该如此。

文档

1) 顶级公开文档将更新,以包含该功能的一般说明以及指向新添加的 API 方法的链接。此外,它应至少包含一个创建、映射、读取和取消映射时钟的示例。2) zx_clock_create 将扩展为描述新的 ZX_CLOCK_OPT_MAPPABLE 标志。 3) 将向时钟参考文档添加新页面,以描述 zx_clock_get_vmozx_clock_read_mappedzx_clock_get_details_mapped

缺点、替代方案和未知因素

创建可映射时钟的替代方案

除了明确禁止上述所有禁止的行为(并为该代码编写测试)之外,实现成本相对较低。进行少量重构,将观测例程移至通用位置;并对内核中的 ClockDispatcher 代码进行少量重组,使其除了内部存储空间之外,还能使用 VMO 作为存储空间。然后,只需进行功能测试和文档编制。

虽然有替代方案,但需要付出更多工作,并且最终会生成非常具体的解决方案,而不是通用工具。

例如,Starnix 可以创建和管理自己的 Linux CLOCK_MONOTONIC 概念,然后将其公开给受限模式进程,方法是使用类似的内存映射技术,或者强制受限模式客户端退出受限模式进入 Starnix 内核。不过,这仍然会留下一些需要解决的问题。

1) 锁定以实现高读取并发。SeqLock 非常酷,但在用户模式下使用它们进行读取交易很容易,而在用户模式下锁定它们以进行独占写入则非常冒险,因为无法停用中断或以其他方式防止非自愿抢占,这可能会导致更新操作在被抢占时阻止读取操作向前推进。2) 获取时钟校正信息。Starnix 想要创建的时钟应该受到与系统级 UTC 参考相同的速率调整,但这些调整是由 Timekeeper 执行的,而不是由 Starnix 执行的。复制 Timerkeeper 所做的工作(除了是冗余工作之外)需要大量工作,因此 Timerkeeper 可能需要将校正信息转发到 Starnix 维护的时钟,而 Starnix 可能需要向 Timekeeper 提供反馈信息,以关闭时钟恢复环路。3) 确保正确的内存顺序语义。根据用于第 1 步的解决方案,需要注意确保在将时钟寄存器观测结果包含在读取事务中的同时,可以观测到时钟状态载荷,而不会出现任何正式的数据竞争。这可能有点棘手,而且很难正确设置和维护。

因此,如果 Starnix 愿意,它可以完成很多此类工作,但这样做会造成相对较大的重复工作量,因为内核时钟目前已经可以完成这些工作。从长远来看,这也意味着需要进行更多测试,并满足更多长期维护要求。

libfasttime 抽象的替代方案

为了向 Starnix 受限模式客户端提供承诺的性能优势,受限模式客户端程序中需要(以某种方式)存在代码,该代码既要了解时钟状态的布局,又要遵循对时钟状态进行一致观测所需的协议。

此提案通过提供 libfasttime 形式的代码来满足此要求,客户端将该代码嵌入到其程序中,以便观察其内存映射时钟。这种方法的明显(且重大)缺点是,它会在无法进行 Zircon 系统调用的客户端之间创建依赖关系,但这些客户端希望使用内存映射时钟功能。如果内核以导致 libfasttime 发生变化的方式发生变化,客户端必须针对新库重新构建,否则可能会出现不兼容的情况。即使这是一个预先存在的问题(在 zx_clock_get_monotonic() 的等效实现中已存在),也不会改变这一事实。

另一种做法可能是正式确定 Zircon 公开的 ABI,严格记录时钟状态的内存布局以及对时钟进行一致观测所需的协议,从而允许客户端实现和维护自己的“可映射的时钟读取”例程。

不过,这样做的结果是:

1) 更改时钟状态布局或用于观察时钟状态的协议所需的难度大幅增加。 基本上,对该功能的 Zircon 端进行更改会变得更加困难。 2) 客户的责任负担大幅增加。他们现在不仅要使用(可能经过测试和维护的)库代码,还要负责阅读并完全理解假设的新 API 规范。此外,他们还需要实现、测试和维护自己的观测例程版本。

因此,虽然这是一种替代方法,但由于上述两个重大缺点,我们目前并未认真考虑采用这种方法。预计未来会推出其他方法来解决客户端(类似于 Starnix 受限模式客户端)的这一问题,这些方法可实现内核灵活性,而不会给客户端带来过多的实现和维护负担。

ZX_CLOCK_OPT_MAPPABLE 的替代方案

与其要求时钟的创建者决定给定时钟是否可映射,不如在需要时动态创建时钟状态 VMO。例如,如果用户调用 zx_vmar_map_clock() 且尚未分配任何 VMO,只需动态分配 VMO 并将其换入内核堆状态即可。

请注意,此转换是单向的。一旦时钟可映射,我们就无法合理地再让时钟不可映射,因为我们无法很好地控制映射的客户端当前是否存在,也无法合理地强制它们改回。

当然,您可以编写代码来实现此目的,但需要解决竞态条件问题。需要在 zx_vmar_map_clock 周围引入锁(或其他序列化机制),以防止需要 VMO 分配的并发请求相互竞争。 换入 VMO 存储空间,替换内核中的状态,实际上就是一次时钟更新,需要在状态从一个位置移动到另一个位置时进行序列锁写入事务。

虽然这可行,但需要编写更多代码,增加复杂性,并进行更多测试。 此外,严格的测试还意味着要测试是否能正确处理并发请求导致的竞态条件,而这对于单元测试等来说,实际上无法确定性地完成。

总而言之,目前认为要求用户在创建时钟时选择启用“可映射性”是一项合理且不会过于繁琐的要求,这样可以避免需要处理后期绑定请求以实现可映射性的复杂性。

zx_vmar_map_clock() 的替代方案

在此提案中,内核时钟对象获得了成为“可映射对象”的能力,加入了 VMO 和 IOB 区域的行列。与 IOB 区域一样,它们会被分配自己的特殊系统调用来映射其底层 VMO,但 VMO 本身永远不会直接提供给用户模式。

此方法的替代方案是跳过专门的映射调用,而是向用户提供对底层 VMO 本身的直接访问权限。因此,请移除 zx_vmar_map_clock() 系统调用,并改为添加 zx_clock_get_vmo() 调用。

优点

假设同样的方法可以应用于 IOB 区域(内核中其他可映射的非严格 VMO 对象),这意味着系统将不再需要将除 VMO 之外的任何内容视为可映射的对象。这种一致性有助于用户理解和推理系统。

++ "What objects are 'mappable' in Zircon? 非常简单!VMO,其他一概不包括”。 ++ “我可以使用‘时钟 vmo’做什么?非常简单!您账号的权限允许您执行的任何操作。

“在诊断映射查询中,我应报告哪个 KOID?”之类的问题变得毫无意义。一个只是报告 VMO 本身的 KOID。现在,它已正式作为内核调度器对象存在(“在后台”),我们可以直接使用分配给调度器的 KOID。同样,VMO 是可命名的对象,这意味着要报告的名称现在也很明显。

另一个好处是,这可能会让开发者更容易遵守前面提到的“您必须准确映射我们要求的大小”这一要求。不过,此要求并未消失,但可以指定 zx_clock_get_vmo() 系统调用,使其除了返回 VMO 的句柄之外,还返回所需的映射大小。这样一来,用户必须知道大小才能获取 VMO(这是首先映射时钟状态的硬性要求),因此很容易满足该要求。

最后,直接公开 VMO 为系统上层提供了共享时钟的替代方法。他们可以复制并分享时钟本身的句柄(具有适当的权限),但现在也可以简单地复制底层 VMO 的句柄(同样具有适当的权限)并分享该句柄。

缺点

不过,直接公开 VMO 并非完全没有缺点。允许映射时钟状态的目的是非常具体的。从用户的角度来看,映射的 VMO 内容甚至未定义。用户唯一需要做的就是使用映射的地址作为令牌,将其传递给开销低于传统对应项的专用系统调用。

不过,公开 VMO 本身会公开大量额外的内核 API 接口。如果用户可以直接访问底层 VMO,那么他们可能会尝试执行大量在功能上毫无意义的操作,但现在这些操作却成为可能。

++ 用户能否创建时钟状态 VMO 的 CoW 克隆,或者创建任何子级?这意味着什么? ++ 用户能否控制时钟状态 VMO 的缓存政策? ++ 如果用户尝试设置时钟状态 VMO 的大小,会发生什么情况?“流”大小如何? ++ 用户可以请求各种 op_range 操作,那么这些操作又该如何处理?它们是否能提供任何价值?它们是否会带来任何安全威胁? ++ 谁控制 VMO 对象的属性? 任何人都可以设置 VMO 的名称,还是应该限制? 如果应限制,应如何限制?

现在可供用户使用的一些操作同时也是毫无意义且无害的;例如 zx_vmo_read()。用户永远不需要这样做,因为要读取的内容甚至未针对用户定义(按设计)。也就是说,允许他们这样做有危险吗?从技术上讲,是的,因为允许它们在不遵循序列锁定协议的情况下读取数据可能会导致正式的数据竞争,因为时钟是并发更新的,但实际上不会。它们可能会从读取中获得垃圾数据,但系统不会停止并发生故障。

不过,对于许多其他操作,答案并不那么明确。CoW 克隆几乎肯定应该被禁止,但对象名称呢?

需要根据具体情况评估每项潜在操作,以确保从安全和隐私角度来看,允许执行某项操作是安全的。出于谨慎考虑,我们预计大多数此类操作都会被禁止。如果这些操作没有实际意义,则默认立场应为禁止这些操作。

这意味着现在需要强制执行这些限制。其中一些可以使用适当的句柄权限来完成,但另一些可能需要自定义强制执行机制。无论采用何种机制,内核中都会存在需要强制执行这些限制的代码(其中一些是新代码)。您需要编写测试,以确保限制已到位,并且正在得到正确执行。测试和强制执行代码也需要永久维护,不仅要维护现有 API,还要维护未来添加到系统中的任何 VMO API。

绝大多数使用映射时钟的用户对上述任何内容都不感兴趣。它们只是想以优化的方式访问时钟对象,并不想设置映射的缓存政策,也不想创建时钟状态的子克隆(无论这意味着什么)。用户可能想要设置映射的名称,但通过命名时钟对象本身或命名 VMO 都可以轻松实现这一点。

很难忽视这样一种观点:让用户访问此 API 界面区域对使用映射时钟的合法用户没有任何好处,但可能有利于试图在公开的 API 界面区域中寻找漏洞的恶意行为者。分析这些潜在攻击、缓解这些攻击以及在系统未来发展过程中强制执行这些缓解措施的成本尚未完全了解,但可以肯定的是,这些成本几乎肯定不小。

总结

目前,我们决定沿用 IOB 区域的先例,使时钟对象本身“可映射”,从而大幅缩减公开的 API 表面积,并有望降低该功能及其维护的总体复杂性。

未来,我们可能会重新评估此决定,或许会决定通过一个模型来统一这两个时钟和 IOB 区域,在该模型中,底层 VMO 会直接公开,而不是“除了 VMO 之外还可以映射的特殊事物”,但目前,系统只会提供非常有限的功能来控制共享状态的使用方式。

在先技术和参考资料

当前 API

序列锁