RFC-0219:Zircon 页面压缩

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

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

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

摘要

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

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

设计初衷

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

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

利益相关方

辅导员

cpu@google.com

审核者

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 内核团队和更广泛的利益相关者分享了内核设计和实现部分的 Pre-RFC 提案文档。

设计

本部分概述了实现、压缩策略以及提议的 API 更改和扩展的高级设计选择。

老化和跟踪

对于老化,匿名页面的处理方式应与由分页器支持的页面类似,这意味着:

  • 向匿名网页添加了反向链接,以便执行 vm_page_tVmObjectPaged + 偏移量查找。这会增加一个成本,即每当页面在 VMO 之间移动时,都需要更新反向链接。
  • 更改了页面队列,不再区别对待由分页器支持的页面和匿名页面。

网页队列中的老化具有现有的活跃集和非活跃集概念。 现有驱逐机制永远不会从活跃集中驱逐,以防止出现抖动情况。匿名页面将属于同一队列,并且在压缩方面具有相同的限制。

如果页面已压缩,则会从页面队列跟踪中移除,类似于被逐出的页面。无法压缩的网页将移至单独的列表,以便系统不再尝试压缩这些网页。如果访问了此类页面,该页面将移回活跃集,并可能再次老化并尝试压缩。

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

  • 在处理 LRU 队列时,即使没有内存压力,也会主动压缩旧页面。
  • 在内存压力下,结合执行逐出操作进行压缩。
  • 压缩以避免 OOM。

这些触发器将通过内核命令行标志进行切换。

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) 搜索实现近乎最佳的打包。
  • 如果左侧或右侧槽位中的数据量足够大,中间槽位可能会变得不可用。

这种策略的优势包括:

  • 实现简单。
  • 可预测的运行,无长尾性能。
  • 最坏情况下会降级为不使用任何额外内存。

可能需要处理的一种病态行为是,在网页解压缩后,可能存在可以填充的空洞,从而释放其他存储网页。如果需要,可以实现这样的压缩系统。

从长远来看,这可能会演变为更通用的堆或 slab 样式分配器,或者使用这种分配器。

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

会计和指标

虽然压缩操作无需任何用户输入,但它确实会影响内存用量的报告方式。我们还希望向用户空间泄露有关压缩性能的更多信息。

内容与已提交

现有查询接口将提交的字节和页面视为直接在 VMO 中分配的用于保存数据的实际内存。

这些现有查询包括:

  • ZX_INFO_TASK_STATS,用于报告以 mem_private_bytesmem_shared_bytes 等为单位的已提交内存。
  • ZX_INFO_PROCESS_MAPS,用于报告映射的 VMO 的 committed_pages
  • 报告 committed_bytesZX_INFO_PROCESS_VMOS / ZX_INFO_VMO
  • ZX_INFO_KMEM_STATS / ZX_INFO_KMEM_STATS_EXTENDED 报告 vmo_bytes,这些报告会被计为已提交。

除了已提交的字节/页面之外,我们还将添加内容字节/页面的概念。任何提及已提交内存的查询都将继续特指直接在 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 内存量。而是提供有关内存在不同年龄段的相对分布情况的深入分析,可用于执行验证,例如 oldest_bytes 在设备内存不足时会下降并接近于零,等等。

为用户分页器提供多个字段来支持可逐出内存和可压缩匿名内存的价值也值得怀疑。在某些情况下,能够区分这两者非常重要,不过在大多数情况下,我预计用户还是希望获得这两者的汇总报告。

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

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

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

扩展程序

除了最初提出的设计之外,还可以进行一些探索和改进。虽然这些通常属于实现细节,但此处包含这些内容是为了说明此设计的长期适用性。绝对应该进行的改进(最好在产品使用之前进行):

  • 移除单独的零页面去重,改为压缩。

此外,您还可以探索以下可选方案,这些方案可能会带来有益的效果:

  • 在不得已驱逐分页器支持的内存之前,先对其进行压缩。
  • 支持在内存压力出现之前执行压缩而不舍弃未压缩的页面。
  • 遍历额外的旧页面列表,并主动压缩页面,而不仅仅是 LRU 队列中的页面。

还可以进一步压缩存储空间和调整压缩算法,无论是直接演变,还是为不同产品提供选项。这些更改可能需要单独的 RFC,具体取决于它们的侵入性或差异性。

实现

实施过程将分为多个阶段,具体如下所述。

常见脚手架

首先会实现常见的虚拟机系统更改,以支持匿名页面的老化和跟踪。这些更改不会带来行为或 API 方面的变化,但风险最高,因此先进行这些更改可实现以下目标:

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

