RFC-0219:Zircon 页面压缩

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

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

问题
  • 60238
Gerrit 更改
  • 685583
作者
审核人
提交日期(年-月-日)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

社交

RFC 前提案文档,其中包含分享给 Zircon 内核团队和更广泛的利益相关方的内核设计和实现部分。

设计

本部分概述了实现、压缩策略的高级设计选择,以及建议的 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:1 的压缩比。

实施过程非常简单,包括:

  • 在任意现有的部分填充网页中搜索广告位。
  • 如果没有现成的合适位置,可将其放入新页面的左侧位置。

两者的复杂程度为:

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

这种策略的好处如下:

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

可能需要处理的一种病理行为是,页面未经压缩后,可能存在可以填充并释放其他存储页的漏洞。此类压缩系统可以在需要时实现。

从长远来看,它可以演变为或使用更通用的堆或 slab 样式分配器。

如需了解一些初始结果,请参阅“实验评估”部分。

会计和指标

虽然压缩操作无需用户输入,但确实会影响内存用量的报告方式。此外,我们还希望向用户空间中渗漏有关压缩执行情况的更多信息。

内容与承诺

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

这些现有查询包括:

  • ZX_INFO_TASK_STATS,用于报告 mem_private_bytesmem_shared_bytes 等中的承诺内存量。
  • ZX_INFO_PROCESS_MAPS,其中报告已映射的 VMO 的 committed_pages
  • ZX_INFO_PROCESS_VMOS / ZX_INFO_VMO(报告 committed_bytes)。
  • ZX_INFO_KMEM_STATS / ZX_INFO_KMEM_STATS_EXTENDED,用于报告计为已提交的 vmo_bytes

除了已提交以外,我们还将额外添加内容字节数/网页数的概念。任何涉及承诺内存的查询都继续适用于直接将内容保存在 VMO 中的内存。而内容指的是内核为用户保留的数据量,并不具体如何说明。也就是说,内容可以直接保存在页面中,因此也会计入已提交,或会被压缩而不计入已提交。

如此大规模的压缩内容被视为存储大小未知。尝试对存储空间中的各个压缩页面进行归因既复杂又不稳定,因为存储空间大小将取决于系统总体压缩行为和导致的碎片化。

虽然匿名 VMO 从概念上看是零,但这并不被视为内核能够记住的内容。如果用户明确提交或修改任何页面,这些页面会变成内容。之后,即使用户离开或将内容重置为 0,内核也没有义务注意这一点,并可能继续将其视为正在跟踪的内容。

就逐出和用户分页器而言,可从用户分页器中检索到的内容不会被视为内核跟踪的内容。因此,逐出量会从承诺量和内容量中减去。

指标

为了了解压缩的性能(无论是为了调整 / 开发还是持续的系统运行状况监控),我们需要收集并提供指标。

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

  • 压缩存储比率。
  • 页面压缩后的时间长度。
  • 哪些类型的网页会被压缩,以及哪些页面会被压缩。
  • 解压缩延迟时间。
  • 执行压缩和解压缩所花费的 CPU 时间。
  • 压缩尝试成功与失败的比率。

API 更改

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

  • 需要 zx_info_maps_mapping_t 才能具有 content_pages 字段。
  • 需要 zx_info_vmo_t 才能具有 content_bytes 字段。
  • ZX_INFO_TASK_RUNTIMEpage_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。否则,系统会保留总定义、最新定义和最早定义。

虽然回收可压缩的内存并不会直接导致 PMM 可用内存增加,但这些查询并不代表可回收的精确 PMM 内存。相反,它们可让您深入了解内存在不同年龄之间的相对分布情况,并可用于执行验证,例如 oldest_bytes 在设备 OOM 时呈下降趋势且接近于零等。

针对用户分页器支持的可逐出内存和可压缩匿名内存的多个字段也存在一些问题。任何情况下,能够区分它们是很有价值的,但大多数时候,我期望获得两者的汇总报告。

