Linux 内存模型(Memory: the flat, the discontiguous, and the sparse)

文章目录

  • 前言
  • 一、FLATMEM
  • 二、DISCONTIGMEM
  • 三、SPARSEMEM
  • 总结
  • 参考资料

前言

计算机系统中的物理内存是一种宝贵的资源,因此人们付出了大量的努力来有效地管理它。由于现代系统上存储器体系结构的复杂性,这项任务变得更加困难。有几个抽象层处理如何布局物理内存的细节;其中一个简单地称为“内存模型”。内核中支持三个模型,但其中一个正在退出。作为理解这种变化的一种方式,本文将更深入地研究内核内存模型的演变、它们的当前状态以及它们可能的未来。

一、FLATMEM

在Linux的早期,内存是扁平的:它是一个简单的线性序列,物理地址从零开始,到几兆字节结束。每个物理页面帧(physical page frame)在内核的mem_map数组中都有一个条目(entry),当时,该数组为每个页面包含一个 unsigned short 条目,该条目统计该页面的引用数。然而,很快地,mem_map条目中的功能增加,包括用于管理内存交换的 age 和脏计数器。

平面内存映射提供了物理页面帧号(page-frame number - PFN)与其对应 struct page 之间的简单快速转换;这只是计算数组索引的一个简单问题。struct page 的布局发生了变化,以适应新的用途(例如页面缓存),并优化 struct page 访问的缓存性能。内存映射仍然是一个整洁高效的平面阵列,但它有一个主要缺点:它不能很好地处理物理地址空间中的大的空洞 (large holes)。要么对应于空洞的内存映射部分将被浪费,要么就像在ARM上所做的那样,内存映射也将具有空洞。

二、DISCONTIGMEM

作为使Linux在NUMA机器上运行良好的努力的一部分,在1999年的内存管理子系统中引入了对高效处理广泛不连续的物理内存的支持。该代码依赖于CONFIG_DISCONTIGMEM配置选项,因此第一个有名称的内存模型是DISCONTIGMEM。

DISCONTIGMEM模型引入了内存节点的概念,它仍然是NUMA内存管理的基础。每个节点都有一个独立的内存管理子系统,具有自己的空闲页面列表、正在使用的页面列表、最近最少使用(LRU)信息和使用情况统计信息。由struct pglist_data(或简称pg_data_t)表示的节点数据包含特定于节点的内存映射。假设每个节点都有连续的物理内存,每个节点有一个页面结构数组可以解决平面内存映射中的大空洞问题。

但这并不是免费的。例如,使用DISCONTIGMEM时,必须确定内存中哪个节点持有给定页,以便将其PFN转换为关联的struct page。同样,必须确定哪个节点的内存映射包含一个struct page来计算其PFN。经过长期的发展,从MIPS64体系结构第一次定义KVADDR_TO_NID()、LOCAL_MAP_BASE()、ADDR_TO_MAPBASE()和LOCAL_BASE_ADDR()宏开始,PFN与struct page之间的相互转换开始依赖于相对简单的pfn_to_page()和page_to_pfn()宏,这些宏在 include/asm-generic/memory_model.h 中定义。

/*
 * supports 3 memory models.
 */
#if defined(CONFIG_FLATMEM)

#define __pfn_to_page(pfn)	(mem_map + ((pfn) - ARCH_PFN_OFFSET))
#define __page_to_pfn(page)	((unsigned long)((page) - mem_map) + \
				 ARCH_PFN_OFFSET)
#elif defined(CONFIG_DISCONTIGMEM)

#define __pfn_to_page(pfn)			\
({	unsigned long __pfn = (pfn);		\
	unsigned long __nid = arch_pfn_to_nid(__pfn);  \
	NODE_DATA(__nid)->node_mem_map + arch_local_page_offset(__pfn, __nid);\
})

#define __page_to_pfn(pg)						\
({	const struct page *__pg = (pg);					\
	struct pglist_data *__pgdat = NODE_DATA(page_to_nid(__pg));	\
	(unsigned long)(__pg - __pgdat->node_mem_map) +			\
	 __pgdat->node_start_pfn;					\
})

#elif defined(CONFIG_SPARSEMEM_VMEMMAP)

/* memmap is virtually contiguous.  */
#define __pfn_to_page(pfn)	(vmemmap + (pfn))
#define __page_to_pfn(page)	(unsigned long)((page) - vmemmap)

#elif defined(CONFIG_SPARSEMEM)
/*
 * Note: section's mem_map is encoded to reflect its start_pfn.
 * section[i].section_mem_map == mem_map's address - start_pfn;
 */
