RFC-0012:Zircon 可舍弃内存

RFC-0012:Zircon 可舍弃内存
状态已接受
领域
  • 内核
说明

介绍了一种用户空间应用机制,该机制用于向内核表明某些内存缓冲区符合回收条件。

问题
Gerrit 更改
作者
审核人
提交日期(年-月-日)2020-10-27
审核日期(年-月-日)2020-12-02

摘要

此 RFC 介绍了一种用户空间应用指示机制的机制, 确保某些内存缓冲区符合回收条件。内核 在系统内存不足时 可用内存。

设计初衷

在 Zircon 等过度使用系统中,管理可用内存是一个复杂的问题, 其中允许用户应用分配比当前 是否可用。这是通过使用虚拟内存实现的 由物理页面作为其中的一部分延迟支持的对象 (VMO) 。

多估将在集群内任意时间点使用的 并根据时间再发送其他内存分配请求失败, 表的可用内存这可能会影响性能 供应用用于缓存目的。另一方面,如果低估了 内存占用过多资源,会导致我们快速用完 系统上的可用内存,从而导致出现内存不足 (OOM) 的情况。 此外,“免费”的定义但内存本身很复杂

Zircon 内核会监控可用物理内存量, 内存压力信号的变化情况。这些信号的目的是 允许用户空间应用缩容(或增加)其内存占用量 根据系统级可用内存级别进行计算。虽然这有助于防止系统 内存不足时,这些信号的发起者( 内核)并不理想。处理 应对内存压力;没有足够的上下文信息,说明需要多少内存 它们应该释放内核可以更好地了解全局内存使用情况 对系统的影响,还可以考虑其他形式的 可回收内存,例如由用户分页器支持且可逐出的内存。

此 RFC 提出了一种机制,使内核能够直接 以便在内存压力下收回用户空间内存缓冲区。有几个 优点:

  • 它可让您更好地控制逐出的内存量;内核可以 查看可用内存级别并仅逐出所需的内存。
  • 内核可以使用 LRU 架构来舍弃内存,这在 调整内存中的当前工作集。
  • 由于内存压力,用户空间有时可能缓慢地丢弃内存 信号。在某些情况下,系统恢复为时已晚。
  • 有时,为了响应内存压力而唤醒的用户空间客户端 需要更多内存。

设计

概览

可舍弃内存协议大致的运作方式如下:

  1. 用户空间进程会创建一个 VMO 并将其标记为 discardable
  2. 在直接访问 VMO (zx_vmo_read/zx_vmo_write) 之前,或者 通过其地址空间 (zx_vmar_map) 中的映射,该进程就会锁定 指示其正在使用中的 VMO。
  3. 该进程完成后会解锁 VMO,这表明其不再有效 所需的资源。内核会将所有已解锁的可舍弃 VMO 视为符合条件 并且会在内存紧张时随时将其舍弃
  4. 当进程需要再次访问 VMO 时,它会尝试锁定它。这个 现在可以通过以下两种方式中的一种来成功执行锁定操作。
    • 锁定可以成功,且 VMO 的页面仍然完整,即 内核尚未将其舍弃。
    • 如果内核舍弃了 VMO,锁定将成功,同时 以告知客户端其页面已被舍弃 它们可以重新初始化它或采取其他必要的操作。
  5. 此过程完成后,将再次解锁 VMO。上锁和开锁罐 根据需要随时以这种方式重复。

请注意,可舍弃内存不能直接替代内存 压力信号观察内存压力变化对于 其他组件级别的决策,例如选择何时启动内存密集型 activity 或线程。将来,我们还可以利用这些信号杀死 组件内的空闲进程内存压力信号还可以 可以更好地控制要释放的内存以及何时释放。

可舍弃内存 API

