RFC-0219:Zircon 页面压缩

RFC-0219:Zircon 页面压缩
状态已接受
区域
  • 内核
说明

在匿名 VMO 页面的内核压缩中。

问题
Gerrit 更改
作者
审核人
提交日期(年-月-日)2022-05-31
审核日期(年-月-日)2023-06-03

摘要

本文档提出了为匿名用户内存添加内核压缩和解压缩系统。

虽然该系统位于内核中,但由于对内存计量的有影响,因此对用户空间而言并不完全透明。但除了 API 更改之外,所有功能更改都包含在内核中。

设计初衷

Fuchsia 经常用于资源受限的设备,在这些设备上,增加内存池可显著改善用户体验。

内存内核压缩是其他操作系统使用的标准技术,支持它有助于 Fuchsia 实现同等功能。

利益相关方

教员

cpu@google.com

Reviewers:

eieio@google.com、rashaeqbal@google.com、maniscalco@google.com

咨询了

mseaborn@google.com、mvanotti@google.com、wez@google.com、plabatut@google.com、jamesr@google.com、davemoore@google.com

社交

与 Zircon 内核团队和更广泛的利益相关方共享的内核设计和实现部分的 RFC 提案前文档。

设计

本部分概述了实现、压缩策略以及建议的 API 更改和扩展的概要设计选项。

老化和跟踪

为了进行老化处理,匿名页应与分页器支持的页面一样处理,这意味着:

  • 向匿名网页添加了回链,以便执行 vm_page_tVmObjectPaged + 偏移量查找。这会增加每次网页在 VMO 之间移动时都需要更新反向链接的开销。
  • 更改了页面队列,以便不对有分页器支持的页面和匿名页面进行不同的处理。

页面队列中的老化具有现有的“有效”和“无效”集的概念。现有驱逐操作绝不会从有效集中驱逐,以防止出现过载情况。匿名网页将属于同一队列,并且具有相同的压缩限制。

如果页面被压缩,则会从页面队列跟踪中移除,类似于被驱逐的页面。压缩失败的网页会移至单独的列表,以免系统再次尝试压缩。如果有用户访问了此类网页,系统会将其移回活跃集,并重新计算其潜在年龄,然后再次尝试压缩。

虽然我们只会压缩闲置网页,但我们支持以下三种触发器:

  • 在处理 LRU 队列时,对旧页面进行提前压缩,即使没有内存压力也是如此。
  • 在内存压力下压缩,同时执行驱逐操作。
  • 压缩以避免 OOM。

这些触发器将通过内核 cmdline 标志切换。

VmPageList 标记

VmPageList 存储区需要具有一种额外的标记,该标记可以:

  • 指示网页存在,但已压缩。
  • 存储足够的元数据,以便在压缩存储空间中查找网页。

VmPageList 的条目已编码,可同时存储 vm_page_t 指针和零页标记,并且可以扩展以进行额外的编码。

目前,页面列表条目是一个 64 位值,该值会编码为以下选项:

  • 0:空条目
  • 最低有效位 = 1:零页标记
  • 其他:指向 vm_page_t 的指针

由于指针对齐会使 vm_page_t 指针的底 3 位始终为零,因此此方案可行。因此,我们可以对此方案进行泛化,使页面列表条目的底 3 位表示类型,其余位表示关联数据。初始类型将如下所示:

  • 0:空条目或 vm_page_t 指针(具体取决于数据位是否已设置)
  • 1:零页标记。剩余数据位必须为 0。
  • 2:压缩后的网页。其余位是从压缩系统返回的不透明令牌。
  • 3-7:目前类型无效。

压缩操作

为了专注于简单高效的初始用例,初始存储策略将执行单页压缩和解压缩,而不会按 VMO 明确对页面进行分组。

不按 VMO 对页面进行分组,可允许以与现有驱逐系统相同的方式提取压缩路径。虽然不同 VMO 中的页面实际上会混杂在单个存储系统中,但严格要求不得在不同 VMO 之间创建锁定依赖项。这是为了防止通过时间通道泄露信息,并防止对延迟时间较长的无关进程进行惩罚。

为避免不必要的锁争用,压缩或解压缩算法的执行应在不持有 VMO 锁的情况下进行,并且在持有解压缩所需的任何锁的情况下,不应进行压缩。

