原文:https://www.kernel.org/doc/html/latest/vm/memory-model.html
Physical Memory Model
系统中的物理内存可以用不同的方式寻址。最简单的情况是物理内存从地址 0 开始并跨越一个连续范围直到最大地址。但是,此范围可能包含 CPU 无法访问的小孔。那么在完全不同的地址上可能有几个连续的范围。而且,不要忘记 NUMA,不同的内存库连接到不同的 CPU。
Linux 使用以下两种内存模型之一来抽象这种多样性:FLATMEM 和 SPARSEMEM。每个架构都定义了它支持的内存模型、默认内存模型是什么以及是否可以手动覆盖该默认值。
所有内存模型都使用排列在一个或多个数组中的 struct page 来跟踪物理页框的状态。
无论选择何种内存模型,物理页帧号(PFN)和对应的结构页之间都存在一对一的映射关系。
每个内存模型都定义了 pfn_to_page() 和 page_to_pfn() 帮助器,它们允许从 PFN 到 struct page 的转换,反之亦然。
FLATMEM
最简单的内存模型是 FLATEM。此模型适用于具有连续或大部分连续物理内存的非 NUMA 系统。
在FLATMEM内存模型中,有一个全局mem_map数组映射整个物理内存。对于大多数架构,这些孔在 mem_map 数组中都有条目。与孔对应的结构页面对象永远不会完全初始化。
要分配 mem_map 数组,特定于体系结构的设置代码应调用 free_area_init() 函数。然而,在调用 memblock_free_all() 将所有内存交给页面分配器之前,映射数组是不可用的。
架构可能会释放 mem_map 数组中未覆盖实际物理页面的部分。在这种情况下,特定于体系结构的 pfn_valid() 实现应该考虑 mem_map 中的漏洞。
使用 FLATMEM,PFN 和结构页面之间的转换很简单:PFN – ARCH_PFN_OFFSET 是 mem_map 数组的索引。
ARCH_PFN_OFFSET 为物理内存从非 0 地址开始的系统定义了第一个页帧号。
SPARSEMEM
SPARSEMEM 是 Linux 中最通用的内存模型,它是唯一支持多种高级功能的内存模型,例如物理内存的热插拔和热移除、非易失性内存设备的替代内存映射和延迟初始化 大型系统的内存映射。
SPARSEMEM 模型将物理内存表示为 section 的集合。 一个section 由包含 section_mem_map
的 struct mem_section
表示,从逻辑上讲,它是指向 struct page
数组的指针。同时也存储了有关section管理的结构。 SECTION_SIZE_BITS 和 MAX_PHYSMEM_BITS 常量指定每个Section的大小和理论最大值,其中MAX_PHYSMEM_BITS 取决于架构,如64位的架构即为2^64字节大小. 则系统中最多可以拥有 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 对象,并且计算行数以适合所有内存节。
以上的设计啥意思呢,可以这里理解,在一开始,所有的mem_section 都是在一个静态的一维数组中存放,此数组即mem_sections ,但是如果系统是非常稀疏的,即大量的section都是指向NULL,那么其实这个一维数组就非常的浪费内存,这个时候就参考了页表的设计,使用二维数组,类似于二级页表,然后使用CONFIG_SPARSEMEM_EXTREME 作为参数,进行兼容,如果CONFIG_SPARSEMEM_EXTREME禁用,那么每一个行仅仅有一个mem_section ,即mem_sections 是一个 mem_sections[NR_MEM_SECTIONS ][1]大小的数组,那这不就是和一维数组没区别吗;但是如果开启了CONFIG_SPARSEMEM_EXTREME ,那么每行就不是1个mem_section ,具体有多少个取决于一个page可以存放多少mem_section ,比如一个mem_section 大小是4B,那么4KB的page就能存放村1000个mem_section .和页表的设计完全一致. 而此时mem_sections[n] 则执行一个一维的mem_section 数组,数组首地址即page的物理地址.
架构设置代码应调用 sparse_init() 来初始化内存Section和内存Maps。
使用 SPARSEMEM 有两种可能的方法将 PFN 转换为相应的page结构 – “classic sparse”和“sparse vmemmap”。选择那种方式是在构建时进行的,它由 CONFIG_SPARSEMEM_VMEMMAP 的值决定。
classic sparse 在 page->flags 中对页面的Scetion Number进行编码,同时使用 PFN 的高位来访问映射该page的section。在Section中,PFN 是页面数组的索引。
sparse vmemmap 使用虚拟映射的内存映射来优化 pfn_to_page 和 page_to_pfn 操作。有一个全局 struct page *vmemmap 指针指向一个几乎连续的 struct page 对象数组。 PFN 是该数组的索引,结构页面与 vmemmap 的偏移量是该页面的 PFN。
要使用 vmemmap,架构必须保留一个虚拟地址范围,这些地址将映射包含内存映射的物理页面,并确保 vmemmap 指向该范围。此外,该架构应该实现 vmemmap_populate() 方法,该方法将分配物理内存并为虚拟内存映射创建页表。如果架构对 vmemmap 映射没有任何特殊要求,则可以使用通用内存管理提供的默认 vmemmap_populate_basepages()。
虚拟映射内存映射允许将持久内存设备的结构页面对象存储在这些设备上的预分配存储中。此存储由 struct vmem_altmap 表示,最终通过一长串函数调用传递给 vmemmap_populate()。 vmemmap_populate() 实现可以使用 vmem_altmap 和 vmemmap_alloc_block_buf() 助手在持久内存设备上分配内存映射。
ZONE_DEVICE
ZONE_DEVICE 工具基于 SPARSEMEM_VMEMMAP 为设备驱动程序识别的物理地址范围提供结构页面 mem_map 服务。 ZONE_DEVICE 的“设备”方面与这些地址范围的页面对象从不在线标记这一事实有关,并且必须对设备进行引用,而不仅仅是保持内存固定以供活动使用的页面。 ZONE_DEVICE 通过 devm_memremap_pages() 执行刚好足够的内存热插拔,以在给定的 pfns 范围内打开 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: Map platform persistent memory to be used as a direct-I/O target via DAX mappings.
hmm: Extend ZONE_DEVICE with ->page_fault() and ->page_free() event callbacks to allow a device-driver to coordinate memory management events related to device-memory, typically GPU memory. See Heterogeneous Memory Management (HMM).
p2pdma: Create struct page objects to allow peer devices in a PCI/-E topology to coordinate direct-DMA operations between themselves, i.e. bypass host memory.