英语

物理内存模型

系统中的物理内存可以用不同的方式寻址。最简单的情况是,物理内存从地址 0 开始,并跨越一个连续的范围,直到最大地址。然而,这个范围可能包含一些小的空洞,CPU 无法访问。然后,可能会有几个完全不同地址的连续范围。而且,不要忘记 NUMA,不同的内存库连接到不同的 CPU。

Linux 使用两种内存模型之一抽象了这种多样性:FLATMEM 和 SPARSEMEM。每个架构定义了它支持的内存模型,默认的内存模型是什么,以及是否可以手动覆盖该默认值。

所有内存模型都使用排列在一个或多个数组中的 struct page 来跟踪物理页面帧的状态。

无论选择哪种内存模型,物理页面帧编号 (PFN) 和相应的 struct page 之间都存在一一映射关系。

每个内存模型都定义了 pfn_to_page()page_to_pfn() 辅助函数,允许从 PFN 转换为 struct page,反之亦然。

FLATMEM

最简单的内存模型是 FLATMEM。此模型适用于具有连续或大部分连续物理内存的非 NUMA 系统。

在 FLATMEM 内存模型中,有一个全局 mem_map 数组,它映射了整个物理内存。对于大多数架构,空洞在 mem_map 数组中都有条目。与空洞对应的 struct page 对象永远不会完全初始化。

要分配 mem_map 数组,特定于架构的设置代码应调用 free_area_init() 函数。然而,在调用 memblock_free_all() 将所有内存交给页面分配器之前,映射数组是不可用的。

架构可以释放不覆盖实际物理页面的 mem_map 数组的部分。在这种情况下,特定于架构的 pfn_valid() 实现应考虑 mem_map 中的空洞。

使用 FLATMEM,PFN 和 struct page 之间的转换很简单:PFN - ARCH_PFN_OFFSETmem_map 数组的索引。

ARCH_PFN_OFFSET 定义了物理内存从与 0 不同的地址开始的系统的第一个页面帧编号。

SPARSEMEM

SPARSEMEM 是 Linux 中最通用的内存模型,也是唯一支持几个高级功能的内存模型,例如物理内存的热插拔和热移除、非易失性内存设备的备用内存映射以及更大系统的内存映射的延迟初始化。

SPARSEMEM 模型将物理内存表示为节的集合。节由 struct mem_section 表示,其中包含 section_mem_map,从逻辑上讲,它是指向 struct pages 数组的指针。但是,它与其他一些魔法一起存储,有助于节的管理。节的大小和最大节数使用每个支持 SPARSEMEM 的架构定义的 SECTION_SIZE_BITSMAX_PHYSMEM_BITS 常量指定。虽然 MAX_PHYSMEM_BITS 是架构支持的物理地址的实际宽度,但 SECTION_SIZE_BITS 是一个任意值。

最大节数表示为 NR_MEM_SECTIONS,定义为

NR\_MEM\_SECTIONS = 2 ^ {(MAX\_PHYSMEM\_BITS - SECTION\_SIZE\_BITS)}

mem_section 对象排列在一个名为 mem_sections 的二维数组中。此数组的大小和位置取决于 CONFIG_SPARSEMEM_EXTREME 和最大可能的节数

  • CONFIG_SPARSEMEM_EXTREME 被禁用时,mem_sections 数组是静态的,并且具有 NR_MEM_SECTIONS 行。每行包含一个 mem_section 对象。

  • CONFIG_SPARSEMEM_EXTREME 被启用时,mem_sections 数组是动态分配的。每行包含 PAGE_SIZE 大小的 mem_section 对象,并且计算行数以适合所有内存节。

架构设置代码应调用 sparse_init() 来初始化内存节和内存映射。

使用 SPARSEMEM,有两种可能的方式将 PFN 转换为相应的 struct page - “经典 sparse” 和 “sparse vmemmap”。选择在构建时进行,并且由 CONFIG_SPARSEMEM_VMEMMAP 的值确定。

经典 sparse 将页面的节号编码到 page->flags 中,并使用 PFN 的高位来访问映射该页面帧的节。在一个节中,PFN 是页面数组的索引。

sparse vmemmap 使用虚拟映射的内存映射来优化 pfn_to_page 和 page_to_pfn 操作。有一个全局 struct page *vmemmap 指针,它指向 struct page 对象的虚拟连续数组。PFN 是该数组的索引,并且 struct pagevmemmap 的偏移量是该页面的 PFN。

要使用 vmemmap,架构必须保留一系列虚拟地址,这些地址将映射包含内存映射的物理页面,并确保 vmemmap 指向该范围。此外,架构应实现 vmemmap_populate() 方法,该方法将分配物理内存并为虚拟内存映射创建页面表。如果架构对 vmemmap 映射没有任何特殊要求,它可以使用通用内存管理提供的默认 vmemmap_populate_basepages()

虚拟映射的内存映射允许将持久性内存设备的 struct page 对象存储在这些设备上预先分配的存储空间中。此存储空间由 struct vmem_altmap 表示,最终通过一长串函数调用传递给 vmemmap_populate()。vmemmap_populate() 实现可以将 vmem_altmapvmemmap_alloc_block_buf() 辅助函数一起使用,以在持久性内存设备上分配内存映射。

ZONE_DEVICE

ZONE_DEVICE 设施构建在 SPARSEMEM_VMEMMAP 之上,为设备驱动程序标识的物理地址范围提供 struct page mem_map 服务。ZONE_DEVICE 的“设备”方面与这些地址范围的页面对象永远不会标记为 online 的事实有关,并且必须针对设备进行引用,而不仅仅是页面,以使内存保持固定以供主动使用。ZONE_DEVICE,通过 devm_memremap_pages(),执行足够的内存热插拔以打开给定 pfn 范围的 pfn_to_page()page_to_pfn()get_user_pages() 服务。由于页面引用计数永远不会降到 1 以下,因此该页面永远不会被跟踪为可用内存,并且该页面的 struct list_head lru 空间被重新用于反向引用到映射内存的主机设备/驱动程序。

虽然 SPARSEMEM 将内存表示为节的集合,可以选择性地收集到内存块中,但 ZONE_DEVICE 用户需要更小的粒度来填充 mem_map。鉴于 ZONE_DEVICE 内存永远不会标记为在线,因此它随后不会受到其内存范围通过 sysfs 内存热插拔 api 在内存块边界上公开的影响。该实现依赖于缺少用户 api 约束来允许将子节大小的内存范围指定给 arch_add_memory(),即内存热插拔的上半部分。子节支持允许 2MB 作为 devm_memremap_pages() 的跨架构通用对齐粒度。

ZONE_DEVICE 的用户是

  • pmem:映射平台持久性内存,以便通过 DAX 映射用作直接 I/O 目标。

  • hmm:使用 ->page_fault()->page_free() 事件回调扩展 ZONE_DEVICE,以允许设备驱动程序协调与设备内存(通常是 GPU 内存)相关的内存管理事件。请参阅 异构内存管理 (HMM)

  • p2pdma:创建 struct page 对象,以允许 PCI/-E 拓扑中的对等设备协调它们之间的直接 DMA 操作,即绕过主机内存。