压缩算法

下面概要介绍了压缩路径,省略了错误处理情况。压缩的输入是页面、其所属的 VMO 以及该页面在 VMO 中的偏移量。偏移量用于验证在获取 VMO 锁定后页面是否仍在 VMO 中,这属于错误处理范畴,否则会在此伪代码中被跳过。

Take VMO lock
Update page list to use temporary compression reference
Release VMO lock
Compress page to buffer
Take compressed storage lock
Allocate storage slot
Release storage lock
Copy buffer to storage
Take VMO lock
Check page list still holds temporary compression reference
Update page list to final compression reference
Release VMO lock

最初使用临时引用(而不是立即分配引用)的原因是,通过在生成引用时向压缩存储系统提供将要存储的数据大小,可以让压缩存储系统在生成引用时更灵活。这样,它就可以表示引用中数据的确切位置,而无需通过其他表进行间接引用。

即使未使用临时引用,仍需要两次获取 VMO 锁,以解决在压缩期间使用页面的任何潜在争用问题。例如,页面(在本例中为引用)在压缩期间可能已取消提交,除非我们重新获取 VMO 锁并进行检查,否则我们不会知道这一点。

在压缩期间,由于未持有 VMO 锁,另一个线程可能会找到临时引用并尝试使用它。由于临时引用实际上不包含可解压缩的数据,因此我们有两种方法可以解决此问题:

  • 等待压缩完成,然后检索原始网页。
  • 复制原始页面(在压缩运行开始时预分配用于此目的的内存)。

这样做是为了确保在整个时间压缩过程中,网页处于已知状态且归您所有。在压缩过程中使用原始页面会出现问题,因为:

  • 在压缩算法读取页面时修改页面会增加压缩算法的潜在 bug 和攻击面,因为它可能无法对并发数据更改进行弹性处理。
  • VMO 的可缓存性或其他属性可能会发生变化,导致压缩器并行缓存用法行为未定义。

这里的建议是复制原始页面,因为这种情况应该不太可能发生,并且复制可以更快地解析请求,同时避免需要使用其他同步机制。此处的复制是在 VMO 锁定下进行的,但这与所有其他写时复制路径一致,在这些路径中,页面复制可以在 VMO 锁定下进行。由于一次只会进行一次压缩,因此任何给定的 VMO 操作最多只会遇到此情形一次。

解压缩算法

实现解压缩是为了利用现有的 PageRequest 系统,该系统已用于延迟分配和执行用户分页器请求。与用户分页器 VMO 缺少页面会生成页面请求类似,VMO 页面列表中的压缩引用也会触发类似过程:

Take VMO lock
Observe page is compressed
Fill in the PageRequest
Drop VMO lock
Wait on the PageRequest
Retry operation

填充页面请求时,我们需要存储页面列表中的压缩引用以及 VMO 和偏移量。这需要扩充现有的 PageRequest 结构。

与用户分页器页面请求和压缩相比,等待操作可以直接解析请求,在本例中,这意味着执行解压缩。

解压缩需要容忍多个线程尝试访问同一页面,并提供一种优先级继承流动的方式:

Declare stack allocated OwnedWaitQueue
Take compressed storage lock
Validate compressed reference
if compressed page has wait queue then
    take thread_lock
    release compressed storage lock
    wait on referenced wait queue
else
    store reference to our allocated wait queue in compressed block
    release compressed storage lock
    decompress into new page
    take VMO lock
    update VMO book keeping
    release VMO lock
    take compressed storage lock
    take thread lock
    remove wait queue reference
    release compressed storage lock
    wake all threads on wait queue

此处之所以使用 thread_lock,是因为执行等待队列操作时需要持有该锁,而没有利用持有 thread_lock 的任何其他属性。

目标压缩大小

由于解压缩会产生开销,因此只有在网页的压缩版本大小缩减足够多时,才值得存储。否则,不妨存储未压缩的版本,以节省解压缩费用。

此目标大小应为可调节的选项,但在其他系统中,70% 的目标大小很常见。

任何无法达到此目标的压缩尝试都会被视为失败,并且网页会放回网页队列中(如“老化和跟踪”部分中所述)。

零页面扫描