我们可以将现有的 zx_vmo_create()zx_vmo_op_range() 系统调用扩展到 支持此功能。

  • zx_vmo_create() 将扩展以支持新的 options 标志 - ZX_VMO_DISCARDABLE。此标志可与 ZX_VMO_RESIZABLE。不过,关于可调整大小的 VMO 的一般建议同样 适用于可舍弃的 VMO,在进程之间共享可调整大小的 VMO 危险,应避免。

  • zx_vmo_op_range() 将扩展以支持 正在上锁和开锁 - ZX_VMO_OP_LOCKZX_VMO_OP_TRY_LOCKZX_VMO_OP_UNLOCK

  • 锁定和解锁将应用于整个 VMO,因此 offsetsize 应该在 VMO 的整个范围内。锁定和解锁 VMO 范围内的更小范围。尽管当前的实现 严格要求使用 offsetsize,以确保只有整个范围 被视为有效允许在 而无需更改客户端的行为

  • ZX_VMO_OP_TRY_LOCK 操作将尝试锁定 VMO,可能会失败。 如果内核未舍弃 VMO,测试会成功,并返回 ZX_ERR_NOT_AVAILABLE(如果内核已将其舍弃)。如果失败, 客户端应使用 ZX_VMO_OP_LOCK 重试,这肯定会 成功。ZX_VMO_OP_TRY_LOCK 操作作为轻量级选项提供,可让您在不使用虚拟机的情况下 设置缓冲区参数客户也可以选择不接受 在锁定 VMO 失败后执行的操作。

  • ZX_VMO_OP_LOCK 操作还需要 buffer 参数,即输出 指向 zx_vmo_lock_state 结构体的指针。此结构体适用于内核 来传回客户端可能认为有用的信息,其中包括:

    • 跟踪锁定范围的offsetsize:分别是size和 客户端传入的 offset 参数。这些代码会返回 完全是为了方便,因此客户端无需跟踪 范围,而是可以直接使用返回的结构。如果 调用成功后,它们将始终与 sizeoffset 相同 传递给 zx_vmo_op_range() 调用的值。
    • discarded_offsetdiscarded_size 跟踪舍弃范围: 是锁定范围(包含舍弃的页面)内的最大值。 并非所有此范围内的网页都可能已被舍弃 - 这只是一个 此范围内所有舍弃子范围的并集,并且可以包含 未舍弃的网页使用当前的 API 时, 如果内核已舍弃该范围,舍弃范围将涵盖整个 VMO。如果 未舍弃,discarded_offsetdiscarded_size 都将设为 零。
  • 锁定本身不会提交 VMO 中的任何页面。它只是标记了 “不可舍弃”由内核负责客户端可以在 使用适用于常规 VMO 的任何现有方法验证 VMO,例如 zx_vmo_write()ZX_VMO_OP_COMMIT,映射 VMO 并直接写入 映射地址

// |options| supports a new flag - ZX_VMO_DISCARDABLE.
zx_status_t zx_vmo_create(uint64_t size, uint32_t options, zx_handle_t* out);

// |op| is ZX_VMO_OP_LOCK, ZX_VMO_OP_TRY_LOCK, and ZX_VMO_OP_UNLOCK to
// respectively lock, try lock and unlock a discardable VMO.
// |offset| must be 0 and |size| must the size of the VMO.
//
// ZX_VMO_OP_LOCK requires |buffer| to point to a |zx_vmo_lock_state| struct,
// and |buffer_size| to be the size of the struct.
//
// Returns ZX_ERR_NOT_SUPPORTED if the vmo has not been created with the
// ZX_VMO_DISCARDABLE flag.
zx_status_t zx_vmo_op_range(zx_handle_t handle,
                            uint32_t op,
                            uint64_t offset,
                            uint64_t size,
                            void* buffer,
                            size_t buffer_size);

// |buffer| for ZX_VMO_OP_LOCK is a pointer to struct |zx_vmo_lock_state|.
typedef struct zx_vmo_lock_state {
  // The |offset| that was passed in.
  uint64_t offset;
  // The |size| that was passed in.
  uint64_t size;
  // Start of the discarded range. Will be 0 if undiscarded.
  uint64_t discarded_offset;
  // The size of discarded range. Will be 0 if undiscarded.
  uint64_t discarded_size;
} zx_vmo_lock_state_t;

