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 Kernel Clocks Objects 已经能够满足 CLOCK_MONOTONIC
的 Linux 版本的大多数要求。它们提供了一种构建用户模式定义的时间轴的方法,该时间轴在被多个观察器观察时始终保证(由内核保证)单调且连续,并且还可以委托给权威(在本例中为计时器)进行维护。
此 RFC 提出了一种方法,可允许以无需进入 Zircon 内核的方式读取 Zircon 内核时钟,从而使读取操作的开销非常低,前提是时钟对象的参考时钟也可以在无需进入 Zircon 内核的情况下读取。此外,这种读取时钟对象的方法将提供一种途径,让 Starnix 客户端无需退出受限模式即可读取时钟,从而使 Starnix 能够向其客户端发布 Linux CLOCK_MONOTONIC
和 UTC,而无需退出受限模式。
利益相关方
- Starnix 实现者
- 计时器维护人员
- 内核维护者
- 媒体维护人员
教员:
- 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 提供一种途径,以便向其受限模式进程提供表示 CLOCK_MONOTONIC
的 Linux 版本的时钟。虽然能够对时钟进行内存映射有次要好处,但此处所述的要求主要源于需要满足 Starnix 和 Linux 定义的要求。
内存映射时钟需要能够:
- 满足
CLOCK_MONOTONIC
的 Linux 要求 - 会受到与系统级 UTC 时钟相同的速率调整。
- 在多个观察器进行的有序观察序列中保持单调性和连续性。
- 以“可接受的运行时开销”从 Starnix 受限模式进程进行查询。
CLOCK_MONOTONIC
的 Linux 要求
根据 Linux clock_gettime
手册页,CLOCK_MONOTONIC
的要求如下。
A nonsettable system-wide clock that represents monotonic time since—as
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 may—depending on the architecture—return
identical (not-increased) time values.
可维护性
CLOCK_MONOTONIC
的 Linux 定义要求,为了维护系统级 UTC 定义,对 CLOCK_MONOTONIC
时间轴也要进行相同的速率调整。目前,表示系统 UTC 时间轴的时钟由组件管理器创建,并通过进程启动协议作为复制句柄(具有适当权限)分发给进程。一个名为 Timekeeper 的进程会收到对此时钟的句柄,并具有写入权限。它负责使用可用的外部参考来维护 UTC 时钟,以持续修正 UTC 的本地表示法。
鉴于上述 Linux 要求,系统中的某个组件需要将 Timekeeper 应用于系统范围的 UTC 时钟的速率调整应用于 Starnix 将向其客户端提供的 Linux CLOCK_MONOTONIC
版本。
一致性
如 Linux 要求所述,CLOCK_MONOTONIC
需要同时单调且连续。因此,观察时钟的线程绝不能看到时钟向后或向前跳转。换句话说,对时钟的观察必须与这些单调递增/连续属性保持一致。
虽然 Linux 手册页中并未明确说明,但我们也认为这项要求意味着时钟不仅从单个观察者的角度来看是一致的,而且还必须从“多观察者”的角度来看是一致的。换句话说,给定由多个线程进行的有序观察序列 (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 内核时钟已保证多观察器一致性(如上所定义)。内核不允许进行任何可能产生不一致观察结果(如时钟创建时所定义的属性所定义)的更新操作。因此,无论维护者存在多少 bug 或恶意,都无法导致观察器看到不一致的序列。
同样,内核中用于观察时钟的协议可保证一致的观察结果。只要 API 中的任何新增内容都遵循相同的协议,就可以保证一致性。
可接受的观察费用
这让我们来到了设计中最关键的部分,即能够以可接受的性能代价观察内核时钟对象。这里的主要目标是设计一种读取时钟的方法,该方法不需要进入 Zircon 内核(从而消除开销),并且还可以以一种方式实现,而该方式甚至不需要受限模式进程退出受限模式(为了发出 Zircon 系统调用,它需要执行此操作,无论该系统调用是否进入 Zircon 内核)。
可映射状态
了解内核时钟对象的运作方式会很有帮助,详情请参阅此处的公开文档。
不过,最重要的是要了解,内核时钟的状态实际上只是一个线性函数,用于将所选的参考时钟(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_read
和 zx_clock_get_details
操作的核心将从内核代码移至 libfasttime
,并且内核代码将重构为使用 libfasttime
实现。2) 针对已映射到本地地址空间的时钟状态运行的 zx_clock_read
和 zx_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 页面中,并且有足够的空间供未来增长。
尽管如此,出于多种原因,仅假设映射时钟的大小现在和将来始终是正好 1 个 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 映射到其地址空间,以便将其用于 read
和 get_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_WRITE
、ZX_VM_PERM_EXECUTE
或 ZX_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_mapped
或 zx_clock_get_details_mapped
)。
这些系统调用将在 Zircon vDSO 中实现,并且只有在访问底层参考时钟时才会进入 Zircon 内核。
这些函数本身的运作方式与其未映射的对应函数(zx_clock_read 和 zx_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) read
和 get_details
实现将移至 libfasttime
,并且内核代码将更改为使用这些实现,而不是使用自己的实现。3) 内核中将添加更新后的 zx_clock_create
行为、zx_clock_get_vmo
的实际实现以及对 ZX_INFO_CLOCK_MAPPED_SIZE
主题的支持。4) zx_clock_read_mapped
和 zx_clock_get_details_mapped
的实际实现将添加到 Zircon vDSO。5) 将向 libzx
添加 C++ 专用封装容器。
6) 现有的 Zircon 内核时钟测试中将添加针对整个功能的测试。我们将在现有公开文档中添加有关该功能和新系统调用的文档。
至此,实现已完成。现在,您可以向 Rust 添加其他特定于语言的绑定,Starnix 现在可以利用 libfasttime
中的功能和代码,为其受限模式客户端高效访问 zircon 时钟。
在经过一段时间的使用和稳定化(在此期间可以根据需要对 API 进行更改)后,该 API 将从 @next
中移出,并被提升为当前 Zircon 内核 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_read
和 zx_clock_get_details
的现有测试将扩展为同时测试这些调用的 mapped
版本。它们的行为应该几乎完全相同,因此测试结果也应该如此。
文档
1) 顶级公开文档将更新为包含该功能的一般说明,以及指向新添加的 API 方法的链接。此外,该文档应至少包含一个创建、映射、读取和取消映射时钟的示例。2) zx_clock_create
将扩展为描述新的 ZX_CLOCK_OPT_MAPPABLE
标志。3) 时钟参考文档中将添加新页面,用于介绍 zx_clock_get_vmo
、zx_clock_read_mapped
和 zx_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)视为可映射对象。这种一致性有助于用户了解和推理系统。
++ “Zircon 中哪些对象是可映射的?”很简单!VMO,仅此而已”。++“What can I do with a 'clock 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
序列锁