由于压缩从根本上需要检查源页面中的所有字节,因此提供了检测和删除零页的重复项的机会。这表示压缩的另一种成功结果,其中报告输入页面只是零页,并且不会将结果存储在压缩存储系统中。相反,VMO 调用方可以将页面替换为零页面标记。

现成的压缩算法实现通常没有报告输入全为零的方法,因此实现此功能需要:

  • 修改或创建实现以执行此跟踪。
  • 将压缩结果与已知签名进行比较。

第二种方法利用了压缩算法是确定性的这一事实,并且通常具有大小不到几十字节的零页表示法。因此,执行大小检查后进行内存比较检查的开销可能足够低。

此提案不需要初始压缩实现即可执行零页检测,但必须构建 API 和 VMO 逻辑来支持它。最终,现有的零页扫描器应被替换,以便压缩支持此功能。

压缩算法

建议将 LZ4 用作初始压缩算法。如需了解详情,请参阅实验评估部分,但该计划有以下重要要求:

  • 适用于小尺寸(4KiB)。
  • 在启动后稳定运行期间不会执行 malloc 或等效操作。
  • 可以并行解压缩,且没有共享的可变状态。压缩必须能够与解压缩并行进行。
  • 虽然并非强制性要求,但系统本身支持多重并行压缩。

LZ4 还有一个实用的附加属性,即能够根据受限的目标缓冲区大小提前中止压缩。

压缩后的存储空间

压缩页面的存储是代码复杂性和运行时之间的权衡,可能会导致碎片化和内存用量增加。选择了一种初始策略,即将固定数量的压缩页(在本例中为 3 个)打包到单个存储页中。

这种存储策略的核心概念是,每个存储页面都可以视为具有左、中、右三个存储槽,并且压缩比率最多为 3:1。

实现非常简单,涉及以下步骤:

  • 搜索任何现有的部分填充的页面,查找空缺的广告位。
  • 如果没有现有合适的槽位,请将其放置在新页面的左侧槽位。

这两项复杂功能分别是:

  • 应根据空闲槽的大小将部分填充的页面分桶到搜索列表中,以便使用 O(1) 搜索实现近乎最佳的打包。
  • 如果左侧或右侧槽中的数据量足够大,中间槽可能会变为不可用。

这种策略的好处在于:

  • 实现简单。
  • 可预测的操作,没有长尾性能问题。
  • 在最糟糕的情况下,降级为不使用任何额外内存。

可能需要处理的一个病态行为是,页面解压缩后,可能会存在可填充的空洞,从而释放其他存储页面。在需要时,可以实现此类压缩系统。

从长远来看,这可能会演变为使用更通用的堆或槽状分配器。

如需查看一些初步结果,请参阅实验评估部分。

会计和指标

虽然压缩操作无需任何用户输入即可完成,但会影响内存用量报告方式。我们还希望将有关压缩性能的其他信息提取到用户空间。

内容与已提交内容

现有查询接口中提及的已提交字节和页面是指直接在 VMO 中分配用于存储数据的实际内存。

这些现有查询如下:

  • 用于报告 mem_private_bytesmem_shared_bytes 等中已提交内存的 ZX_INFO_TASK_STATS
  • ZX_INFO_PROCESS_MAPS,其中报告了映射的 VMO 的 committed_pages
  • 报告 committed_bytesZX_INFO_PROCESS_VMOS / ZX_INFO_VMO
  • 报告被计为已提交的 vmo_bytesZX_INFO_KMEM_STATS / ZX_INFO_KMEM_STATS_EXTENDED

除了已提交外,我们还将添加内容字节/页的概念。任何涉及已提交内存的查询仍会继续应用,具体指的是直接在 VMO 中存储内容的内存。内容将改为指内核为用户保留的数据量,而不具体说明方式。也就是说,内容可以直接存储在页面中,因此也会计入已提交内容;也可以压缩,而不计入已提交内容。

在这种规模下,系统会将压缩的内容视为存储空间大小未知。尝试归因存储空间中的各个压缩页会很复杂且不稳定,因为存储空间大小取决于系统的总压缩行为和由此产生的碎片化。

虽然匿名 VMO 在开始时理论上为零,但这不被视为内核记忆的内容。如果用户明确提交或修改了任何网页,则这些网页就会成为内容。之后,即使用户离开或将内容重置为零,内核也不必注意到这一点,并且可能会继续将其视为正在跟踪的内容。

