| RFC-0266:可映射内存的内核时钟 | |
|---|---|
| 状态 | 已接受 |
| 区域 |
|
| 说明 | 为用户模式进程提供了一种无需进入 Zircon 内核即可读取内核时钟状态的方法。 |
| 问题 | |
| Gerrit 变更 | |
| 作者 | |
| 审核人 | |
| 提交日期(年-月-日) | 2024-11-21 |
| 审核日期(年-月-日) | 2025-02-14 |
问题陈述
Starnix 需要一种方法来为其客户端程序提供行为与 CLOCK_MONOTONIC 的 Linux 定义相匹配的时钟。此时钟需要表示一个单调时间轴,该时间轴会经历与系统级 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
Reviewers:
- 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 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.
可维护性
Linux 对 CLOCK_MONOTONIC 的定义要求,为维持系统级 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) 完全不同的内容。
这里的主要优势在于,由于它是内核对象,因此时钟的维护可以委托给 Timekeeper,后者目前也维护 UTC 时钟,因此可以相对简单地将对 Fuchsia UTC 时钟所做的相同速率调整应用于 Linux CLOCK_MONOTONIC 时钟。
一致性
Zircon 内核时钟已保证多观察器一致性(如上所述)。内核不允许执行任何可能导致观测结果不一致(根据创建时钟时所用的属性定义)的更新操作。因此,无论维护者多么有缺陷或恶意,都无法让观察者看到不一致的序列。
同样,内核中用于观察时钟的协议可保证观察结果的一致性。只要对 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 页面中,并为未来增长留出充足的空间。
也就是说,出于多种原因,简单地假设映射时钟的大小现在和将来始终正好是一个 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) 将向 Zircon vDSO 添加 API 桩,以实现上述 3 个调用。
这些桩的初始实现将仅返回 ZX_ERR_NOT_SUPPORTED。这些 API 最初将添加到 @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) C++ 专用封装容器将添加到 libzx。
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 执行的。复制 Timekeeper 的工作将是一项非常繁重的工作(除了冗余工作之外),因此 Timekeeper 可能需要将校正信息转发到 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? 非常简单!VMOs, and nothing else". ++“我可以用‘时钟 VMO’做什么?非常简单!授予您的句柄的权限允许您执行的任何操作。
诸如“在诊断映射查询中应报告哪个 KOID?”之类的问题将不复存在。一种是直接报告 VMO 本身的 KOID。现在,它已正式作为内核 Dispatcher 对象存在(“在后台”),我们可以直接使用分配给该 Dispatcher 的 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
序列锁