停用压缩 / 延迟敏感 VMO

对于对延迟敏感的应用,需要一种机制来在 VMO 上停用压缩功能,因为无论解压缩延迟有多短,可能都会超出其容忍能力。

如何避免回收和其他类型的内核 activity 可能会增加内存访问的延迟时间,这是一个正在着手解决的问题。因此,本文不提供任何设计,但这项工作依赖于压缩功能。

扩展程序

除了最初提出的设计之外,我们还可以进行一些探索和改进。虽然它们通常属于实现细节中,但在此处添加它们是为了激励此设计的长期适宜性。以下改进绝对应在对产品使用之前进行:

  • 移除单独的零页面去重处理,改为采用压缩格式。

除此之外,可能提供实用优势的可选建议包括:

  • 在执行逐出之前压缩分页器支持的内存。
  • 支持执行压缩,而不会舍弃未压缩的页面,直至内存紧张。
  • 浏览额外的旧页面列表,并热切地压缩位于 LRU 队列中的网页。

也可以通过进一步压缩存储和压缩算法调整的方式进行直接演变,或者为不同产品提供选项。这些可能需要单独的 RFC,具体取决于它们的侵入程度或不同程度。

实现

实现流程将分为此处所列的多个阶段。

公共基架

我们将首先实现常见的虚拟机系统更改,以支持匿名页面的老化和跟踪。这些更改没有行为或 API 更改,但风险最高的更改,先执行这些操作可以实现以下效果:

  • 将更改保留在树中,并尽可能延长测试时间。
  • 能够衡量候选压缩池(即旧的匿名网页)。

内核实现

完成常见更改后,主内核实现可以位于树中功能标志后面。这样,您就可以着陆和测试压缩的实现,以确保其稳定,然后再通过任何 API 公开其是否存在。

API 演变

针对指标和计费方式更改执行 API 演变和结构体迁移。由于内核实现位于树中,因此可以连接 API 更改,但压缩操作仍将位于功能标志后面,因此除了其他字段之外,常规用户不会看到任何功能更改。

集成、扩展和调优

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

此测试的结果将决定:

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

扩展和微调

扩展设计的部分内容现在可以着陆,并且总体实现将持续调整。

性能

您需要评估两个方面的效果。

常见开销

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

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

其他 VMO 路径会产生少量开销来维护反向链接和更新存在时间,但这应该被纳入考虑范围,不会产生可衡量的影响。

这些路径由现有的 VMO 微基准(将成为主要验证工具)涵盖。

压缩性能

压缩本质上是在 CPU 与内存之间进行权衡,因此其性能只能结合产品及其需求来评估。

除此之外,我们提出的指标 API 还可让您了解压缩的开销,然后压缩可调参数可以根据节省的内存量来控制这些成本。

安全注意事项

计时渠道

虽然压缩的页面可能具有同一位置的存储空间,但访问此存储空间的过程已定义,以使 VMO 上没有可能产生可测量通道的传递依赖项。此举是有意避免的,目的是避免可能类似于内存去重攻击的攻击。

尽管缺少传递依赖项,如果攻击者能够控制与密钥一起位于同一页面中的数据,则仍可能会创建计时渠道。这些数据经过了精心制作,使得压缩根据 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,在 Linux 中,它由用户空间文件系统驱动程序实现。遗憾的是,它也有很多同样的缺点。

权益

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

  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 的复杂性。现在,它变成了一个打包字段,而不是一个计数器,并且所有读写操作都需要保留或遮盖额外的位。
  • 访问收集(发生在持有拱形空间锁的情况下)会随着队列信息需要解码和更新正确计数而增加。
  • 多个计数字段增加了复杂性,以保持它们的一致性。

正如该方案中所述,考虑到额外的实现复杂性和运行时费用,我认为分页器字节字段实际上用于哪些用途,具有单独的计数几乎没有什么价值。