与驱逐和用户分页器相关,可从用户分页器检索的内容不属于内核跟踪的内容。因此,驱逐会从已提交内容和内容数量中扣除。

指标

为了了解压缩的效果(用于调优 / 开发以及持续监控系统运行状况),我们希望收集和提供指标。

这些指标应能够回答有关压缩性能如何以及对系统性能有何影响的问题。具体而言,我们希望了解以下信息:

  • 压缩存储比率。
  • 网页压缩所花费的时间。
  • 正在压缩哪些类型的网页以及触发压缩的原因。
  • 解压缩延迟时间。
  • 执行压缩和解压缩所花费的 CPU 时间。
  • 成功的压缩尝试次数与失败的压缩尝试次数之比。

API 更改

通过结构体演变和查询版本控制进行扩展:

  • zx_info_maps_mapping_t 应包含 content_pages 字段。
  • zx_info_vmo_t 应包含 content_bytes 字段。
  • ZX_INFO_TASK_RUNTIME,以将 page_fault_decompress_time 报告为总 page_fault 时间的一部分。

需要为这些进行版本控制的 zx_object_get_info 查询如下:

  • ZX_INFO_PROCESS_MAPS
  • ZX_INFO_PROCESS_VMOS
  • ZX_INFO_VMO
  • ZX_INFO_TASK_RUNTIME

zx_info_vmo_t 中的现有字段(例如 committed_bytes)的注释和文档将更新,以明确说明它们不会统计处于压缩状态的网页。

确切内容将在迭代阶段确定,但建议向报告添加 ZX_INFO_KMEM_STATS_COMPRESSION 查询:

struct zx_info_kmem_stats_compression {
    // Size in bytes of the content that is current being compressed.
    uint64_t uncompressed_content_bytes;

    // Size in bytes of all memory, including metadata, fragmentation and other
    // overheads, of the compressed memory area. Note that due to base book
    // keeping overhead this could be non-zero, even when
    // |uncompressed_content_bytes| is zero.
    uint64_t compressed_storage_bytes;

    // Size in bytes of any fragmentation in the compressed memory area.
    uint64_t compressed_fragmentation_bytes;

    // Total amount of time compression has spent on a CPU across all threads.
    // Compression may happen in parallel and so this can increase faster than
    // wall clock time.
    zx_duration_t compression_time;

    // Total amount of time decompression has spent on a CPU across all threads.
    // Decompression may happen in parallel and so this can increase faster than
    // wall clock time.
    zx_duration_t decompression_time;

    // Total number of times compression has been done on a page, regardless of
    // whether the compressed result was ultimately retained.
    uint64_t total_page_compression_attempts;

    // How many of the total compression attempts were considered failed and
    // were not stored. An example reason for failure would be a page not being
    // compressed sufficiently to be considered worth storing.
    uint64_t failed_page_compression_attempts;

    // Number of times pages have been decompressed.
    uint64_t total_page_decompressions;

    // Number of times a page was removed from storage without needing to be
    // decompressed. An example that would cause this is a VMO being destroyed.
    uint64_t compressed_page_evictions;

    // How many pages compressed due to the page being inactive, but without
    // there being memory pressure.
    uint64_t eager_page_compressions;

    // How many pages compressed due to general memory pressure.
    uint64_t memory_pressure_page_compressions;

    // How many pages compressed due to attempting to avoid OOM or near OOM
    // scenarios.
    uint64_t critical_memory_page_compressions;

    // The nanoseconds in the base unit of time for
    // |pages_decompressed_within_log_time|.
    uint64_t pages_decompressed_unit_ns;

    // How long pages spent compressed before being decompressed, grouped in log
    // buckets. Pages that got evicted, and hence were not decompressed, are not
    // counted here. Buckets are in |pages_decompressed_unit_ns| and round up
    // such that:
    // 0: Pages decompressed in <1 unit
    // 1: Pages decompressed between 1 and 2 units
    // 2: Pages decompressed between 2 and 4 units
    // ...
    // 7: Pages decompressed between 64 and 128 units
    // How many pages are held compressed for longer than 128 units can be
    // inferred by subtracting from |total_page_decompressions|.
    uint64_t pages_decompressed_within_log_time[8];
};