zx::vmo 接口将进行扩展,以支持 ZX_VMO_OP_LOCKop_range() 执行 ZX_VMO_OP_TRY_LOCKZX_VMO_OP_UNLOCK 操作。Rust、Go 和 Dart 绑定也将更新。

此 API 可让客户端灵活地共享可舍弃的 VMO 运行多个进程需要访问 VMO 的每个进程都可以执行此操作 根据需要锁定和解锁 VMO。任何 基于锁定等假设的进程之间需要进行协调 状态。只有在没有虚拟机时,内核才会将 VMO 视为符合回收条件 已经锁上了

针对 VMO 的限制

  • 只有 VmObjectPaged 类型支持可舍弃内存 API,因为 根据定义,无法舍弃VmObjectPhysical

  • 该 API 与 VMO 克隆(快照和 COW 克隆)不兼容,并且 因为在克隆层次结构中舍弃 VMO 可能会导致令人惊讶的 行为zx_vmo_create_child() 系统调用将针对可舍弃的 VMO 失败。

  • 不能在 options 参数中使用 ZX_VMO_DISCARDABLE 标志 zx_pager_create_vmo()。这样做的一个主要原因是,由分页器支持的 VMO 而可舍弃的 VMO 则不能。此外,可舍弃性暗示 因此无需额外标记。

与现有 VMO 操作交互

现有 VMO 操作的语义仍与之前相同。对于 例如,zx_vmo_read() 不会在之前验证可舍弃 VMO 是否已锁定 来允许执行该操作。客户有责任确保他们 在访问 VMO 时锁定 VMO,以确保内核 请勿将其舍弃这会限制此更改的表面区域。通过 它只能保证内核在工作时不会舍弃 VMO 的页面 它已锁定。

即使舍弃 VMO,该 VMO 的所有映射也仍然有效。 只要客户端在访问映射之前锁定 VMO 就行。客户端需要 如果 VMO 已被舍弃,则无需重新创建映射。

内核舍弃 VMO 后,在未先行处理的情况下 如果 VMO 没有已提交的页面,则该虚拟机没有 提供按需提交页面的机制例如,zx_vmo_read() 将失败 和ZX_ERR_OUT_OF_RANGE。如果 VMO 映射到进程中”地址空间 解锁对映射地址的访问将导致严重页面错误 异常。

内核实现

跟踪元数据

  • VmObjectPaged 中的 options_ 位掩码将进行扩展,以支持 kDiscardable 标志;目前我们只使用了 32 位中的 4 位
  • 将向 VmObjectPaged 添加新的 lock_count 字段,用于跟踪 VMO 上未完成的锁定操作的数量。
  • 内核将维护一个可回收 VMO 的全局列表,即 解锁了系统上的可舍弃 VMO。该列表将按如下方式更新: <ph type="x-smartling-placeholder">
      </ph>
    • ZX_VMO_OP_LOCK 会递增 VMO 的 lock_count。如果lock_count 从 0 到 1,VMO 将从全局可回收列表中移除。
    • ZX_VMO_OP_UNLOCK 会递减 VMO 的 lock_count。如果 lock_count 降为 0,则 VMO 将被添加到全局可回收对象中 。

回收逻辑

可舍弃的 VMO 会在满足以下条件时被添加到全局可回收列表中: lock_count 会降至零,并会在再次锁定时被移除。这可维持 系统中所有已解锁的可舍弃 VMO 的 LRU 顺序。内存不足时 内核可以按顺序将 VMO 从此列表中移出队列并将其舍弃, 查看可用内存级别这是一个非常简单的 回收逻辑实际可能是什么样子一些其他功能 我们稍后会提到

舍弃操作

