物理内存模型¶
系统中物理内存的寻址方式可能不同。最简单的情况是物理内存从地址 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_OFFSET 是 mem_map 数组的索引。
ARCH_PFN_OFFSET 定义了物理内存起始地址不是 0 的系统的第一个页帧号。
SPARSEMEM¶
SPARSEMEM 是 Linux 中最通用的内存模型,它是唯一支持多个高级功能的内存模型,例如物理内存的热插拔和热移除、非易失性存储设备的替代内存映射以及大型系统内存映射的延迟初始化。
SPARSEMEM 模型将物理内存表示为节的集合。一个节用 struct mem_section 表示,它包含 section_mem_map,逻辑上是指向 struct pages 数组的指针。然而,它存储了一些其他技巧,有助于节的管理。节的大小和最大节数由支持 SPARSEMEM 的每个架构定义的 SECTION_SIZE_BITS 和 MAX_PHYSMEM_BITS 常量指定。虽然 MAX_PHYSMEM_BITS 是架构支持的物理地址的实际宽度,但 SECTION_SIZE_BITS 是一个任意值。
最大节数表示为 NR_MEM_SECTIONS,定义为
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 page 与 vmemmap 的偏移量是该页面的 PFN。
要使用 vmemmap,架构必须保留一系列虚拟地址,这些地址将映射包含内存映射的物理页面,并确保 vmemmap 指向该范围。此外,架构应实现 vmemmap_populate()
方法,该方法将分配物理内存并为虚拟内存映射创建页表。如果架构对 vmemmap 映射没有任何特殊要求,它可以使用通用内存管理提供的默认 vmemmap_populate_basepages()
。
虚拟映射的内存映射允许在这些设备上的预分配存储中存储持久性内存设备的 struct page 对象。此存储由 struct vmem_altmap 表示,该结构最终通过一系列函数调用传递给 vmemmap_populate()。vmemmap_populate() 实现可以使用 vmem_altmap 以及 vmemmap_alloc_block_buf()
助手函数在持久性内存设备上分配内存映射。
ZONE_DEVICE¶
ZONE_DEVICE 功能建立在 SPARSEMEM_VMEMMAP 之上,为设备驱动程序标识的物理地址范围提供 struct page mem_map 服务。ZONE_DEVICE 的 “设备” 方面与以下事实相关:这些地址范围的页对象永远不会标记为在线,并且必须针对设备而不是仅仅针对页面获取引用,以保持内存固定以供活动使用。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 操作,即绕过主机内存。