报告网页压缩所花的时间意味着,系统必须为每个压缩的网页记录时间戳。这应该不会对潜在的压缩比产生重大影响,这样您就可以知道网页是否正在快速解压缩。

ZX_INFO_KMEM_STATS_EXTENDED 分页器字节

ZX_INFO_KMEM_STATS_EXTENDED 查询包含与页面器支持的 VMO 中的字节数相关的字段。具体而言,这些字段包括:

    // The amount of memory committed to pager-backed VMOs.
    uint64_t vmo_pager_total_bytes;

    // The amount of memory committed to pager-backed VMOs, that has been most
    // recently accessed, and would not be eligible for eviction by the kernel
    // under memory pressure.
    uint64_t vmo_pager_newest_bytes;

    // The amount of memory committed to pager-backed VMOs, that has been least
    // recently accessed, and would be the first to be evicted by the kernel
    // under memory pressure.
    uint64_t vmo_pager_oldest_bytes;

所提议的实现将在页面队列中统一使用有页面缓冲区支持的 VMO 页面和匿名 VMO 页面,这会导致提供此类信息变得不可行。因此,建议将这些定义为指所有可分页 / 可回收内存,而不仅仅是特定可驱逐用户分页器支持的 VMO。否则,系统会保留总数、最新定义和最早定义。

虽然压缩内存的回收不会直接导致 PMM 可用内存增加,但这些查询并不代表可回收的 PMM 内存的确切数量。而是提供有关各个年龄段内存相对分布的深入分析,并可用于执行验证,例如在设备发生 OOM 时 oldest_bytes 会下降并接近零等。

为用户分页器支持的可驱逐内存和可压缩匿名内存提供多个字段也是值得怀疑的。在某些情况下,能够区分这两者非常有用,但在大多数情况下,我希望看到这两者的汇总报告。

停用对压缩 / 延迟时间敏感的 VMO

对于对延迟敏感的应用,需要有一种机制来让它们在 VMO 上停用压缩,因为无论解压缩延迟时间有多短,都可能超出它们能够容忍的范围。

避免回收和其他可能增加内存访问延迟时间的内核活动是一项正在解决的现有问题。因此,本文不会提出任何设计,但这项工作是压缩的依赖项。

扩展程序

除了这个最初提出的设计之外,我们还可以进行一些探索和改进。虽然这些通常属于实现细节,但我们在此处列出它们是为了说明此设计的长期适用性。以下改进绝对有必要进行,最好是在产品中使用之前进行:

  • 移除了单独的零页去重功能,改为使用压缩功能。

您也可以考虑探索其他可选方案,这些方案可能带来实用的好处,例如:

  • 先压缩由分页器支持的内存,然后再考虑将其驱逐。
  • 支持在出现内存压力之前执行压缩,而不会丢弃未压缩的页面。
  • 遍历其他旧页面列表,并不仅仅压缩 LRU 队列中的页面。

我们还可以进一步优化压缩存储和压缩算法,这既可以作为直接演变,也可以为不同产品提供选项。这些可能需要单独的 RFC,具体取决于它们的侵入性或差异程度。

实现

实施过程将分为以下多个阶段完成。

常见脚手架

我们将首先实现支持匿名网页老化和跟踪的常见虚拟机系统更改。这些更改不会导致行为或 API 变更,但风险最高,先进行这些更改有助于:

  • 将更改纳入树中并尽可能长时间进行测试。
  • 能够衡量候选压缩池(即旧的匿名网页)。

内核实现

完成常规更改后,主内核实现可以通过功能标志植入到树中。这样一来,您就可以在通过任何 API 公开其存在之前,发布并测试压缩功能的实现,以确保其稳定。

API 演变

针对指标和会计更改执行 API 演变和结构体迁移。由于内核实现位于树中,因此可以关联 API 更改,但压缩功能仍会位于功能标志后面,因此除了额外的字段之外,普通用户不会看到任何功能更改。

集成、扩展程序和调整

实现经过测试且能够报告指标后,您可以在不同的产品上对其进行评估,以找出任何问题,从而执行任何扩展和调整。

此测试的结果将决定:

  • 压缩功能默认处于开启状态,部分商品会选择停用,或者
  • 压缩功能默认处于关闭状态,部分产品将选择启用。

扩展和调优

现在,我们可以发布扩展设计的部分内容,并会不断优化常规实现。