“舍弃”是在内核端实现的,具体方法是: 并将 VMO 的内部状态设置为 discarded。 如果 VMO 满足以下条件,VmObjectPaged::GetPageLocked() 将失败并显示 ZX_ERR_NOT_FOUND 状态为 discarded。在后续操作中,系统会清除 discarded 状态, ZX_VMO_OP_LOCK 操作。GetPageLocked() 是对某个对象的所有访问 VMO 的网页通过 zx_vmo_read/write 系统调用和网页向下延伸至 通过虚拟机映射进行访问这使我们能够在 以及在舍弃未锁定的 VMO 时, 可通过映射访问未锁定的 VMO。

实现

这是一个新 API,因此此阶段没有依赖项。内核端 可以单独完成。实施该 API 后 用户空间客户可以开始采用它。

性能

性能影响因客户端用例而异。那里 客户端在使用 API 时需要注意的一些事项

  • 用于在之前锁定和解锁可舍弃 VMO 的 zx_vmo_op_range() 系统调用 会导致性能关键路径上的延迟明显增加。因此, 系统调用应该用在可能导致延迟时间增加的代码路径上 是容忍还是隐藏的。
  • 由于缓存被保留在内存中,因此客户端的性能也可能得到提升 延长使用时间客户端必须丢弃的缓冲区 现在,由于内核只会 尽可能多地舍弃内存。客户端可以使用缓存跟踪此更改 命中率、需要重新初始化缓冲区的次数等。

安全注意事项

无。

隐私注意事项

无。

测试

  • 通过多个线程执行新 API 的核心测试 / 单元测试。
  • 用于验证内核端回收行为的单元测试,即仅 可以舍弃未锁定的 VMO。

文档

需要更新 Zircon 系统调用文档以包含新 API。

缺点、替代方案和未知问题

在 VMO 中锁定范围

回收的粒度选择为整个 VMO,而不是 支持对 VMO 中的范围进行更精细的舍弃操作。这里有 这背后的几个原因。

  • 重建被舍弃一些页面的 VMO 可能会比较棘手。考虑 一般用例,其中 VMO 用于表示一个匿名内存资源, 因此,重新处理被舍弃的页面很可能是零填充, CANNOT TRANSLATE剩余未舍弃的网页。 仅保留 VMO 的部分网页可能没什么价值, 也就是说,VMO 仅在完全填充时才有意义。
  • VMO 粒度使 VmObjectPaged 实现变得简单,需要 尽可能少的跟踪元数据。我们无需跟踪锁定范围,之后便可进行匹配 解锁。也不涉及复杂的范围合并。
  • 它还使回收逻辑相当轻巧 多个内存块改为支持页面粒度 可能需要维护页面队列、对可舍弃页面进行老化, 类似于逐出用户分页器支持的页面的机制。

我们提议的 API 没有关闭,因此提供了 Future(如果需要的话),使用 offsetsize 参数 当前未使用的 zx_vmo_op_range()。向 Locking API(页面粒度锁定)似乎是对 当前提案这将使支持成本很少的客户 具有单个 VMO 的可舍弃区域可能会让人望而却步。

舍弃的内核实现

当内核收回可舍弃的 VMO 时,它会废弃其页面并跟踪 其状态为 discarded。今后针对网页的未锁定请求将在 discarded 状态;当 VMO 再次锁定后,discarded 状态为 已清除。这里的另一种替代方案是直接废弃页面, 明确跟踪状态。不过,通过跟踪 discarded 状态, 更严格的故障模型。例如,假设客户有一个 映射在其地址空间中的可舍弃 VMO,内核曾在某些时候将其丢弃 。如果客户端现在尝试在没有先通过映射访问 VMO 则会导致严重的页面错误。而如果内核 只取消提交页面,后续解锁的访问只会导致系统返回 0 无声地将网页发送给客户这可能未被检测到,或者 因为意外的零页面而导致更细微的错误。