内核实现

完成常见更改后,主要内核实现可以放置在树中,并受功能标志控制。这样一来,我们就可以先落实并测试压缩功能的实现,确保其稳定,然后再通过任何 API 公开其存在。

API 演变

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

集成、扩展和调整

在测试并能够报告指标后,可以针对不同的产品评估实现情况,以发现任何问题,从而进行任何扩展和调整。

此测试的结果将决定是否:

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

扩展和调优

现在可以实现部分扩展设计,并且会不断调整常规实现。

性能

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

常见开销

即使停用压缩,也需要对通用虚拟机代码进行根本性更改,而这些更改会对性能产生影响。

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

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

这些路径已包含在现有的 VMO 微基准中,后者将成为主要的验证工具。

压缩性能

从本质上讲,压缩就是用 CPU 换取内存,因此只能根据产品及其需求来评估压缩的性能。

否则,建议的指标 API 可用于了解压缩的成本,然后可以使用压缩可调参数来控制这些成本,同时考虑节省的内存量。

安全注意事项

计时渠道

虽然压缩页面可能具有共址存储空间,但访问此存储空间的过程已定义为不依赖于可能导致可测量渠道的 VMO。这样做是为了避免可能类似于内存重复数据删除攻击的攻击。

尽管缺少传递依赖项,但如果攻击者可以控制与机密数据位于同一页面中的数据,仍有可能创建时序通道。攻击者可以精心设计此数据,使其能够根据密钥成功或失败地进行压缩,从而了解有关密钥的一些信息。此类攻击是否切实可行尚不清楚,不过,如果您对此有疑虑,也可以使用防止对延迟敏感型任务进行压缩的相同机制。

系统行为渠道

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

此信息可间接反映整体系统行为。 不过,我们认为,了解您控制的 VMO 中的网页是否经过压缩,不足以了解有关其他进程的任何具体信息。

LZ4

虽然 LZ4 库在文件数量和 LoC 指标方面很小,但它仍然是以低级 C 语言编写的非平凡代码,可能存在 bug。该库需要包含以下内容:

  • 安全审核,以确定总体适用性。
  • 进一步强化和测试。
  • 可能会重写有问题的部分。

通过强化和重写,可以将部分或全部实现移植到更安全的表示形式,例如 Rust 或 WUFFS。

测试

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

虚拟机正确性

通用虚拟机更改和压缩特定更改的技术正确性将通过单元测试和集成测试进行评估。

对于无论内核单元测试或额外的核心测试中是否启用压缩都会生效的常规虚拟机变更,将根据需要添加。

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

系统行为

虽然压缩不是免费的,但它不应不利地回归产品负责人认为至关重要的任何行为。这些行为因产品而异,但一个示例是在低内存场景下出现音频跳过、加剧的抖动和糟糕的用户体验。

这包括手动测试产品、利用锁争用跟踪等工具,以及与产品测试团队和产品负责人合作进行评估。

文档

将记录对 zx_object_get_info 查询的添加和更改,以及其他命令行选项。

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

未知因素和迭代

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

缺点

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

用户寻呼机压缩替代方案

它可以通过用户分页器机制外包给用户空间,而不是在内核中执行压缩和解压缩。在许多方面,这与 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 等支持的情况下运行。

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

  • 成功压缩是指压缩到原始大小的至少 70%。
  • 压缩时间会针对所有网页进行统计,包括最终未以压缩形式存储的网页。这与实际使用情况相符,因为事先无法知道压缩是否会成功。
  • 解压缩时间仅针对成功压缩的网页进行统计。
  • 总大小会统计每个网页所需的存储空间,因此成功压缩的网页会将其压缩后的大小添加到统计中,而压缩失败的网页则按 4KiB 进行统计。

这种统计方式的理由是,最终目标是释放内存并尽可能减少 CPU 使用量。如果某种压缩算法有时能快速很好地压缩页面,有时却会花费大量 CPU 资源,但最终未能成功压缩,那么这种算法实际上既无法节省太多内存,又会占用大量 CPU 资源。

NUC 7

算法 压缩(MiB/s) 解压缩(MiB/秒) 解压缩最差延迟时间(纳秒) 总大小 (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/秒) 解压缩最差延迟时间(纳秒) 总大小 (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/秒) 解压缩最差延迟时间(纳秒) 总大小 (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 个网页未压缩。

老虎机 存储空间页面 总 MiB 数 节省的 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 内存管理系统。