#define __page_to_pfn(pg)					\
({	const struct page *__pg = (pg);				\
	int __sec = page_to_section(__pg);			\
	(unsigned long)(__pg - __section_mem_map_addr(__nr_to_section(__sec)));	\
})

#define __pfn_to_page(pfn)				\
({	unsigned long __pfn = (pfn);			\
	struct mem_section *__sec = __pfn_to_section(__pfn);	\
	__section_mem_map_addr(__sec) + __pfn;		\
})
#endif /* CONFIG_FLATMEM/DISCONTIGMEM/SPARSEMEM */

/*
 * Convert a physical address to a Page Frame Number and back
 */
#define	__phys_to_pfn(paddr)	PHYS_PFN(paddr)
#define	__pfn_to_phys(pfn)	PFN_PHYS(pfn)

#define page_to_pfn __page_to_pfn
#define pfn_to_page __pfn_to_page

不论是哪种内存模型,PFN与struct page之间的相互转换公共接口都是 pfn_to_page()和page_to_pfn()宏。

#define page_to_pfn __page_to_pfn
#define pfn_to_page __pfn_to_page

然而,DISCONTIGMEM有一个弱点:内存热插拔和热删除。实际的NUMA节点粒度太粗,无法提供适当的热插拔支持,拆分节点会产生大量不必要的碎片和开销。请记住,每个节点都有一个独立的内存管理结构以及所有相关的成本;进一步拆分节点会显著增加这些成本。

三、SPARSEMEM

随着SPARSEMEM的引入,上述DISCONTIGMEM模型的限制得到了解决。该模型将内存映射抽象为由体系结构定义的任意大小的 sections 的集合。由struct mem_section表示的每个节(如代码中所述)是:“从逻辑上讲,是一个指向 struct pages 数组的指针。然而,它是以其他 magic 方式存储的”。这些部分的数组是一个元内存映射,它可以在SECTION_SIZE粒度上被有效地截断。为了在PFN和 struct page 之间进行有效转换,PFN的几个高位用于索引到节数组中。对于另一个方向,节号被编码在页面标志(page flags)中。

struct mem_section {
	/*
	 * This is, logically, a pointer to an array of struct
	 * pages.  However, it is stored with some other magic.
	 * (see sparse.c::sparse_init_one_section())
	 *
	 * Additionally during early boot we encode node id of
	 * the location of the section here to guide allocation.
	 * (see sparse.c::memory_present())
	 *
	 * Making it a UL at least makes someone do a cast
	 * before using it wrong.
	 */
	unsigned long section_mem_map;

	struct mem_section_usage *usage;
#ifdef CONFIG_PAGE_EXTENSION
	/*
	 * If SPARSEMEM, pgdat doesn't have page_ext pointer. We use
	 * section. (see page_ext.h about this.)
	 */
	struct page_ext *page_ext;
	unsigned long pad;
#endif
	/*
	 * WARNING: mem_section must be a power-of-2 in size for the
	 * calculation and use of SECTION_ROOT_MASK to make sense.
	 */
};

在SPARSEEM引入Linux内核几个月后,SPARSEEM扩展了SPARSEEM_EXTREME,它适用于物理地址空间特别 sparse 的系统。SPARSEMEM_Extreme向Sections数组添加了第二个维度,并使此数组变得 sparse 。使用SPARSEMEM_EXTREME,第一级变成了指向mem_section结构的指针,并且实际的mem_section对象是根据实际填充的物理内存动态分配的。
centos 7 ,x86_64架构:

# cat /boot/config-3.10.0-1160.el7.x86_64 | grep SPARSEMEM_EXTREME
CONFIG_SPARSEMEM_EXTREME=y

Kylin Linux Desktop V10,aarch64架构:

# cat /boot/config-5.4.18-74-generic | grep SPARSEMEM_EXTREME
CONFIG_SPARSEMEM_EXTREME=y
/*
 * Permanent SPARSEMEM data:
 *
 * 1) mem_section	- memory sections, mem_map's for valid memory
 */
#ifdef CONFIG_SPARSEMEM_EXTREME
struct mem_section **mem_section;

EXPORT_SYMBOL(mem_section);

mem_section是一个全局二维数组,struct mem_section **表示指向 struct mem_section 结构体指针的指针。mem_section 数组用于存储指向描述各个内存区域的 struct mem_section 结构体的指针。这个结构体是用来管理 SPARSEMEM 内存模型中的内存区域的。