性能

需要评估性能的两个方面。

常见开销

即使停用了压缩功能,也需要对常见的虚拟机代码进行根本性更改,这些更改会影响性能。

向匿名网页添加反向链接预计会给所有 VMO 克隆创建和销毁事件带来可衡量的开销。这些操作不在性能关键路径上,因为它们通常发生在初始化和拆解步骤期间,但应检查对产品的任何影响。

其他 VMO 路径会产生一些开销来维护反向链接和更新时间戳,但这应该属于噪声,不会产生可衡量的影响。

现有的 VMO 微基准测试涵盖了这些路径,它将成为主要的验证工具。

压缩性能

压缩的核心是 CPU 与内存之间的权衡,因此其性能只能在产品及其需求的背景下进行评估。

否则,建议的指标 API 可让您了解压缩费用,然后您可以使用压缩可调参数来控制这些费用(相对于节省的内存量)。

安全注意事项

时间渠道

虽然压缩页面可能具有共存存储空间,但访问此存储空间的过程已定义为不对 VMOs 存在任何传递依赖项,这些依赖项可能会导致可衡量的渠道。这样做是为了避免可能类似于内存去重攻击的攻击。

尽管缺少传递依赖项,但如果攻击者可以控制与密钥在同一页面中共存的数据,仍然有可能创建时序通道。攻击者可以操纵这些数据,使压缩成功或失败取决于 Secret,从而了解 Secret 的相关信息。此类攻击是否实际上可行尚不清楚,但如果您担心这一点,也可以使用防止对对延迟敏感的任务进行压缩的相同机制。

系统行为渠道

您可以通过 ZX_INFO_KMEM_STATS_COMPRESSION 直接查询总系统内存用量,也可以通过查询 VMO 等对象并观察内容和已提交字节之间是否存在差异来推断总系统内存用量。您还可以查询 ZX_INFO_KMEM_STATS_COMPRESSION 中的其他统计信息。

这些信息可间接提供有关整体系统行为的信息。不过,我们认为,仅知道您控制的 VMO 中的网页是否已压缩,并不足以了解另一个进程的任何具体信息。

LZ4

虽然 LZ4 库的文件数量和 LoC 指标很小,但它仍然是用低级 C 语言编写的非琐碎代码,并且可能存在 bug。该库需要混合使用以下各项:

  • 安全审核,以确保整体适宜性。
  • 进行了额外的强化和测试。
  • 可能需要重写有问题的部分。

结合使用固化和重写功能可能会导致将部分或全部实现移植到更安全的表示法(例如 Rust 或 WUFFS)。

测试

与效果类似,测试也有两个维度。

虚拟机正确性

我们将通过单元测试和集成测试来评估常见虚拟机更改以及压缩专用更改的技术正确性。

对于无论内核单元测试中是否启用压缩都会生效的常规虚拟机更改,我们会酌情添加其他核心测试。

使用 QEMU 运行程序的集成测试将用于运行启用压缩的其他测试。

系统行为

虽然压缩并非免费的,但不应对产品所有者认为至关重要的任何行为产生负面影响。这些行为因产品而异,例如音频跳跃、在低内存场景下加剧抖动和用户体验不佳。

这需要使用锁争用跟踪等工具手动测试产品,并与产品测试团队和产品所有者合作进行评估。

文档

我们将记录对 zx_object_get_info 查询的添加和更改,以及其他 cmdline 选项。

缺点、替代方案和未知情况

未知数和迭代

在对真实产品进行适当实现和评估之前,我们无法完全了解此提案的确切益处和成本。虽然此提案会努力为所有算法和结构提供具体且合理的初始提案,但这些提案几乎肯定会根据实际使用数据而发生变化。

缺点

此方案的主要缺点是会增加内核中虚拟机系统的复杂性。虽然只需停用压缩即可避免压缩的任何性能方面问题,但压缩功能的存在需要对常见基础架构进行更改。因此,即使未使用压缩功能,也存在与压缩相关的永久性复杂性和风险。

用户分页器压缩替代方案

您可以通过用户分页机制将压缩和解压缩工作外包到用户空间,而不是在内核中执行这些操作。在许多方面,这与 Linux 上的 ZRAM 类似,后者由用户空间文件系统驱动程序实现。遗憾的是,它也存在许多相同的缺点。

