本文档介绍了 Sysmem 用于管理内存的 VMOs 层次结构。本文档假定您熟悉 Fuchsia 上的虚拟内存和非 sysmem 内存的统计。
堆
所有 VMO 均从堆中分配。当前堆如下所示:
名称 | 固定池 | 说明 |
---|---|---|
Sysmem-core | 否 | 通用主内存 |
SysmemAmlogicProtectedPool | 是 | 无法从 CPU 访问内存 |
SysmemContiguousPool | 是 | 物理上连续的主内存 |
tee_secure | 是 | 专用于 Amlogic 解密编码视频的受保护内存 |
Sysmem-external-heap(可能有多个) | 否 | 目前用于金鱼 |
Sysmem-contig-core | 否 | 连续主内存;仅在没有 SysmemContiguousPool 的系统上使用 |
并非所有堆都会从固定池进行子分配;例如,“Core”和“Contig Core”可以从主内存进行分配。某些堆直接与 fuchsia.sysmem.HeapType 值相对应,但所选堆也可能取决于 BufferMemorySettings 成员(例如 is_physically_contiguous
)。
Sysmem-core
Sysmem-core 从主内存分配。如果未向内存添加任何约束条件,则此选项为默认选项。
SysmemContiguousPool
系统上的 CPU 和某些其他设备只需要虚拟连续内存。它们可以选择具有任何物理地址的任意页面,并依赖于 MMU 硬件来分配新的连续虚拟地址。这样可以轻松分配内存,因为可以使用任何物理页面。
不过,某些硬件不具备 MMU 或分散收集功能。这种硬件需要物理上连续的地址空间,即内存中的每个页面在 RAM 中的顺序相同。随着系统的运行,主内存会变得越来越碎片化,并且很难找到连续的大量可用内存,因为其他已分配的页面恰好随机分散在内存中。
为了避免出现此问题,Sysmem 有一个单独的连续内存池。它会在启动后不久(内存碎片化之前)分配一个大型内存池,然后将较小的部分分配给应用。从理论上讲,这种内存可能仍然会出现碎片化,但在实践中,由于只有较大的内存块从池中分配,并且一个块中的所有内存都会同时释放回池中,因此这种方法是可行的。
SysmemAmlogicProtectedPool
在搭载 Amlogic SoC 的系统中,需要在具有访问控制权限的特殊区域中分配 DRM(受保护内存)视频,以确保应用无法读取已解密的视频。CPU 必须无法访问这些区域,只有 GPU 和其他硬件才能在特殊模式下访问这些区域,在这种模式下,硬件会通过线路连接,以确保其不会泄露内存。
内存不能被任意标记为受保护或不受保护。硬件只能将少量(小于 32 个)区域标记为受保护区域。为此,sysmem 可以在启动后立即分配一个受保护的池(类似于连续池),并告知固件保护该区域中的所有内存。然后,它可以从此受保护内存池中进行子分配。
tee_secure
tee_secure 适用于存储其他类型数据的另一种受保护内存。必须告知 Zircon 不要从内存中分配,并且永远不要触碰它。ZBI另一个驱动程序可以从固件检索内存及其位置信息,然后告知 sysmem。Sysmem 可以根据需要从此堆中进行子分配。
Sysmem-external-heap
外部堆不一定使用实际内存。例如,Goldfish 堆是一个外部堆,表示 FEMU 虚拟机外部的视频内存。客户端可以传递 VMO 句柄,但不应直接写入内存;而是由 Goldfish 驱动程序使用 VMO koid 查找主机资源。
VMO 层次结构
Sysmem 使用 VMO 层次结构来跟踪客户端的内存用量。有三种方法可以让 VMO 保持活跃状态:
VMO 的句柄。
将 VMO 映射到进程的地址空间。
表示已映射到设备的 PMT。
出于安全考虑,在所有这些类型的引用都消失之前,sysmem 无法回收 VMO 的内存以供其他客户端使用。对于普通 VMO,内核会通过仅在所有引用都消失后销毁 VMO 来处理此问题。但是,sysmem 会从较大的物理地址范围中进行 VMO 子分配,因此需要了解 VMO 是否已被销毁,以便决定要重复使用哪些内存范围。
内核支持 ZX_VMO_ZERO_CHILDREN
信号,以便处理这些用例:如果 VMO 的所有子项均已关闭,则系统会在父 VMO 上发送 ZX_VMO_ZERO_CHILDREN
信号。
客户端叶 VMO
这些是分发给客户端的 VMO;客户端会在 VMO 分配之前通过调用 BufferCollection.SetName 为它们命名。客户端还可以直接在 VMO 上设置 ZX_PROP_NAME
,但不建议这样做,因为 sysmem 驱动程序无法访问该名称。
只要 BufferCollection 继续引用这些 VMO,Sysmem 也会保留对这些 VMO 的引用,即使目前没有任何子项具有 VMO 句柄也是如此。
中间 VMO
每个叶 VMO 都有一个中间 VMO 作为父级。叶级 VMO 和中间 VMO 之间是一对一的映射关系。这些名称由堆设置,对于来自堆的所有 VMO,这些名称通常是固定的。例如,来自连续池的 VMO 的 SysmemContiguousPool-child。
Sysmem 使用这些 VMO 来检测是否已清除对叶 VMO 的所有引用;一旦收到 ZX_VMO_ZERO_CHILDREN 信号,它就会知道可以安全地删除 VMO 并可能重复使用空间。中间 VMO 绝不会传递到系统进程之外,因此客户端绝不能直接引用它们。
堆 VMOs
这些值代表堆中的 VMO 从中分配内存的整个内存池。它们通常在启动后立即分配,以确保有足够的内存可用。堆 VMO 还可以表示划出的物理地址范围,例如 tee_secure 会叠加引导加载程序分配的特定物理范围。
中间 VMO 是从堆 vmo 分配的slice,因此每个中间 VMO 都代表堆中的不同内存范围
如果堆不代表物理内存池,则不需要堆 VMO。在这种情况下,系统会分配中间 VMO,而无需父级 VMO。
报告内存
检查
Sysmem 提供 Inspect 层次结构,以向快照和其他客户端应用报告其内存用量。下面是一个简单的层次结构示例:
root:
sysmem:
collections:
logical-collection-0:
allocator_id = 1
heap = 0
min_coded_height = 1024
min_coded_width = 600
name = vc-framebuffer
pixel_format = 101
pixel_format_modifier = 0
size_bytes = 2490368
vmo_count = 1
collection-5:
channel_koid = 20048
debug_id = 5498
debug_name = driver_host
collection-6:
channel_koid = 20050
debug_id = 5498
debug_name = driver_host
collection-at-allocation-7:
debug_id = 19829
debug_name = virtual-console.cm
min_buffer_count = 1
collection-at-allocation-8:
debug_id = 5498
debug_name = driver_host
collection-at-allocation-9:
debug_id = 5498
debug_name = driver_host
vmo-20085:
koid = 20085
heaps:
SysmemContiguousPool:
allocations_failed = 0
allocations_failed_fragmentation = 0
free_at_high_water_mark = 37498880
high_water_mark = 2490368
id = 1
is_ready = true
last_allocation_failed_timestamp_ns = 0
max_free_at_high_water = 37498880
size = 39989248
used_size = 2490368
vmo-20085:
koid = 20085
size = 2490368
SysmemRamMemoryAllocator:
id = 0
Sysmem 会通过 /dev/diagnostics/class/sysmem/XXX.inspect
文件中的检查层次结构报告其对内存的看法(其中 XXX 是伪随机 3 位数字标识符)。所显示的每个逻辑集合都代表一组由一组客户端分配的相同缓冲区。这些逻辑集合包含该集合中实时中间 VMO 的 koid 列表。koid 在系统生命周期内是唯一的,可用于在 memgraph 输出中唯一标识 sysmem VMO。
所有堆也都有检查节点。这些信息可能包括所有子 VMO 的大小和 koid,以及有关堆已满程度和是否失败分配的信息。某些堆仅包含名称和 ID 属性,而没有从中分配的 VMO 的相关信息。
逻辑集合的 allocator_id
与用于分配其内存的堆的 id
相匹配。
由于 sysmem 无法查看系统中的其他进程,因此可检查的数据有限。例如,它不知道哪些其他进程持有对其 VMO 的引用,只知道至少有一个进程持有。它也不知道创建 VMO 的客户端进程的确切名称。Sysmem 客户端应使用其进程名称和 koid 调用 Allocator.SetDebugClientInfo,但系统不会强制执行此操作,也无法保证客户端设置的名称是否正确。
不过,有些信息只能通过检查数据来确定。例如,客户端进程可以保留对 BufferCollection 的通道,而无需保留对 VMO 的任何句柄。只有 sysmem 知道其进程中 BufferCollection 通道和 VMO 之间的映射。channel_koid
属性提供有关频道服务器 koid 的信息。
ZX_INFO_PROCESS_VMOs
memgraph
和 mem
工具使用此系统调用。它可以确定哪些进程有对 VMO 的引用,这对于以安全的方式将内存归因于进程至关重要。
sysmem 使用的 VMO 层次结构可能会导致这些工具出现问题。例如,mem
会忽略没有任何提交内存(有物理内存支持的已分配内存)的 VMO,以免输出内容过于繁杂。这会导致 mem 忽略叶 VMO,因为树中的根 VMO 实际上分配了内存。Mem 有一些黑客技巧,可将内存信息沿着树向下传播到 SysmemContiguousPool
和 SysmemAmlogicProtectedPool
的子 VMO,它会查看叶 VMO 的“大小”,并假定所有这些内存都已分配。这仅适用于分配时不会重叠的固定大小的池,因此它仅限于一组硬编码的池。
外部堆 VMO 也很复杂,因为它们实际上并不占用客机虚拟机中的内存。因此,mem 不报告它们(它们的已提交内存大小为 0)是正确的做法,但这意味着很难将主机系统上的内存归因于客户机中的进程。
memgraph -v
会对内存信息进行较少的处理,但用户需要自行进行处理以确定内存用量。此外,由于 VMO 不一定具有一致的名称,因此也可能很难确定哪些 VMO 来自 sysmem。
统一方法
任何希望全面准确地了解 sysmem VMO 的实用程序都必须合成 inspect 和 ZX_INFO_PROCESS_VMOS
信息。Sysmem 的检查数据应是哪些 sysmem VMO 存在的可信来源,而内核是哪些进程持有 VMO 引用的可信来源。这需要迭代逻辑缓冲区集合条目并列出其 koid,然后查看 ZX_INFO_PROCESS_VMOS
以查找其大小以及引用其子项的进程。
实用程序可以为每个进程创建 ZX_INFO_HANDLE_TABLE
快照。然后,它可以使用该表在 channel_koid
中查找 koid,以确定哪个进程保留了该 BufferCollection。
在某些情况下,系统无法正确计算内存用量。主要问题是,通道消息中持有的句柄未在任何位置报告,因此无法对这些引用进行说明。客户端可以将 VMO 句柄推入到通道中,而从不从通道中读取,甚至内核也不知道将内存归因于谁。在这些情况下,可以将调试客户端信息用作后备。
未来可能发生的变化
为每个客户端创建一个中间 VMO,以便 sysmem 自行确定哪些客户端仍有对 VMO 的引用。
让组件框架将不可伪造的标识符传递给 sysmem,而不是让客户端传递可伪造的调试名称。