SPARSEMEM的另一个增强是在2007年添加的;它称为对SPARSEMEM的通用虚拟内存映射支持,或SPARSEMEM_VMEMMAP。SPARSEMEM_VMEMMAP背后的思想是将整个内存映射到一个几乎连续的区域,但只有活动部分使用物理页面进行支持。这种模型在32位系统中不能很好地工作,因为在32位系统中,物理内存大小可能接近甚至超过虚拟地址空间。然而,对于64位系统来说,SPARSEMEM_VMEMMAP显然是很有好的。以额外的页表条目为代价,page_to_pfn()和pfn_to_page()变得与平面模型一样简单。

# cat /boot/config-3.10.0-1160.el7.x86_64 | grep SPARSEMEM_VMEMMAP
CONFIG_SPARSEMEM_VMEMMAP_ENABLE=y
CONFIG_SPARSEMEM_VMEMMAP=y
# cat /boot/config-5.4.18-74-generic | grep SPARSEMEM_VMEMMAP
CONFIG_SPARSEMEM_VMEMMAP_ENABLE=y
CONFIG_SPARSEMEM_VMEMMAP=y

请参考git提交:index : kernel/git/torvalds/linux.git

Generic Virtual Memmap support for SPARSEMEM:
SPARSEEM是一个非常好的框架,它在所有架构上统一了相当多的代码。如果它可以成为默认值,那么我们就可以摆脱各种形式的DISCONTIG和内存映射上的其他变体,那就太好了。到目前为止,阻碍这一点的是SPARSEEM为virt_to_page和page_address引入的额外查找。这样一来,执行此操作的代码必须保存在一个单独的函数中,并且不能内联使用。

此补丁为SPARSEEM引入了一种虚拟memmap模式,在该模式中,memmap被映射到一个虚拟连续区域,只有活动部分得到物理支持。这允许virt_to_page/page_address成为简单的移位/添加操作。不需要页面标志字段,不需要表查找,也不需要任何涉及内存的内容。

两个关键操作pfn_to_page和page_to_page变为:

/* memmap is virtually contiguous.  */
#define __pfn_to_page(pfn)      (vmemmap + (pfn))
#define __page_to_pfn(page)     ((page) - vmemmap)

通过对内存映射进行虚拟映射,我们可以在不浪费物理内存的情况下进行简单的访问。由于内核内存通常已经1:1映射,这不会带来额外的开销。虚拟映射必须足够大,以允许为所有有效的物理页分配和映射结构页。这将使虚拟内存映射难以在支持36个地址位的32位平台上使用。

然而,如果有足够的虚拟空间可用,并且arch已经使用TLB映射其1-1内核空间(例如IA64和x86_64的情况),则该技术使SPARSEEM查找比CONFIG_FLATMEM更高效。FLATMEM需要读取mem_map变量的内容以获得memmap的开头,然后将偏移量添加到所需的条目中。vmemmap是一个常量,我们可以简单地添加偏移量。

此修补程序有可能使SPARSMEM成为大多数系统的默认选项(甚至是唯一选项)。在大多数平台上,它应该是UP、SMP和NUMA上的最佳选择。然后,我们甚至可以移除其他内存型号:FlatMEM、DISCONTIG等。

SPARSEEM内存模型的最后一次扩展是最近的(2016);它是由越来越多的持久存储设备的使用所推动的。为了支持将内存映射直接存储在这些设备上,而不是存储在主内存中,虚拟内存映射可以使用struct vmem_altmap,它将在持久内存中提供页面结构。

早在2005年,SPARSEEM就被描述为“一种新的、更具实验性的 ‘discontiguous memory’ 的替代品”。添加SPARSEMEM_VMEMMAP的提交指出,它“有可能让我们将SPARSEEM作为大多数系统的默认(甚至是唯一)选项”。事实上,一些体系结构已经从DISCONTIGMM转换为SPARSEEM。2008年,SPARSEMEM_VMEMMAP成为x86-64唯一受支持的内存型号,因为它只比FLATMEM稍微贵一点,但比DISCONTIMEM更高效。

最近的内存管理开发,如内存热插拔、持久内存支持和各种性能优化,都是针对SPARSEMEM模型的。但较老的模型仍然存在,这带来了体系结构和内存管理代码中大量的#ifdef块的成本,以及相关配置选项。目前正在进行一项工作,将DISCONTIGMEM的剩余用户完全转换到SPARSEMEM,但对ia64和MIPS64等体系结构进行更改并非易事。

内存区域(section)和节点(node)之间的关系:
每个节点可以包含一个或多个内存区域。内存区域的划分可以根据节点的物理布局和内存分布来进行,以便更好地利用 NUMA 系统中的资源。每个内存区域可以属于一个特定的节点,而一个节点可以包含多个内存区域。