这里的另一种替代方案是在内部将 VMO 调整为零。这个 默认情况下,系统会为我们提供我们想要的故障模型, 状态跟踪不过,这需要跟踪内部 以及由实现定义的 VMO 大小 用户看到的内容虽然具有由实现定义的内部尺寸 这对于将来的其他使用场景来说可能也会有所助益, 使用两种不同的大小概念会让人感到困惑,而且容易出错。直到我们 还有其他一些具体应用场景, 以及外部尺寸,因此我们选择避免采用这种方式。

使用原子加快锁定 API

这种锁定优化功能提供了一种备用的低延迟选项, 可解锁可舍弃的 VMO,专供希望锁定的客户端使用 而且解锁频率相当高这纯粹是性能优化 我们将来会根据需要添加此类功能。

该 API 使用称为 Metex 的锁定基元,类似于 Zircon futex,它允许通过用户空间原子快速锁定 系统调用的费用

可舍弃的 VMO 可与一个 Metex 关联,后者将用于锁定和 解锁手机,而不是使用 zx_vmo_op_range() 系统调用。一个 Metex 有三个 状态:已锁定(正在由用户空间客户端使用)、可舍弃(符合 由内核回收)和“需要系统调用”(可能已被 内核,则需要系统调用来检查状态)。上锁和开锁 可以通过原子方式翻转 VMO, Meex 处于已锁定和可舍弃状态之间的状态。当内核舍弃 VMO,它会以原子方式将其状态翻转为“需要系统调用”,表示 客户端需要与内核同步以检查舍弃状态。 此提案的更多详细信息不在此 RFC 的讨论范围内, 一个单独的文件中

基于分页器的创建 API

由分页器支持的任何 VMO 实际上都是可舍弃的 VMO,因为 分页器提供了一种机制,可按需重新填充舍弃的网页。该 此 RFC 中建议的可舍弃内存是匿名可舍弃的 内存;另一种是有文件支持的可舍弃内存,例如 由 blobfs 用户分页器填充的 blob 的内存中表示形式。 考虑到这一点,我们可以考虑使用一个替代创建 API, 可舍弃的 VMO 与分页器相关联。VMO 创建调用可能如下所示: 如下所示:

zx_pager_create(0, &pager_handle);

zx_pager_create_vmo(pager_handle, 0, pager_port_handle, vmo_key, vmo_size,
                    &vmo_handle);

使用 zx_vmo_op_range() 时,锁定和解锁功能可按预期运行。 只有在解锁后,内核才能随意舍弃 VMO 中的页面。

这种方法的优势在于它为我们提供了统一的创建 API 并将其存储在各种可舍弃内存中 - 无论这些内存是否受文件支持 还是匿名访问

但是,在这种情况下,分页器实际上并不用于特殊用途。开始时间 它处理的是通用匿名内存,那么它很可能只提供零 网页点播。分页器更适合需要 以专门的方式填充某些特定内容。推出 增加的间接性,这在技术复杂性和 只是为了按需创建零页面 这似乎没必要此功能已存在于常规的内核中 (非分页器支持的)VMO。

使用保留器对象锁定

此处提议的 Locking API 为可舍弃的 VMO 可能出现的 bug 留有余地 被意外(或恶意)解锁。我们可能会遇到 进程认为 VMO 已锁定,但另一个进程已将其解锁,即 第二个进程会发出额外的解锁时间。这会导致第一个进程 访问 VMO 时会出现错误或崩溃,即使它已正确锁定也是如此 访问权限。

我们可以使用 Retainer 对象,该对象会在创建 VMO 时锁定 VMO,并在 已销毁。

zx_vmo_create_retainer(vmo_handle, &retainer_handle);

只要保留器句柄打开,VMO 就会保持锁定状态。在 如上例所示,每个进程都将使用自己的保留器来锁定 从而避免因错误而需要额外解锁这种锁定方式 有助于降低出现此类错误的可能性 错误发生。