实验评估

为了提出 LZ4 压缩算法和压缩的存储策略,我们构建了一个实验性压缩版本,该压缩文件包含充足的部分,可以:

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

目的是通过以下因素评估不同的压缩算法:

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

第二点特别重要,因为内核代码在不支持 FPU/SSE/AVX 等的情况下运行。

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

  • 成功压缩意味着压缩到原始大小的至少 70%。
  • 系统会计入所有压缩时间,包括最终未压缩存储的网页。这与实际使用情况一致,在这种情况下,不知道压缩是否会成功。
  • 系统只会对成功压缩的网页统计解压缩时间。
  • 总大小会计入每个页面所需的存储空间,因此成功压缩的页面会计入压缩大小,而压缩失败的页面则会计为 4KiB。

采用这种计数方式的原因是,其最终目标是释放内存并使用最少的 CPU。如果压缩算法有时会非常好地快速压缩网页,但会占用大量 CPU 却导致压缩失败,那么实际上并不会节省大量内存,也会占用大量的 CPU。

NUC 7

算法 压缩 (MiB/s) 解压缩 (MiB/s) 解压缩最差延迟时间(纳秒) 总大小 (MiB)
Zstd(-7) 752.1470396 /admanager/answer/1446.748532 9302.3125 18.61393929
Minilzo 1212.400326 /admanager/answer/2135.49407 /admanager/answer/6817.839844 16.16410065
lz4(1) /admanager/answer/1389.675715 /admanager/answer/2920.094092 /admanager/answer/6355.849609 17.21983242
lz4(11) 1801.55426 3136.318174 5255.708984 /admanager/answer/19.92521095
WKDM 1986.560443 3119.523406 3095.283203 21.98047638

ARM A53

算法 压缩 (MiB/s) 解压缩 (MiB/s) 解压缩最差延迟时间(纳秒) 总大小 (MiB)
Zstd(-7) 189.3189527 486.6744277 /admanager/answer/29856.93359 18.61393929
Minilzo 317.4546381 528.877112 /admanager/answer/15633.99219 16.16410065
lz4(1) /admanager/answer/294.5410547 1127.360347 /admanager/answer/12683.55273 17.21983242
lz4(11) 378.2672263 /admanager/answer/1247.749072 12452.67676 /admanager/answer/19.92521095
WKDM 337.6087322 471.9796684 14254.39453 21.98047638

ARM A73

算法 压缩 (MiB/s) 解压缩 (MiB/s) 解压缩最差延迟时间(纳秒) 总大小 (MiB)
Zstd(-7) /admanager/answer/284.3621221 623.15235 20958.90332 18.61393929
Minilzo 487.4546545 747.9218419 /admanager/answer/16784.83105 16.16410065
lz4(1) 441.5927551 /admanager/answer/1159.839867 10007.28418 17.21983242
lz4(11) 573.7741984 /admanager/answer/1240.964187 9987.875 /admanager/answer/19.92521095
WKDM /admanager/answer/639.5418085 597.6976995 13867.47266 21.98047638

压缩后的存储空间

使用相同的 64MiB 测试数据集来评估所提议的压缩存储系统。在本例中,我们通过两种模式(只有左槽和右槽位的双页系统)以及提议的三槽位系统对其进行了评估。这样做的目的是尝试量化中间槽比真正简单的双槽系统包含的复杂性所带来的好处。

LZ4 是提议的压缩算法,用于生成要存储的压缩数据,并将加速系数设置为 11。这意味着,16384 个输入页中有 12510 个产生需要存储的数据,有 3874 个未压缩。

老虎机 存储页面 总 MiB 节省 MiB 压缩比
双槽 6255 土耳其里拉 24.4 岁 1.62
三槽 4276 31.8 岁 32.1 土耳其里拉

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

早期技术和参考资料

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

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