section就是几个page组合而成,比page更大一些的内存区域,但又比node的范围要小。这样整个系统的物理内存就被分成一个个section,并由mem_section结构体表示。而这个结构体中保存了该section范围的struct page结构体的地址。

总结

目前对于 x86_64 和 aarch64 架构内存模型都是采用SPARSEMEM模型。

Linux 内核中的 SPARSEMEM 内存模型是一种用于管理大型物理内存的机制。它旨在克服传统的连续内存模型在处理大量物理内存时遇到的限制。

SPARSEMEM 内存模型的关键思想是将物理内存划分为多个内存区域(sections),每个内存区域都是一段连续的物理内存页。这种划分使得内核能够更有效地管理大型物理内存,而无需在内存映射和管理方面消耗过多的资源。

以下是 SPARSEMEM 内存模型的一些关键特点和机制:
(1)基于区域的内存管理:在Sparse Memory模型中,section是管理内存online/offline的最小内存单元。Sparsemem 内存模型将物理内存划分为多个区域(section)。每个区域是一段连续的物理内存页。每个区域都有一个描述符(mem_section)来跟踪该区域的状态和属性。区域的大小是与架构相关的,通常是 1 GB 或 2 GB 的大小。

(2)内存描述符表:Sparsemem 内存模型维护一个内存描述符表,其中包含每个区域的描述符。描述符存储了对应内存区域的状态信息,例如是否已使用、保留或空闲等。

(3)内存映射:内存描述符表实现了虚拟地址与物理内存区域之间的高效映射关系。当发生针对特定虚拟地址的页面错误时,会通过内存描述符表查找与该地址相关联的物理内存区域。

(4)动态页表分配:Sparsemem 内存模型仅为已使用的物理页动态分配页表项,而不是为整个物理内存范围分配页表项。这种方式显著降低了页表管理的内存开销,尤其适用于具有大量物理内存的系统。

(5)内存分配与释放:当内核或用户进程请求内存时,Sparsemem 内存模型使用内存描述符表来确定适合分配的空闲内存区域。类似地,当内存被释放时,对应的页表项会被释放,并在描述符表中将内存区域标记为空闲状态。

(6)稀疏位图表示:为了高效跟踪内存区域中已分配和空闲的物理页,Sparsemem 内存模型使用了稀疏位图表示。这种表示方法可以在不需要维护完整位图的情况下,高效地存储和检索页面分配信息。

其优点:
(1)动态内存管理:SPARSEMEM 模型支持动态内存管理,意味着内存区域可以根据系统的需求进行动态管理。根据内存的分布和使用情况,内核可以动态创建、合并或删除内存区域,以适应不断变化的内存需求并优化内存利用。

(2)内存块设备:SPARSEMEM 支持内存块设备的概念,它是用于管理特定大小内存块的抽象。内存块设备提供了统一的接口,用于在给定的物理地址范围内分配和释放内存块。

(3)内存策略:SPARSEMEM 模型支持内存策略,用于指定内存分配的规则。内存策略定义了根据内存区域的属性进行选择的规则,例如与特定处理器或 NUMA 节点的接近性。这允许对内存分配决策进行精细控制,以优化性能并减少延迟。

(4)内存热插拔:SPARSEMEM 支持内存热插拔功能,允许在运行中的系统中添加或移除内存而无需重新启动。这个功能在服务器环境中特别有用,可以进行内存扩展或替换。

(5)内存平衡:SPARSEMEM 模型包括用于在不同内存区域和节点之间平衡内存的机制。内存平衡的目标是均衡地分配内存以避免不平衡,并确保最佳利用内存资源。

(6)对多种体系结构的支持:SPARSEMEM 的设计是与体系结构无关的,并且可以在 Linux 内核支持的各种平台和硬件体系结构上使用。它提供了一致的内存管理接口,不受底层硬件配置的影响。

Sparsemem 内存模型为管理大型物理内存提供了一种高效和可扩展的方法。它减少了页表管理的内存开销,同时确保了内存的高效分配和释放。它特别适用于具有大量物理内存的系统,如高端服务器或超级计算机。

参考资料

Linux 5.4.18

https://lwn.net/Articles/789304/
https://www.cnblogs.com/LoyenWang/p/11523678.html
https://zhuanlan.zhihu.com/p/220068494
http://www.wowotech.net/memory_management/memory_model.html
https://zhuanlan.zhihu.com/p/355205941

你可能感兴趣的:(Linux,内存管理,linux,c语言)