优势

想要在用户空间中进行压缩的主要原因有两个:

  1. 从内核中移除了复杂的压缩和解压缩代码。
  2. 在用户实现方面提供灵活性。

遗憾的是,(1) 并不完全正确,因为除非为每个安全域创建单独的密封实例(无论如何确定),否则此用户空间进程将能够查看系统中每个进程的匿名内存。这会使其变得非常可信,因为任何漏洞都可能会查看和修改系统中的任何用户内存。因此,与内核相比,对此处压缩和解压缩代码的信任要求不会降低。

提供灵活的实现方式无疑是值得的,不过如果有一种机制可以将特定的匿名 VMO 分配给特定的压缩器,那就更有吸引力了。这样一来,系统甚至可能无需进行压缩,而是可以实现换页或任何其他策略。

缺点

用户空间压缩的主要缺点是它对内核施加的限制。能够乐观地压缩和解压缩页面(甚至可能无需丢弃未压缩的页面),这让内核的实现非常灵活。所有此类方案都可以通过用户分页器实现,但这会变成一个更复杂的分布式系统问题,需要非常谨慎地进行 API 和并发推理。

虽然 Zircon 是一个组件系统,并且旨在以低成本切换用户空间任务,但需要向用户空间进程进行向下调用以解压缩 4KiB 页面,这会增加大量延迟时间和开销。这会促使延迟压缩,直到网页过时,从而减少可节省的内存。对于某些情况下现有的用户分页器故障,假设正在查询运行缓慢的永久存储空间,因此驱逐已偏向于尽可能延迟驱逐。

即使用户空间处理压缩和解压缩,仍需要完成用于跟踪网页访问时间和触发压缩的所有内核更改。因此,所有可调参数和指标仍需要存在于内核中。此外,虚拟机系统目前无法将用户分页器与子 VMO 相关联,因此需要对虚拟机系统进行进一步更改和重新设计。

内核代码解释器

我们可以通过用户提供的内核代码(类似于 Linux eBPF)支持用户提供的算法,而不是固定的压缩算法。这样,用户实现就可以灵活调整,而不会因执行用户下调而产生相关性能影响。

虽然不需要显式模式转换,但由于性能特性未知,内核仍需要将其视为与用户下调类似,因此在调用所提供的代码时仍需要注意锁定和其他依赖项。

这种方法的主要缺点是,内核中的虚拟机需要足够强大,才能提供可接受的压缩性能,这需要进行大量的实现,而这些实现肯定会很复杂,并会大大增加内核的可信计算基础。

未来

未来,我们可能需要一种方法来让匿名 VMO 由交换存储空间提供支持。这绝对需要通过用户分页器机制在用户空间中实现。此时,此类换页实现可以进行压缩,而不仅仅是换页。不过,鉴于深度内核集成压缩带来的好处,我认为这两者可以共存,而不是取代内核压缩。

会计核算替代方案

该提案目前建议为内容字节/页面提供新的定义,以配合现有的已提交计数。您可以使用其他命名选项,而不更改要统计的内容,例如:

  • 常驻且敬业
  • 居民和内容

这两种方法都需要更改已提交的所有现有用法,是更大规模的更改,并且会导致这两个 API 版本之间出现更大的不连续性。

ZX_INFO_KMEM_STATS_EXTENDED 分页器字节替代方案

虽然所提议的实现会统一使用分页器支持的队列和匿名队列,以致于无法轻松分离其计数,但另一种实现可以将计数分开。

这种替代实现会为每个可回收队列提供两组不同的计数器。存储在 vm_page_t 中的 page_queue_priv 中的高位将用于区分在队列之间移动页面时需要更新哪个计数。

这种方法的缺点包括:

  • page_queue_priv 的使用方式变得更加复杂。它现在变成了一个打包字段,而不是计数器,并且所有读取和写入都需要保留或掩盖额外的位。
  • 访问过的收割(在持有 arch aspace 锁的情况下发生)的开销会随着队列信息需要解码和更新正确的计数而增加。
  • 多个计数字段,并且在保持这些字段一致性方面增加了复杂性。

正如提案中所述,我认为,鉴于实现复杂性和运行时开销的增加,对于分页器字节字段的实际用途,单独统计这些字节的价值微乎其微。

实验评估