这种方法的缺点是,内核需要存储更多元数据才能跟踪 VMO 的锁定状态现在,我们拥有与 具有可舍弃的 VMO,而不是单个 lock_count 字段。我们可能还会 如果想消除出现这种问题的可能性 恶意用户导致内核无限增长。

回收订单的优先级

为了简化初始操作,内核会回收已解锁的 按 LRU 顺序排列的可舍弃 VMO。我们可以明确地探讨如何邀请客户 根据需要指定将来的回收优先级顺序(每个 优先频段仍可按 LRU 顺序收回)。建议的 API 未来可以通过当前未使用的 buffer 支持此功能。 zx_vmo_op_range() 中的 ZX_VMO_OP_UNLOCK 参数的值。

不过,我们可能并不需要这种级别的控制;全球 LRU 可能就足够了如果客户确实想对 当某些缓冲区被回收时,它们可能会转而接受内存压力 信号,并丢弃这些缓冲区本身。

与其他回收策略的交互

目前,我们还可以通过另外两种机制回收内存:

  • 用户分页器支持的内存(内存 blob)的页面逐出,由 处于关键内存压力水平(接近 OOM)。
  • 内存压力信号,用户空间组件本身会在 “严重”和“警告”内存压力等级。

我们需要弄清楚可舍弃内存在此方案中的位置, 确保没有任何单一回收策略承担大部分负担。 例如,我们可能希望 移到可舍弃内存中

锁定由分页器支持的 VMO

我们可以将 ZX_VMO_OP_LOCKZX_VMO_OP_UNLOCK 操作扩展到 分页器支持的 VMO。开发者希望支持 我们可能希望提供 使用场景。例如,blobfs 可以锁定内存中的 blob 的 VMO 或者不太适合内核 LRU 逐出方案的内容, 从而避免对它们重新进行分页的性能开销。

锁定由分页器支持的 VMO 可与可舍弃内存 API 完美契合, 因为基于用户分页器的 VMO 本质上可视为一种可丢弃的 而用户分页器提供一种专门的机制来重新填充 页面。锁定和解锁同时适用于这两种类型的可舍弃内容 这两种类型之间的主要区别在于它们在内存中 创建并填充数据

决定何时重新填充已舍弃的 VMO

客户端可能需要一种方式来确定重新填充舍弃项何时是安全的 VMO。如果在内存压力下重新填充 VMO,额外的页面 可能会加剧系统的内存压力, 哎呀,此外,VMO 随后被解锁后,它有可能 如果内存压力持续存在,则会舍弃。这可能会导致抖动,即导致 客户端反复重新填充 VMO,只是看到内核很快舍弃它 。

目前,观察系统内存压力水平的唯一机制是 订阅fuchsia.memorypressure服务 这个应用场景的成本很高。我们可以考虑扩展此服务 执行一次性查询的方法。我们还可以考虑传回 通过 zx_vmo_lock_state 结构体的压力水平指示符,可以是 当前内存压力等级本身,或者是一个 系统是否面临内存压力

用于跟踪解锁的 VMO 访问的调试辅助工具

在构建标志后面启用会失败的额外检查可能很有用 已解锁的可舍弃 VMO 的系统调用。这有助于开发者轻松找到 如果 VMO 访问之前没有锁定,而不必依赖 VMO 在内存压力下被舍弃,然后才导致失败。 随着我们不断地增加,对 VMO 锁定状态进行此类检查的成本可能很快就会增加 所以无法在生产环境中启用, 但它们在作为调试工具时可能很有用。

通过映射捕获未锁定的 VMO 访问可能更难 实施。为此,我们可以探索以下几种方法:

  • 当已映射的可舍弃 VMO 处于解锁状态时,取消映射。通过这种方法, 需要确保现有的 VMO / VMAR 语义保持不变。
  • 训练封装容器围绕锁定 / 解锁调用告知 ASAN 已解锁的 VMO 使用 ASAN_POISON_MEMORY_REGION 界面。

先验技术和参考资料