为了提出 LZ4 压缩算法和压缩存储策略,我们构建了一个实验版压缩,其中包含足够的部分来:

  • 收集一组将要压缩的非活跃匿名网页的示例数据集。
  • 对此数据集运行不同的压缩算法。

目的是评估不同压缩算法,并考虑以下因素:

  • 来自实际产品的逼真输入数据。
  • 在受限执行环境中,正确匹配内核。

第二点尤为重要,因为内核代码在运行时不支持 FPU/SSE/AVX 等。

评估使用 64 MiB 的输入数据集进行,每个 4 KiB 页面都单独馈送给压缩器,就像在实际操作中一样。生成的数据的解读方式如下:

  • 成功压缩是指压缩后大小至少为原始大小的 70%。
  • 系统会统计所有网页的压缩时间,包括最终未压缩存储的网页。这与实际用例相符,在实际用例中,您无法提前知道压缩是否会成功。
  • 系统仅会针对成功压缩的网页计算解压缩时间。
  • “总大小”统计每个网页所需的存储空间,因此成功压缩的网页的压缩大小会计入统计,而压缩失败的网页会计为 4 KiB。

采用这种计数方式的原因在于,最终目标是释放内存并尽可能减少 CPU 用量。如果压缩算法有时可以非常快速地很好地压缩页面,但在其他情况下会消耗大量 CPU 资源,然后失败,那么它实际上既不会节省太多内存,也不会使用大量 CPU 资源。

NUC 7

算法 压缩(MiB/s) 解压缩速度(MiB/s) 解压缩最长延迟时间 (ns) 总大小 (MiB)
Zstd(-7) 752.1470396 1446.748532 9302.3125 18.61393929
minilzo 1212.400326 2135.49407 6817.839844 16.16410065
lz4(1) 1389.675715 2920.094092 6355.849609 17.21983242
lz4(11) 1801.55426 3136.318174 5255.708984 19.92521095
WKdm 1986.560443 3119.523406 3095.283203 21.98047638

ARM A53

算法 压缩(MiB/s) 解压缩速度(MiB/s) 解压缩最长延迟时间 (ns) 总大小 (MiB)
Zstd(-7) 189.3189527 486.6744277 29856.93359 18.61393929
minilzo 317.4546381 528.877112 15633.99219 16.16410065
lz4(1) 294.5410547 1127.360347 12683.55273 17.21983242
lz4(11) 378.2672263 1247.749072 12452.67676 19.92521095
WKdm 337.6087322 471.9796684 14254.39453 21.98047638

ARM A73

算法 压缩(MiB/s) 解压缩速度(MiB/s) 解压缩最长延迟时间 (ns) 总大小 (MiB)
Zstd(-7) 284.3621221 623.15235 20958.90332 18.61393929
minilzo 487.4546545 747.9218419 16784.83105 16.16410065
lz4(1) 441.5927551 1159.839867 10007.28418 17.21983242
lz4(11) 573.7741984 1240.964187 9987.875 19.92521095
WKdm 639.5418085 597.6976995 13867.47266 21.98047638

压缩后的存储空间

我们使用相同的 64 MiB 测试数据集来评估所提议的压缩存储系统。在本例中,我们在两种模式下对其进行了评估:仅包含左侧和右侧槽位的双页面系统,以及我们提出的三槽系统。这样做的目的是尝试量化与真正简单的双槽系统相比,添加中间槽带来的复杂性优势。

LZ4 是提议的压缩算法,用于生成要存储的压缩数据,加速因子设置为 11。这意味着,对于 16384 个输入页面,12510 个页面生成了需要存储的数据,3874 个页面保持未压缩状态。

老虎机 用于存储的页面 总 MB 节省 MiB 压缩比
双槽 6255 39.5 24.4 1.62
三槽 4276 31.8 32.1 2.01

请注意,6255 正好是 12510 的一半,这表示每个页面都有一个可用的伙伴,这意味着,对于 LZ4 输入,所实现的压缩比是此存储策略的最佳结果。

在先技术和参考文档

所有主流现代操作系统(Windows、MacOS、Linux 和 Linux 衍生产品,如 CastOS 等)都支持内存压缩,作为磁盘交换的替代方案或补充方案。

三页存储策略的灵感来自 Linux zbud 和 z3fold 内存管理系统。