Linux Kernel:物理内存模型

本文采用 Linux 内核 v3.10 版本 x86_64 架构

一、概述

操作系统是构建在硬件架构之上的,Linux 自然也不能幸免。目前,主要有两种类型的物理内存架构:UMA(Uniform Memory Access,一致性内存访问)架构和 NUMA (Non-Uniform Memory Access,非一致性内存访问)架构。UMA 将可用内存以连续的方式组织起来,系统中各 CPU 到内存的距离相同,访问时间一致;NUMA 架构将系统中的内存和 CPU 分成不同的组(节点),每个 CPU 访问本节点的内存(称为本地内存,local memory)比访问其它节点的内存(称为非本节点内存 non-local memory 或远端内存 remote memory)速度要快。

在这两种内存架构的基础上,分为三种内存模型,分别是:平坦内存模型非连续内存模型稀疏内存模型。平坦内存模型对应着内核配置选项 FLATMEM,非连续内存模型对应着内核配置选项 DISCONTIGMEM,稀疏内存模型对应着内核配置选项 SPARSEMEM 或者 SPARSEMEM_VMEMMAP。

二、物理内存架构

2.1 UMA 架构

在单处理器时期,架构如下图所示:

随着多处理器时代的来临,架构演变成如下结构:

在这种架构下,所有的 CPU 位于总线的一侧,而所有的内存条组成的整块内存位于总线的另一侧。任何 CPU 想要访问内存都要经过总线,而且距离都是一样的,这种架构称为 SMPSymmetric Multiprocessing,对称多处理器 )架构。在 SMP 架构下,任何处理器访问内存的距离是相同的,所以其访问内存的速度是一致的。这种架构也被成为基于 SMP 的 UMAUniform Memory Access,一致性内存访问)架构。

UMA 架构的特点是简单,但是有一个显著的缺点:由于所有处理器访问内存都要经过总线,当处理器数量很多时,总线就会成为整个系统的瓶颈。

2.2 NUMA 架构

鉴于 UMA 系统的上述缺点,它其实不适合构建大型的计算机系统。为了解决上述问题,提高 CPU 访问内存的性能和扩展性,于是引入了一种新的架构,即非一致性内存访问Non-uniform memory accessNUMA )架构。在 NUMA 架构下,内存和处理器被划分成多个组,称为节点(Node)。每个节点都有自己的内存(称为本地内存),并可包含一个或多个处理器。节点和节点之间通过 QPI(Intel QuickPath Interconnect)完成互联,其架构如下图所示:

注:这里的 Core 指的是物理核,HT(Hyper-Threading,超线程)指的是逻辑核。

在 NUMA 架构下,任意一个 CPU 都可以访问所有节点的内存,访问自己节点的本地内存是最快的,但访问其他节点的内存就会慢很多,这就导致了 CPU 访问内存的速度不一致,所以叫做非一致性内存访问架构。

NUMA 架构严格意义上来讲不属于 SMP 的范畴,但是由于其每个处理器访问内存的模式是一致的,所以在逻辑上属于对称多处理 (SMP) 架构的扩展。

2.3 总结

在 UMA 架构中,所有处理器共享物理内存,每个处理器访问内存的时间是一致的。在 NUMA 模型中,处理器访问本节点的内存速度最快,访问其它节点的内存速度较慢。

三、三种内存模型

Linux 内核是以页为基本单位进行物理内存管理的,通常情况下,每一页对应着 4KB 的物理内存。内核使用数据结构 struct page 来描述物理页,同时,每个物理页都对应着一个编号即页帧号 PFN(Page Frame Number),所以struct page 和 PFN 是一一对应的。页帧号的计算方式非常简单,将物理地址右移物理页对应的位数即可。以 4KB ( <math xmlns="http://www.w3.org/1998/Math/MathML"> 2 12 2^{12} </math>212)大小的页为例,每个页占用 12 位,所以将物理地址右移 12 位即可得到该地址对应的页帧号。在内核中定义了宏 PAGE_SHIFT 来表示 4KB 页的要移位大小,即:

c 复制代码
// file: arch/x86/include/asm/page_types.h
/* PAGE_SHIFT determines the page size */
#define PAGE_SHIFT	12

所以, PFN 就等于物理地址右移 PAGE_SHIFT 位。

由于经常需要将页帧号 PFN 和 struct page 相互转换,内核为此专门定义了两个宏来辅助完成此功能,即 page_to_pfnpfn_to_page

c 复制代码
// file: include/asm-generic/memory_model.h
#define page_to_pfn __page_to_pfn
#define pfn_to_page __pfn_to_page

两者的相互转换受到物理内存模型的影响,不同的内存模型其转换逻辑是不同的。所谓的物理内存模型,就是内核是如何看待、组织物理页布局并进行管理的。不同的内存模型,其 page 结构体的组织方式也是不同的,这就导致了 PFN 和 page 转换逻辑的差异。

3.1 平坦内存模型 FLATMEM

平坦内存模型是最简单的内存模型,此模型适用于具有连续或大部分连续物理内存的非 NUMA 系统(或者单节点的 NUMA 系统)。

在这种模型下,处理器将物理内存看做是一个连续的,没有空洞的地址空间。内核定义了一个全局的 struct page 数组 mem_map ,用于保存所有的 struct page 对象。由于 struct page 对象和 PFN 是一一对应的,所以每个 PFN 对应着 mem_map 中的一个成员。

3.1.1 全局变量 mem_map

mem_map 是一个全局变量,表示 struct page 数组,其声明如下:

c 复制代码
// file: mm/memory.c
#ifndef CONFIG_NEED_MULTIPLE_NODES
......
struct page *mem_map;
......
#endif

mem_mapalloc_node_mem_map 函数中被初始化。在 NUMA 架构下,每个节点由 struct pglist_data 结构体表示,其中 pglist_data->node_mem_map 字段指示该节点对应的 struct page 数组的起始位置。

c 复制代码
// file: include/linux/mmzone.h
typedef struct pglist_data {
......
#ifdef CONFIG_FLAT_NODE_MEM_MAP	/* means !SPARSEMEM */
	struct page *node_mem_map;
......
#endif
} pg_data_t;

由于平坦内存模型相当于只有一个节点,所以 mem_map 对应于节点 0 的 pglist_data->node_mem_map 的值。

c 复制代码
// file: mm/page_alloc.c
static void __init_refok alloc_node_mem_map(struct pglist_data *pgdat)
{
	/* Skip empty nodes */
   /*
    * pgdat->node_spanned_pages 指示节点中的物理页数量(包括内存空洞)
    * 如果该值为 0,说明该节点没有内存,直接跳过
    */
	if (!pgdat->node_spanned_pages)
		return;
/*
 * 配置选项 CONFIG_FLAT_NODE_MEM_MAP 指示将每个节点的内存当做平坦模型来看待
 * 对应着非稀疏内存模型
 * config FLAT_NODE_MEM_MAP
 *	def_bool y
 *	depends on !SPARSEMEM
 */
#ifdef CONFIG_FLAT_NODE_MEM_MAP
	/* ia64 gets its own node_mem_map, before this, without bootmem */
   /*
    * pgdat->node_mem_map 指示节点对应的 struct page 数组的起始地址
    * 如果该值为 NULL,说明还未进行初始化,那么就需要对其进行初始化
    */
	if (!pgdat->node_mem_map) {
		unsigned long size, start, end;
		struct page *map;

		/*
		 * The zone's endpoints aren't required to be MAX_ORDER
		 * aligned but the node_mem_map endpoints must be in order
		 * for the buddy allocator to function correctly.
		 */
           /*
            * MAX_ORDER 指示伙伴系统中的最大分配阶,扩展为 11,
            * 表示伙伴系统支持 2 的 0 次方到 2 的 10 次方共 11 种内存分配大小
            * #define MAX_ORDER 11
            * MAX_ORDER_NR_PAGES 指示最大分配阶下每次分配的物理页数量,扩展为 1 << 10 即 1024 个页,对应 4MB 的内存
            * #define MAX_ORDER_NR_PAGES (1 << (MAX_ORDER - 1))
            * 
            * pgdat->node_start_pfn 表示节点的起始页帧号
            * pgdat_end_pfn(pgdat) 获取节点的结束页帧号(包括内存空洞)
            * 二者都需要向上对齐到 MAX_ORDER,这是伙伴系统的要求
            * size 计算出节点对应的 struct page 数组占用的空间
            * 接下来,从节点内存中分配 size 大小的内存用于保存 struct page 数组,内存的起始地址保存到变量 map 中
            * 最后,根据对齐结果,修正 map 的值,并赋值给 pgdat->node_mem_map,用作 struct page 数组的起始地址
            *
            */
		start = pgdat->node_start_pfn & ~(MAX_ORDER_NR_PAGES - 1);
		end = pgdat_end_pfn(pgdat);
		end = ALIGN(end, MAX_ORDER_NR_PAGES);
		size =  (end - start) * sizeof(struct page);
		map = alloc_remap(pgdat->node_id, size);
		if (!map)
			map = alloc_bootmem_node_nopanic(pgdat, size);
		pgdat->node_mem_map = map + (pgdat->node_start_pfn - start);
	}
    
/*
 * 内核配置选项 CONFIG_NEED_MULTIPLE_NODES 表示是否需要多个节点,默认为 yes
 * 配置了该选项意味着是非连续内存模型(DISCONTIGMEM)或者 NUMA 架构
 ************************************************************
 * config NEED_MULTIPLE_NODES
 *	def_bool y
 *	depends on DISCONTIGMEM || NUMA
 ************************************************************
 * 如果没有设置该选项,说明只有一个节点,对应于平坦内存模型
 */
#ifndef CONFIG_NEED_MULTIPLE_NODES
	/*
	 * With no DISCONTIG, the global mem_map is just set as node 0's
	 */
    /*
     * 对于平坦内存模型,将 mem_map 设置为节点 0 的 node_mem_map 的值
     * 宏 NODE_DATA 用于获取指定节点的 struct pglist_data 实例
     */
	if (pgdat == NODE_DATA(0)) {
		mem_map = NODE_DATA(0)->node_mem_map;
/*
 * 内核配置选项 CONFIG_HAVE_MEMBLOCK_NODE_MAP 指示对于 memblock 内存块是否需要区分不同的节点,
 * memblock 用于启动时内存管理
 * 
 * x86 架构下,该配置项默认为 yes
 * 此时,当 mem_map 对应的页帧号不等于节点的起始页帧号时,还需要进一步调整
 */      
#ifdef CONFIG_HAVE_MEMBLOCK_NODE_MAP
		if (page_to_pfn(mem_map) != pgdat->node_start_pfn)
			mem_map -= (pgdat->node_start_pfn - ARCH_PFN_OFFSET);
#endif /* CONFIG_HAVE_MEMBLOCK_NODE_MAP */
	}
#endif
#endif /* CONFIG_FLAT_NODE_MEM_MAP */
}

alloc_node_mem_map 函数的完整调用流程如下:

start_kernel() -> setup_arch() -> pagetable_init() -> x86_init.paging.pagetable_init() -> native_pagetable_init() -> paging_init() -> zone_sizes_init() -> free_area_init_nodes() -> free_area_init_node() -> alloc_node_mem_map()

3.1.2 pfn 和 page 的相互转换

在平坦内存模型下,pfn 和 struct page 的转换逻辑相对简单,如下所示:

c 复制代码
// file: include/asm-generic/memory_model.h
#define __pfn_to_page(pfn)	(mem_map + ((pfn) - ARCH_PFN_OFFSET))
#define __page_to_pfn(page)	((unsigned long)((page) - mem_map) + \
				 ARCH_PFN_OFFSET)

可以看出,pfn 和 struct page 实际是线性对应的关系,PFN - ARCH_PFN_OFFSET 就是 mem_map 数组的索引 。其中,mem_map 就是上文提到的全局数组,ARCH_PFN_OFFSET 是与处理器架构相关的页帧偏移量,其定义了物理内存起始地址不为 0 的系统的第一个页帧号。对于 x86 架构而言,ARCH_PFN_OFFSET 的值为 0。

为了保证内容的完整性,我们来看下 ARCH_PFN_OFFSET 是如何定义的。

在设置了内核配置选项 CONFIG_FLATMEM 的前提下,如果没有定义 ARCH_PFN_OFFSET,那么就会将 ARCH_PFN_OFFSET 定义为 0。

c 复制代码
// file: include/asm-generic/memory_model.h
#if defined(CONFIG_FLATMEM)

#ifndef ARCH_PFN_OFFSET
#define ARCH_PFN_OFFSET		(0UL)
#endif

实际上,ARCH_PFN_OFFSETinclude/asm-generic/page.h 文件中是有定义的,但其依赖于 PAGE_OFFSETPAGE_SHIFT 的实现。

c 复制代码
// file: include/asm-generic/page.h
#ifndef ARCH_PFN_OFFSET
#define ARCH_PFN_OFFSET		(PAGE_OFFSET >> PAGE_SHIFT)
#endif

正如我们上文所说的,在 4KB 页的情况下,宏 PAGE_SHIFT 扩展为 12;而宏 PAGE_OFFSET 依赖于内核配置选项 CONFIG_KERNEL_RAM_BASE_ADDRESS。由于 x86 架构不会配置该选项,所以 ARCH_PFN_OFFSET 最终扩展为 0。

c 复制代码
// file: include/asm-generic/page.h
#ifdef CONFIG_KERNEL_RAM_BASE_ADDRESS
#define PAGE_OFFSET		(CONFIG_KERNEL_RAM_BASE_ADDRESS)
#else
#define PAGE_OFFSET		(0)
#endif

另外,在 x86 架构下,alloc_remap 函数是一个空函数,所以 mem_map 数组内存的分配实际上是在 alloc_bootmem_node_nopanic 函数中进行的。

c 复制代码
// file: include/linux/bootmem.h
static inline void *alloc_remap(int nid, unsigned long size)
{
	return NULL;
}

alloc_bootmem_node_nopanic 函数的调用链中,最终会调用到 __alloc_memory_core_early 函数,该函通过 memblock_find_in_range_node 函数分配到物理内存后,使用宏 phys_to_virt 将物理内存地址转换成虚拟内存地址。

c 复制代码
// file: mm/nobootmem.c
static void * __init __alloc_memory_core_early(int nid, u64 size, u64 align,
					u64 goal, u64 limit)
{
......
	addr = memblock_find_in_range_node(goal, limit, size, align, nid);
......
	ptr = phys_to_virt(addr);
......
	return ptr;
}

phys_to_virt 函数实际等同于 __va 宏。

c 复制代码
// file: arch/x86/include/asm/io.h
static inline void *phys_to_virt(phys_addr_t address)
{
	return __va(address);
}

__va(x)__pa(x) 这两个宏我们在以前的文章中也多次提到过,其功能就是将物理内存地址和直接映射区的虚拟地址进行转换。换句话说,mem_map 数组位于虚拟地址的直接映射区

3.1.3 示意图

平坦内存模型下,struct page 与 物理页对应关系示意图

3.2 非连续内存模型 DISCONTIGMEM

在平坦内存模型下,处理器将物理内存看做一段连续的地址空间。但是,物理内存可能会存在空洞。特别是在 NUMA 架构下,各个节点的物理内存地址不再连续,这样在节点和节点之间就会出现较大的内存空洞。对于大多数架构,内存空洞在 mem_map 数组中都有对应的 struct page 对象。也就是说,有些 page 对象实际映射到的是内存空洞。而映射到内存空洞的 struct page 对象永远不会完全初始化,也无法使用,所以这些 struct page 对象所占用的空间就被白白浪费掉了。

为了解决这个问题,引入了非连续内存模型。在非连续内存模型下,系统将每个节点的内存看做是一段单独的地址连续的平坦内存,然后在每个节点对应的pglist_data 结构体实例的 node_mem_map 字段中,保存着该节点对应的 struct page 数组的基地址。这样,就将一段不连续的内存空间分割成了多个连续的内存区间,每段区间都对应着平坦内存模型。

在非连续内存模型下,PFN 和 struct page 之间的转换逻辑如下所示:

c 复制代码
// file: include/asm-generic/memory_model.h
#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;					\
})

每个节点的struct page 数组的起始地址由 pglist_data->node_mem_map 表示(相当于平坦内存模型的 mem_map ),PFN 到节点起始页帧号( pglist_data->node_start_pfn )的偏移量对应着数组pglist_data->node_mem_map 的索引值,两者相加就能得到 PFN 对应的 struct page 的地址。

在此之前,先要获取到 PFN 对应的节点 id(通过 arch_pfn_to_nid),然后通过宏 NODE_DATA 获取到节点 id 对应的 struct pglist_data 实例及其 node_mem_map 字段的值。宏 arch_local_page_offset 计算指定 PFN 相对于节点起始页帧的偏移量,即数组的索引。

c 复制代码
// file: include/asm-generic/memory_model.h
#ifndef arch_pfn_to_nid
#define arch_pfn_to_nid(pfn)	pfn_to_nid(pfn)
#endif

#ifndef arch_local_page_offset
#define arch_local_page_offset(pfn, nid)	\
	((pfn) - NODE_DATA(nid)->node_start_pfn)
#endif

注意:非连续内存模型已经废弃不用了 。非连续内存模型可以看做稀疏内存模型的一种特例,而且经测试其负载比稀疏内存模型还要高,所以在 x86-64 架构下该内存模型已经被稀疏内存模型所替代。详情请参考:x86: 64-bit, make sparsemem vmemmap the only memory model

在 v5.14 之后的内核版本中, CONFIG_DISCONTIGMEM 相关的代码已经被移除了。

3.3 稀疏内存模型 SPARSEMEM

随着内存热插拔技术的出现,不止节点间内存地址不连续,单个节点内的内存地址不连续也成了常态。这时候,再用非连续内存模型就不合适了,于是又引入了稀疏内存模型 SPARSEMEM。稀疏内存模型是 Linux 中最通用的内存模型,其实不管是平坦内存模型还是非连续内存模型,都可以看做是稀疏内存模型的一种特殊状态。

稀疏内存模型使用 section 来管理 struct page 数组,section 替代了非连续内存模型中的节点的角色。由于每个 section 管理的 struct page 数量比节点要少的多,所以管理的粒度更细,更适合大块空洞很多的场景。

section 由 struct mem_section 表示,其中的 section_mem_map 字段在逻辑上是指向 struct page 数组的指针。

c 复制代码
// file: include/linux/mmzone.h
struct mem_section {
	unsigned long section_mem_map;
......
};

类比非连续内存模型,稀疏内存模型示意如下:

是不是跟非连续内存模型非常相似?当然,上图只是一个简单的示意,并没有展示出 section 的组织方式。另外,在稀疏内存模型中, struct page 在虚拟地址中的布局也分为两种,上图也只展示了一种。

稀疏内存模型是 Linux 内核默认的内存模型,也是本文的重点。接下来,我们从数据结构和基础概念开始介绍。

四、数据结构及基本概念

4.1 物理地址层级

支持 SPARSEMEM 内存模型的体系结构会定义 SECTION_SIZE_BITS 和 MAX_PHYSMEM_BITS 常量。其中 MAX_PHYSMEM_BITS 指示系统支持的最大物理地址位数,SECTION_SIZE_BITS 指示物理地址中 section 所占用的位数。在 x86-64 架构下,这两个宏定义如下:

c 复制代码
// file: arch/x86/include/asm/sparsemem.h
# define SECTION_SIZE_BITS	27 /* matt - 128 is convenient right now */
# define MAX_PHYSMEM_BITS	46

再加上宏 PAGE_SHIFT,这三个常量构成了物理地址的层级结构:

c 复制代码
// file: arch/x86/include/asm/page_types.h
/* PAGE_SHIFT determines the page size */
#define PAGE_SHIFT	12

从上图可知,每个 section 最多包含 <math xmlns="http://www.w3.org/1998/Math/MathML"> 2 ( S E C T I O N _ S I Z E _ B I T S − P A G E _ S H I F T ) 2^{(SECTION\_SIZE\_BITS-PAGE\_SHIFT)} </math>2(SECTION_SIZE_BITS−PAGE_SHIFT) 个页;而系统中 section 的最大数量表示为 NR_MEM_SECTIONS,定义如下:

c 复制代码
// file: include/linux/mmzone.h
#define NR_MEM_SECTIONS		(1UL << SECTIONS_SHIFT)

其中,SECTIONS_SHIFT 表示在物理地址中 section 索引占用的位数。

c 复制代码
// file: include/linux/page-flags-layout.h
/* SECTION_SHIFT	#bits space required to store a section # */
#define SECTIONS_SHIFT	(MAX_PHYSMEM_BITS - SECTION_SIZE_BITS)

最终,NR_MEM_SECTIONS 计算为:

对于 x86-64 架构,SECTION_SIZE_BITS 扩展为 27,MAX_PHYSMEM_BITS 扩展为 46;所以,NR_MEM_SECTIONS 扩展为 <math xmlns="http://www.w3.org/1998/Math/MathML"> 2 19 2^{19} </math>219​。

我们把上图更新一下,加上 SECTIONS_SHIFT,这样看起来更直观 :

另外,由于页帧号(page frame number, PFN)是将物理地址右移 PAGE_SHIFT 位得到的,所以 PFN 包含两部分:section 位移section 索引

为了方便进行 section number 与 PFN 以及物理地址之间的转换,内核定义了如下宏:

c 复制代码
// file: include/linux/mmzone.h
/*
 * SECTION_SHIFT    		#bits space required to store a section #
 *
 * PA_SECTION_SHIFT		physical address to/from section number
 * PFN_SECTION_SHIFT		pfn to/from section number
 */
#define PA_SECTION_SHIFT	(SECTION_SIZE_BITS)
#define PFN_SECTION_SHIFT	(SECTION_SIZE_BITS - PAGE_SHIFT)

从名字也能够看出来,PA_SECTION_SHIFT 表示物理地址中 section number 的移位数;PFN_SECTION_SHIFT 表示 PFN 中 section number 的移位数。我们再更新下示意图,把这两个宏加上:

现在,进行 PFN 和 section 之间的转换就很容易了:

c 复制代码
// file: include/linux/mmzone.h
#define pfn_to_section_nr(pfn) ((pfn) >> PFN_SECTION_SHIFT)
#define section_nr_to_pfn(sec) ((sec) << PFN_SECTION_SHIFT)

另外,通过简单的移位操作就能够计算出每个 section 包含的 page 数量,即 PAGES_PER_SECTION:

c 复制代码
#define PAGES_PER_SECTION       (1UL << PFN_SECTION_SHIFT)
#define PAGE_SECTION_MASK	(~(PAGES_PER_SECTION-1))

接下来,内核定义了掩码 PAGE_SECTION_MASK,通过该掩码就能屏蔽掉 PFN 中的低位,使其对齐到 section;或者屏蔽掉物理地址中的低位,使其对齐到 section,比如 memory_present 函数中的

c 复制代码
/* 屏蔽 PFN 中的低位,使其对齐到 section */
start &= PAGE_SECTION_MASK

或者 sparse_early_usemaps_alloc_pgdat_section 函数中的

c 复制代码
/* 屏蔽物理地址中的低位,使其对齐到 section */
goal = __pa(pgdat) & (PAGE_SECTION_MASK << PAGE_SHIFT)

4.2 page->flags 的布局

根据内存模型的不同以及 page->flags 自身空间大小的限制,page->flags 共有五种可能的布局,我们先来看下内核的说明:

c 复制代码
// file: include/linux/page-flags-layout.h
/*
 * page->flags layout:
 *
 * There are five possibilities for how page->flags get laid out.  The first
 * pair is for the normal case without sparsemem. The second pair is for
 * sparsemem when there is plenty of space for node and section information.
 * The last is when there is insufficient space in page->flags and a separate
 * lookup is necessary.
 *
 * No sparsemem or sparsemem vmemmap: |       NODE     | ZONE |          ... | FLAGS |
 *         " plus space for last_nid: |       NODE     | ZONE | LAST_NID ... | FLAGS |
 * classic sparse with space for node:| SECTION | NODE | ZONE |          ... | FLAGS |
 *         " plus space for last_nid: | SECTION | NODE | ZONE | LAST_NID ... | FLAGS |
 * classic sparse no space for node:  | SECTION |     ZONE    | ... | FLAGS |
 */

可以看到,page->flags 最多由五部分组成:section id、node id、zone id、last node id 以及页标志 flags。这五部分中, last node id 与 NUMA Balancing 有关,我们暂不介绍; 其余各部分的含义都很明确,无需多说。

在五种布局中,前两种适用于"非稀疏 "模型或者 "稀疏 vmemmap "模型,后三种适用于"经典稀疏(classic sparse) "内存模型。理论上,page->flags 只有两种布局就够了,一种适用于"非稀疏 "模型或者 "vmemmap 稀疏 "模型;另一种适用于"经典稀疏 "内存模型。但是,由于不同架构中 page->flags 的位数是不同的,而各组成部分所需的空间也不固定,这样就可能出现 page->flags 空间不足导致无法容纳所有特征的情况。这种情况下,有的特征就无法放置到 page->flags,从而衍生出了五种不同的布局。

另外,上文中也提到过,稀疏内存模型又分为两种:"经典稀疏 "和"稀疏 vmemmap "。它们之间的区别我们在下文讲到 "pfn 和 page 的相互转换"时再跟大家详细剖析。

完整的page->flags 布局如下图所示:

内核为每个组成部分的位宽都定位了单独的宏,如下图所示:
提示:page->flags中不存在的部分,其位宽为 0。

其中,宏 NR_PAGEFLAGS 表示页标志的数量,每个标志对应一个比特位,该宏定义如下:

c 复制代码
// file: include/generated/bounds.h
#define NR_PAGEFLAGS 25 /* __NR_PAGEFLAGS	# */

ZONE 的位宽由 MAX_NR_ZONES (扩展为 4)决定,由于系统中 ZONE 最多有 4 种,只需要两个比特位就能够区分,所以 ZONE 的位宽 ZONES_WIDTH 为 2:

c 复制代码
// file: include/generated/bounds.h
#define MAX_NR_ZONES 4 /* __MAX_NR_ZONES	# */
c 复制代码
// file: include/linux/page-flags-layout.h
#define ZONES_WIDTH		ZONES_SHIFT

#if MAX_NR_ZONES < 2
#define ZONES_SHIFT 0
#elif MAX_NR_ZONES <= 2
#define ZONES_SHIFT 1
#elif MAX_NR_ZONES <= 4
#define ZONES_SHIFT 2     // MAX_NR_ZONES 为 4,所以 ZONES_SHIFT 为 2
#else
#error ZONES_SHIFT -- too many zones configured adjust calculation
#endif

SECTION 的位宽 SECTIONS_WIDTH 定义如下:

c 复制代码
// file: include/linux/page-flags-layout.h
#if defined(CONFIG_SPARSEMEM) && !defined(CONFIG_SPARSEMEM_VMEMMAP)
#define SECTIONS_WIDTH		SECTIONS_SHIFT
#else
#define SECTIONS_WIDTH		0
#endif

CONFIG_SPARSEMEM 表明系统支持稀疏内存模型 ,CONFIG_SPARSEMEM_VMEMMAP 表明系统支持"稀疏 vmemmap "内存模型。启用 CONFIG_SPARSEMEM 而禁用 CONFIG_SPARSEMEM_VMEMMAP ,表示系统使用的是"经典稀疏 "模型。可以看到,只有在"**经典稀疏"**模型下,SECTIONS_WIDTH 才有意义,此时SECTIONS_WIDTH 等同于 SECTIONS_SHIFT(上文介绍过,扩展为 19);否则,SECTIONS_WIDTH 扩展为 0,表明 section id 并未放置到 page->flags 中。

另外,在"经典稀疏"模型下,还会定义宏 SECTION_IN_PAGE_FLAGS:

c 复制代码
// file: include/linux/mm.h
#if defined(CONFIG_SPARSEMEM) && !defined(CONFIG_SPARSEMEM_VMEMMAP)
#define SECTION_IN_PAGE_FLAGS
#endif

该宏用于指示 page->flags 中是否包含 section id。

在 x86-64 系统下,page->flags 一共 64 位,现在已经分配出去了 25+2+19 = 46 位,还剩 18 个比特位。这 18 位会优先放置 node id,其位宽定义为 NODES_WIDTH;如果还有空间,则会放置 LAST_NID,其位宽定义为 LAST_NID_WIDTH。page->flags 空间充足的情况下,NODES_WIDTH 和 LAST_NID_WIDTH 都等于 NODES_SHIFT(指示节点 id 占用的比特位数量),该值由配置参数 CONFIG_NODES_SHIFT 决定;否则,NODES_WIDTH 或 LAST_NID_WIDTH 设置为 0 。

c 复制代码
// file: include/linux/page-flags-layout.h
#if SECTIONS_WIDTH+ZONES_WIDTH+NODES_SHIFT <= BITS_PER_LONG - NR_PAGEFLAGS
#define NODES_WIDTH		NODES_SHIFT
#else
#ifdef CONFIG_SPARSEMEM_VMEMMAP
#error "Vmemmap: No space for nodes field in page flags"
#endif
#define NODES_WIDTH		0
#endif

#if SECTIONS_WIDTH+ZONES_WIDTH+NODES_SHIFT+LAST_NID_SHIFT <= BITS_PER_LONG - NR_PAGEFLAGS
#define LAST_NID_WIDTH LAST_NID_SHIFT
#else
#define LAST_NID_WIDTH 0
#endif

如果 page->flags 中无法容纳 node id,那么会定义宏 NODE_NOT_IN_PAGE_FLAGS。同理,如果 page->flags 中无法容 last node id,那么会定义宏 LAST_NID_NOT_IN_PAGE_FLAGS:

c 复制代码
// file: include/linux/page-flags-layout.h
/*
 * We are going to use the flags for the page to node mapping if its in
 * there.  This includes the case where there is no node, so it is implicit.
 */
#if !(NODES_WIDTH > 0 || NODES_SHIFT == 0)
#define NODE_NOT_IN_PAGE_FLAGS
#endif

#if defined(CONFIG_NUMA_BALANCING) && LAST_NID_WIDTH == 0
#define LAST_NID_NOT_IN_PAGE_FLAGS
#endif

有了各部分的位宽,就能计算出各部分的通用位偏移:

c 复制代码
// file: include/linux/mm.h
/* Page flags: | [SECTION] | [NODE] | ZONE | [LAST_NID] | ... | FLAGS | */
#define SECTIONS_PGOFF		((sizeof(unsigned long)*8) - SECTIONS_WIDTH)
#define NODES_PGOFF		(SECTIONS_PGOFF - NODES_WIDTH)
#define ZONES_PGOFF		(NODES_PGOFF - ZONES_WIDTH)
#define LAST_NID_PGOFF		(ZONES_PGOFF - LAST_NID_WIDTH)

考虑到有的部分并不存在于 page->flags 中,需要对位偏移进行修正。修正的原则:如果某部分不存在,则位偏移为 0。修正后各部分的位偏移如下:

c 复制代码
// file: include/linux/mm.h
/*
 * Define the bit shifts to access each section.  For non-existent
 * sections we define the shift as 0; that plus a 0 mask ensures
 * the compiler will optimise away reference to them.
 */
#define SECTIONS_PGSHIFT	(SECTIONS_PGOFF * (SECTIONS_WIDTH != 0))
#define NODES_PGSHIFT		(NODES_PGOFF * (NODES_WIDTH != 0))
#define ZONES_PGSHIFT		(ZONES_PGOFF * (ZONES_WIDTH != 0))
#define LAST_NID_PGSHIFT	(LAST_NID_PGOFF * (LAST_NID_WIDTH != 0))

通过位宽,就能计算出各组成部分的掩码,掩码的作用是将各部分的比特位设置为1:

c 复制代码
// file: include/linux/mm.h
#define ZONES_MASK		((1UL << ZONES_WIDTH) - 1)
#define NODES_MASK		((1UL << NODES_WIDTH) - 1)
#define SECTIONS_MASK		((1UL << SECTIONS_WIDTH) - 1)
#define LAST_NID_MASK		((1UL << LAST_NID_WIDTH) - 1)

通过掩码和位偏移,就能方便的获取到各组成部分的值。比如通过如下操作,就能获取到 page->flags 中 seciton id 的值:

c 复制代码
// file: include/linux/mm.h
static inline unsigned long page_to_section(const struct page *page)
{
	return (page->flags >> SECTIONS_PGSHIFT) & SECTIONS_MASK;
}

4.3 struct mem_section -- section 的数据描述

内核使用数据结构 struct mem_section 来描述 section:

c 复制代码
// file: include/linux/mmzone.h
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;

	/* See declaration of similar field in struct zone */
	unsigned long *pageblock_flags;
......
};

section_mem_map 逻辑上指向 struct page 数组。为什么说是逻辑上呢,主要有两个方面的原因:

  • section_mem_map 中存储了一些有助于 section 管理的其它标志。
  • section_mem_map中保存的是经过编码后的 struct page 数组地址,要解码后才能使用,不能直接使用。

我们来先看下 section_mem_map 中包含了哪些标志位:

c 复制代码
// file:include/linux/mmzone.h
/*
 * We use the lower bits of the mem_map pointer to store
 * a little bit of information.  There should be at least
 * 3 bits here due to 32-bit alignment.
 */
#define	SECTION_MARKED_PRESENT	(1UL<<0)
#define SECTION_HAS_MEM_MAP	(1UL<<1)
#define SECTION_MAP_LAST_BIT	(1UL<<2)
#define SECTION_MAP_MASK	(~(SECTION_MAP_LAST_BIT-1))
#define SECTION_NID_SHIFT	2

从代码中可以看出,section_mem_map 中的低 2 位(位 0 - 位 1)有特殊用途:

  • 位 0(SECTION_MARKED_PRESENT),指示 section 是否存在;
  • 位 1(SECTION_HAS_MEM_MAP),指示 section 是否已经与 struct page 建立了映射关系或者说 section 是否有效;

而宏 SECTION_MAP_LAST_BIT 仅用于计算位掩码 SECTION_MAP_MASK。宏 SECTION_MAP_MASK 用作这些特殊位的掩码,其低 2 位为 0,可用于屏蔽这些特殊位,其示意图如下:

我们来看下 SECTION_MARKED_PRESENT 和 SECTION_HAS_MEM_MAP 的使用:

c 复制代码
// file: include/linux/mmzone.h
static inline int present_section(struct mem_section *section)
{
	return (section && (section->section_mem_map & SECTION_MARKED_PRESENT));
}

static inline int valid_section(struct mem_section *section)
{
	return (section && (section->section_mem_map & SECTION_HAS_MEM_MAP));
}

内联函数 present_sectionvalid_section 分别用来判断 section 是否存在以及是否有效,它们分别用到了 SECTION_MARKED_PRESENT 和 SECTION_HAS_MEM_MAP。如果标志位有效,则返回正确的结构体指针;否则,返回空指针 NULL。

另外,还有一个宏 SECTION_NID_SHIFT,我们一起介绍下。在系统初始化早期, section_mem_map 字段还未用作映射地址之前,该字段保存的是 section 所在节点的 id,但是其低 2 位保留用作上文所述的特殊标志,所以定义了一个位偏移量 SECTION_NID_SHIFT。在 memory_present 函数中,可以看到该用法:

c 复制代码
// file: mm/sparse.c
/* Record a memory area against a node. */
void __init memory_present(int nid, unsigned long start, unsigned long end)
{
......
	for (pfn = start; pfn < end; pfn += PAGES_PER_SECTION) {
......
		if (!ms->section_mem_map)
			ms->section_mem_map = sparse_encode_early_nid(nid) |
							SECTION_MARKED_PRESENT;
	}
}

sparse_encode_early_nid 函数用于对 node id 进行编码,即左移 SECTION_NID_SHIFT 位,将低位空出来:

c 复制代码
// file: mm/sparse.c
/*
 * During early boot, before section_mem_map is used for an actual
 * mem_map, we use section_mem_map to store the section's NUMA
 * node.  This keeps us from having to use another data structure.  The
 * node information is cleared just before we store the real mem_map.
 */
static inline unsigned long sparse_encode_early_nid(int nid)
{
	return (nid << SECTION_NID_SHIFT);
}

介绍完了 section_mem_map 中的特殊标志位,我们再来看下 section_mem_map 的编解码。在 sparse_init_one_section 函数中,会对 section_mem_map 字段进行初始化:

c 复制代码
// file: mm/sparse.c
static int __meminit sparse_init_one_section(struct mem_section *ms,
		unsigned long pnum, struct page *mem_map,
		unsigned long *pageblock_bitmap)
{
......
	ms->section_mem_map &= ~SECTION_MAP_MASK;
	ms->section_mem_map |= sparse_encode_mem_map(mem_map, pnum) |
							SECTION_HAS_MEM_MAP;
......
}

注意:此处保存的是 struct page 数组地址,会覆盖掉 memory_prensent 函数中保存的节点 id。

sparse_init_one_section 接收 4 个参数,其中 ms 指向要进行初始化的 section,pnum 是 section id,mem_map 指向 struct page 数组的起始地址。

上文介绍过,SECTION_MAP_MASK 的低 2 位为 0,高位为 1,取反后低 2 位为 1 ,高位为 0。第一步的按位与操作,会保留 section_mem_map 中的特殊标志位,将高位置零。实际上此时 section_mem_map 中,只设置了 SECTION_MARKED_PRESENT 标志(见 memory_present 函数)。第二步,将编码后的 mem_map 连同特殊标志位 SECTION_HAS_MEM_MAP 一起,保存到 section_mem_map 中。

c 复制代码
// file: mm/sparse.c
/*
 * Subtle, we encode the real pfn into the mem_map such that
 * the identity pfn - section_mem_map will return the actual
 * physical page frame number.
 */
static unsigned long sparse_encode_mem_map(struct page *mem_map, unsigned long pnum)
{
	return (unsigned long)(mem_map - (section_nr_to_pfn(pnum)));
}

section_nr_to_pfn 将 section number 转换成 pfn,转换原理请参考 "4.1 物理地址层级 " 小节。通过编码,将绝对地址 mem_map 转换成了基于 section 的相对地址。

4.4 struct mem_section 对象的存储

系统所有的struct mem_section 对象保存在一个名为 mem_section 的二维数组中,数组内存的分配方式取决于配置项 CONFIG_SPARSEMEM_EXTREME:

  • 当 CONFIG_SPARSEMEM_EXTREME 禁用时,mem_section 数组是静态分配的。数组有 NR_SECTION_ROOTS 行,每行包含 SECTIONS_PER_ROOT 个 struct mem_section 对象。
  • 当启用 CONFIG_SPARSEMEM_EXTREME 时, mem_section 数组初始时只包含 NR_SECTION_ROOTS 个 struct mem_section 指针,它们所指向的 struct mem_section 数组需要动态分配。
c 复制代码
// file: mm/sparse.c
/*
 * Permanent SPARSEMEM data:
 *
 * 1) mem_section	- memory sections, mem_map's for valid memory
 */
#ifdef CONFIG_SPARSEMEM_EXTREME
struct mem_section *mem_section[NR_SECTION_ROOTS]
	____cacheline_internodealigned_in_smp;
#else
struct mem_section mem_section[NR_SECTION_ROOTS][SECTIONS_PER_ROOT]
	____cacheline_internodealigned_in_smp;
#endif

配置项 CONFIG_SPARSEMEM_EXTREME 指示内存是否非常稀疏。对于 x86 架构来说,该项默认是启用的。

接下来,我们看下 NR_SECTION_ROOTS 和 SECTIONS_PER_ROOT 的计算。

SECTIONS_PER_ROOT 表示二维数组 mem_section 中每行容纳的 struct mem_section 对象数量,其计算过程如下:

  • 当启用 CONFIG_SPARSEMEM_EXTREME 时,SECTIONS_PER_ROOT 表示一个页面能够容纳的 struct mem_section 对象数量;
  • 在禁用 CONFIG_SPARSEMEM_EXTREME 时,SECTIONS_PER_ROOT 为 1。
c 复制代码
// file: include/linux/mmzone.h
#ifdef CONFIG_SPARSEMEM_EXTREME
#define SECTIONS_PER_ROOT       (PAGE_SIZE / sizeof (struct mem_section))
#else
#define SECTIONS_PER_ROOT	1
#endif

知道了 section 的总数量 NR_MEM_SECTIONS 以及每行的数量 SECTIONS_PER_ROOT,就能够计算出二维数组的行数:

c 复制代码
// file: include/linux/mmzone.h
#define NR_SECTION_ROOTS	DIV_ROUND_UP(NR_MEM_SECTIONS, SECTIONS_PER_ROOT)

可以看到,求值过程实际上就是用总数除以每行的数量并向上圆整,具体计算通过宏 DIV_ROUND_UP 执行。

宏 DIV_ROUND_UP 是内核抽象出来的通用算法,其定义如下:

c 复制代码
// file: include/linux/kernel.h
#define DIV_ROUND_UP(n,d) (((n) + (d) - 1) / (d))

对于静态分配的二维数组,内存空洞也会占用空间,这就会导致内存空间的浪费。如果内存非常稀疏,那么浪费的内存还是比较大的。

对于动态分配的数组,由于数组成员是指针,可以将对应于内存空洞的数组成员设置为 NULL,以节省内存空间。对于非常稀疏的情况(启用CONFIG_SPARSEMEM_EXTREME),使用动态分配的数组可以有效的节省内存空间。

由于 CONFIG_SPARSEMEM_EXTREME 默认是启用的,我们只关注动态分配的情况。

4.5 pfn 和 page 的相互转换

使用稀疏内存模型 SPARSEMEM 有两种方法可以将 PFN 转换为相应的 page: "经典稀疏(classic sparse) "和"稀疏 vmemmap (sparse vmemmap) "。具体使用哪一种,是在构建时通过 CONFIG_SPARSEMEM_VMEMMAP 来决定的。如果启用了 CONFIG_SPARSEMEM_VMEMMAP,就使用 "稀疏 vmemmap (sparse vmemmap) "的方式进行转换;否则,使用 "经典稀疏(classic sparse)"方式进行转换。

4.5.1 经典稀疏(classic sparse)

如 4.1 及 4.2 小节所述,"经典稀疏"将页面所属的 section number 编码在 page->flags 中,并使用 PFN 的高位来索引该页帧的 section。而在一个 section 内,PFN 就是 struct page 数组的索引。在"经典稀疏"方法下,struct page 数组是分散在虚拟内存的不同位置,其 pfn_to_pagepage_to_pfn 操作定义如下:

c 复制代码
// file: include/asm-generic/memory_model.h
/*
 * Note: section's mem_map is encorded 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;		\
})

在进行转换时,使用 section 作为中介,转换过程类似于 pfn <--> section <--> page。

4.5.1.1 page_to_pfn

page_to_pfn 的基本原理如下:

  • page->flags 中获取到 page 对应的 section number(通过 page_to_section 函数)
  • 根据 section number 获取对应的 struct mem_section 结构体(通过 __nr_to_section 函数)
  • 通过 __section_mem_map_addr 函数获取 struct page 数组的基地址,即 mem_section->section_mem_map 。注意,这是一个相对地址 ,是将 struct page 数组的绝对地址减去 section 对应的 pfn 的到的(回想下"4.3 小节 "提到的 sparse_encode_mem_map 函数)。
  • 最后,用当前的 struct page 指针减去 mem_section->section_mem_map(相当与用 section 的 pfn 加上页偏移),就得到当前 page 对应的页帧号。
4.5.1.2 pfn_to_page

pfn_to_page 的基本原理如下:

  • 获取到页帧号 pfn 对应的 struct mem_section 结构体指针(通过 __pfn_to_section 函数)。我们在 "4.1 物理地址层级 "小节中介绍过,pfn 由section 偏移和 section 编号组成,pfn 右移 PFN_SECTION_SHIFT 位就可以得到 section number。而通过 __nr_to_section 函数,可以获取 section number 对应的 struct mem_section 结构体;
  • 通过 __section_mem_map_addr 函数获取 struct page 数组的基地址,即 mem_section->section_mem_map ,这是一个相对地址。
  • 将上一步获取到的基地址,加上当前页帧号,就得到了对应的 struct page 地址。

这里可以看到,将 struct page 减去 section 的起始 pfn 后的差值编码到 section_mem_map 中确实使转换更加的方便。

接下来,我们介绍下转换过程中用到的函数。

4.5.1.3 page_to_section、set_page_section

page_to_section 函数用于获取 struct page 对应的 section number,该函数的实现依赖宏 SECTION_IN_PAGE_FLAGS。我们在上文中介绍过,当开启了 CONFIG_SPARSEMEM 但并未开启 CONFIG_SPARSEMEM_VMEMMAP 的情况下,会定义宏 SECTION_IN_PAGE_FLAGS,此时使用的是 "经典稀疏" 模型。

c 复制代码
// file: include/linux/mm.h
#if defined(CONFIG_SPARSEMEM) && !defined(CONFIG_SPARSEMEM_VMEMMAP)
#define SECTION_IN_PAGE_FLAGS
#endif

当定义了宏 SECTION_IN_PAGE_FLAGS 时,page_to_section 以及 set_page_section 函数的实现如下:

c 复制代码
// file: include/linux/mm.h
#ifdef SECTION_IN_PAGE_FLAGS
static inline void set_page_section(struct page *page, unsigned long section)
{
	page->flags &= ~(SECTIONS_MASK << SECTIONS_PGSHIFT);
	page->flags |= (section & SECTIONS_MASK) << SECTIONS_PGSHIFT;
}

static inline unsigned long page_to_section(const struct page *page)
{
	return (page->flags >> SECTIONS_PGSHIFT) & SECTIONS_MASK;
}
#endif

函数 set_page_sectionpage_to_section 分别用来保存和取出 section number。这两个函数内部,用到了上文介绍的掩码和位偏移。为了看的更直观,我们把上文的图再拿过来用一下:

c 复制代码
// file: include/linux/mm.h
#define SECTIONS_MASK		((1UL << SECTIONS_WIDTH) - 1)

可以看到,set_page_section 函数先将 page->flags 中的 section 部分清零,然后将 section 编号移动到对应的位置。

page_to_section 函数直接截取 page->flags 中 section 部分的值并返回。

4.5.1.4 __section_mem_map_addr

__section_mem_map_addr 函数用于获取 section 对应的 struct page 数组的地址,即 section->section_mem_map。由于 section_mem_map 的低位保存着特殊标志,所以需要使用 SECTION_MAP_MASK 掩码来清除这些特殊标志位。

c 复制代码
// file: include/linux/mmzone.h
static inline struct page *__section_mem_map_addr(struct mem_section *section)
{
	unsigned long map = section->section_mem_map;
	map &= SECTION_MAP_MASK;
	return (struct page *)map;
}
4.5.1.5 __nr_to_section

__nr_to_section 函数用于获取 section number 对应的 struct mem_section 结构体指针:

c 复制代码
// file: include/linux/mmzone.h
static inline struct mem_section *__nr_to_section(unsigned long nr)
{
	if (!mem_section[SECTION_NR_TO_ROOT(nr)])
		return NULL;
	return &mem_section[SECTION_NR_TO_ROOT(nr)][nr & SECTION_ROOT_MASK];
}

由于 mem_section 是一个二维数组,要定位一个数组成员需要知道该成员所在的行和列。宏 SECTION_NR_TO_ROOT 用于计算 section 在二维数组中的行号;nr & SECTION_ROOT_MASK 计算 section 所在的列。

c 复制代码
// file: include/linux/mmzone.h
#define SECTION_NR_TO_ROOT(sec)	((sec) / SECTIONS_PER_ROOT)
#define SECTION_ROOT_MASK	(SECTIONS_PER_ROOT - 1)

其中,SECTIONS_PER_ROOT 表示每一行的成员数量,我们在上文已经介绍过了:

c 复制代码
#ifdef CONFIG_SPARSEMEM_EXTREME
#define SECTIONS_PER_ROOT       (PAGE_SIZE / sizeof (struct mem_section))
#else
#define SECTIONS_PER_ROOT	1
#endif
4.5.1.6 __pfn_to_section

__pfn_to_section 函数计算页帧号 pfn 对应的 struct mem_section 指针,其内部用到了宏 pfn_to_section_nr 以及上文介绍的 __nr_to_section 函数。

c 复制代码
// file: include/linux/mmzone.h
static inline struct mem_section *__pfn_to_section(unsigned long pfn)
{
	return __nr_to_section(pfn_to_section_nr(pfn));
}

我们在"4.1 物理地址层级"小节介绍了物理地址及 pfn 的组成:

可以看到,pfn 和 section 之间的转换非常简单,只需要进行移位操作就可以了:

c 复制代码
// file: include/linux/mmzone.h
#define pfn_to_section_nr(pfn) ((pfn) >> PFN_SECTION_SHIFT)
#define section_nr_to_pfn(sec) ((sec) << PFN_SECTION_SHIFT)
4.5.1.7 示意图

"经典稀疏"内存模型结构如下图所示:

在经典"经典稀疏"模型下,struct page 数组的虚拟地址位于直接映射区,所以不需要再为它们单独创建页表了。

4.5.2 稀疏 vmemmap(sparse vmemmap)

4.5.2.1 "经典稀疏"的问题

"经典稀疏"内存模型可以正常工作,但存在以下问题:

  • section id 要保存在 page->flags 中,但由于 page->flags 空间有限,有可能会导致 node id 无法保存到 page->flags 中。此时,内核必须使用额外的机制来保存 section 和 node 的对应关系,如下面代码所示:
c 复制代码
// file: mm/sparse.c
#ifdef NODE_NOT_IN_PAGE_FLAGS
/*
 * If we did not store the node number in the page then we have to
 * do a lookup in the section_to_node_table in order to find which
 * node the page belongs to.
 */
#if MAX_NUMNODES <= 256
static u8 section_to_node_table[NR_MEM_SECTIONS] __cacheline_aligned;
#else
static u16 section_to_node_table[NR_MEM_SECTIONS] __cacheline_aligned;
#endif

int page_to_nid(const struct page *page)
{
	return section_to_node_table[page_to_section(page)];
}
EXPORT_SYMBOL(page_to_nid);

static void set_section_nid(unsigned long section_nr, int nid)
{
	section_to_node_table[section_nr] = nid;
}

在上述代码片段中,由于 page->flags 中没有包含 node id ,内核定义了静态变量 section_to_node_table,来保存 section 和 node 的映射关系。为此,还专门定义了两个函数:page_to_nidset_section_nid,这就会增加代码的复杂度。

  • pfn 和 page 之间不能直接转换,还需要通过 section 做中转。
4.5.2.2 简单的转换算法

为了解决以上问题,引入了 "稀疏 vmemmap " 内存模型。该模型通过将 struct page 映射到一段连续的虚拟内存空间来优化 pfn_to_pagepage_to_pfn 操作。内核定义了一个全局变量 struct page *vmemmap,它指向一个虚拟地址连续的 struct page 数组。PFN 就是该数组的索引,struct page 与 vmemmap 的偏移量是该页面的 PFN,所以其转换关系如下:

c 复制代码
// file: include/asm-generic/memory_model.h
/* memmap is virtually contiguous.  */
#define __pfn_to_page(pfn)	(vmemmap + (pfn))
#define __page_to_pfn(page)	(unsigned long)((page) - vmemmap)

宏 vmemmap 定义如下:

c 复制代码
// file: arch/x86/include/asm/pgtable_64.h
#define vmemmap ((struct page *)VMEMMAP_START)
c 复制代码
// file: arch/x86/include/asm/pgtable_64_types.h
#define VMEMMAP_START	 _AC(0xffffea0000000000, UL)

在 x86-64 架构中,在内核态虚拟地址空间中特意开辟了一段 1TB 大小的空间用于映射 struct page 数组,这就是虚拟内存映射区(virtual memory map),请看 Linux 内核空间虚拟内存布局图:

4.5.2.3 示意图

综上所述,"稀疏 vmemmap" 的整体架构如下图所示:

五、APIS

笔者在"四、数据结构及其基本概念"一节中已经介绍了一些接口,接下来介绍其它接口。

5.1 内存分配接口

由于在进行内存模型的初始化时,内核主分配器还未就绪,所以只能使用 memblock 分配器来分配内存。在本文中,涉及到以下几个分配函数:alloc_bootmem__earlyonly_bootmem_alloc__alloc_bootmem_node_high___alloc_bootmem_node_nopanic,它们的关系如下图所示:

可以看到,不管是哪个接口,最终都会调用 memblock_find_rang_node 函数来分配内存。

memblock_find_rang_node() 函数接收 5 个参数:

  • @start: 候选区域的起始地址
  • @end: 候选区域的结束地址
  • @size:需要查找的空闲区域大小
  • @align:空闲区域的对齐字节
  • @nid:空闲区域的节点id,如果为 MAX_NUMNODES,则不限制节点

该函数从可用内存中,在 startend 范围内,查找指定节点的大小为 size 的空闲区域,空闲区域要对齐到 align 字节。在查找时,从高地址向低地址查找。查找成功,返回内存地址;否则返回 0。

memblock_find_rang_node 函数的详细实现请参考笔者以前的文章:Linux Kernel:启动时内存管理(MemBlock 分配器)

5.2 sparse_encode_early_nid、sparse_early_nid

在系统初始化早期,section_mem_map 中保存的是节点 id(见 memory_present 函数)。sparse_encode_early_nid 函数对节点 id 进行编码,即左移 SECTION_NID_SHIFT(扩展为 2)位。编码后,会将节点 id 保存到 section_mem_map 中。

sparse_early_nid 函数从 section_mem_map 字段中获取节点 id。

c 复制代码
// file: mm/sparse.c
/*
 * During early boot, before section_mem_map is used for an actual
 * mem_map, we use section_mem_map to store the section's NUMA
 * node.  This keeps us from having to use another data structure.  The
 * node information is cleared just before we store the real mem_map.
 */
static inline unsigned long sparse_encode_early_nid(int nid)
{
	return (nid << SECTION_NID_SHIFT);
}

static inline int sparse_early_nid(struct mem_section *section)
{
	return (section->section_mem_map >> SECTION_NID_SHIFT);
}

5.3 检测节点是否存在 -- present_section_nr

present_section_nr 函数判断 section 是否存在,它唯一的参数是 section number。

__nr_to_section 将 section number 转换成 struct mem_section 结构体,present_section 函数检测 section 是否存在。检测方法就是通过上文提到的特殊标志位 MARKED_PRESENT。如果 section_mem_map 中该标志位置位,说明 section 存在;否则,说明不存在。

c 复制代码
// file: include/linux/mmzone.h
static inline int present_section_nr(unsigned long nr)
{
	return present_section(__nr_to_section(nr));
}

5.3.1 present_section

c 复制代码
// file: include/linux/mmzone.h
static inline int present_section(struct mem_section *section)
{
	return (section && (section->section_mem_map & SECTION_MARKED_PRESENT));
}

5.4 pfn 和 section number 相互转换

pfn_to_section_nr、section_nr_to_pfn

c 复制代码
// file: include/linux/mmzone.h
#define pfn_to_section_nr(pfn) ((pfn) >> PFN_SECTION_SHIFT)
#define section_nr_to_pfn(sec) ((sec) << PFN_SECTION_SHIFT)

这两个宏比较简单,对比 PFN 结构图,很容易理解。

5.5 编、解码 mem_map

5.5.1 sparse_encode_mem_map

这里 mem_map 是指向 struct page 数组的指针,pnum 是 section number。

section_nr_to_pfn 根据 section number 获取对应的页帧号 pfn,然后 mem_map 减去该值获取相对地址。

c 复制代码
// file: mm/sparse.c
/*
 * Subtle, we encode the real pfn into the mem_map such that
 * the identity pfn - section_mem_map will return the actual
 * physical page frame number.
 */
static unsigned long sparse_encode_mem_map(struct page *mem_map, unsigned long pnum)
{
	return (unsigned long)(mem_map - (section_nr_to_pfn(pnum)));
}

5.5.2 sparse_decode_mem_map

coded_mem_map 是通过 sparse_encode_mem_map 函数编码后的 section_mem_map 值,通过 SECTION_MAP_MASK 掩码屏蔽掉低位的特殊标志,然后把 coded_mem_map 加上 section 对应的 pfn 完成解码操作,得到 struct page 数组指针。

c 复制代码
/*
 * Decode mem_map from the coded memmap
 */
struct page *sparse_decode_mem_map(unsigned long coded_mem_map, unsigned long pnum)
{
	/* mask off the extra low bits of information */
	coded_mem_map &= SECTION_MAP_MASK;
	return ((struct page *)coded_mem_map) + section_nr_to_pfn(pnum);
}

六、源码分析 -- 稀疏内存模型的初始化

由于 CONFIG_SPARSEMEM_VMEMMAP 默认是启用的,所以内核默认使用的是 "稀疏 vmemmap" 内存模型。在下文中我们主要关注"稀疏 vmemmap" 内存模型的初始化。

6.1 调用链

稀疏内存模型的初始化工作主要是在 paging_init 函数中进行的,其调用链如下:

6.1.1 setup_arch

c 复制代码
// file:
void __init setup_arch(char **cmdline_p)
{
......
    x86_init.paging.pagetable_init();
......
}

6.1.2 pagetable_init

c 复制代码
// file: arch/x86/kernel/x86_init.c
/*
 * The platform setup functions are preset with the default functions
 * for standard PC hardware.
 */
struct x86_init_ops x86_init __initdata = {
......
	.paging = {
		.pagetable_init		= native_pagetable_init,
	},
......
}

6.1.3 native_pagetable_init

c 复制代码
// file: arch/x86/include/asm/pgtable_types.h
#define native_pagetable_init        paging_init

6.2 paging_init

paging_init 函数主要进行稀疏内存模型的初始化以及 zone 的初始化。

稀疏内存模型的初始化主要分为两步,分别在两个函数中执行:

  • sparse_memory_present_with_active_regions 函数中,会为 struct mem_section 对象分配空间。

  • sparse_init 函数中会为 struct page 数组分配空间并填充页表。

c 复制代码
// file: arch/x86/mm/init_64.c
void __init paging_init(void)
{
	sparse_memory_present_with_active_regions(MAX_NUMNODES);
	sparse_init();

	/*
	 * clear the default setting with node 0
	 * note: don't use nodes_clear here, that is really clearing when
	 *	 numa support is not compiled in, and later node_set_state
	 *	 will not set it back.
	 */
	node_clear_state(0, N_MEMORY);
	if (N_MEMORY != N_NORMAL_MEMORY)
		node_clear_state(0, N_NORMAL_MEMORY);

	zone_sizes_init();
}

6.3 sparse_memory_present_with_active_regions

for_each_mem_pfn_range 会遍历 MemBlock (详见Linux Kernel:启动时内存管理(MemBlock 分配器))管理的所有可用内存块,并将内存块的起始页帧、结束页帧和节点 id 分别保存到 start_pfnend_pfnthis_nid 中。针对每个内存块,会调用 memory_present 函数为其构建 struct mem_section 对象。

c 复制代码
// file: mm/page_alloc.c
/**
 * sparse_memory_present_with_active_regions - Call memory_present for each active range
 * @nid: The node to call memory_present for. If MAX_NUMNODES, all nodes will be used.
 *
 * If an architecture guarantees that all ranges registered with
 * add_active_ranges() contain no holes and may be freed, this
 * function may be used instead of calling memory_present() manually.
 */
void __init sparse_memory_present_with_active_regions(int nid)
{
	unsigned long start_pfn, end_pfn;
	int i, this_nid;

	for_each_mem_pfn_range(i, nid, &start_pfn, &end_pfn, &this_nid)
		memory_present(this_nid, start_pfn, end_pfn);
}

6.3.1 memory_present

memory_present 函数为 memblock 内存块创建 struct mem_section 对象。我们在 "4.4 struct mem_section " 小节中介绍过,struct mem_section 对象是保存在一个二维数组中的。对于特别稀疏(启用 CONFIG_SPARSEMEM_EXTREME )的内存模型,我们只静态分配了一维数组,数组成员指向的 struct mem_section 对象需要动态分配。分配工作是在 memory_present 函数中完成的,更具体的说,是在 sparse_index_init 函数中完成的。

startend 分别表示内存块的起始和结束页帧号,mminit_validate_memmodel_limits 函数检查 startend 是否超出稀疏内存模型所能表示的最大页帧号 max_sparsemem_pfn。如果超出范围,则打印出警告信息,并对 startend 进行修正。

每个 section 包含 PAGES_PER_SECTION 个 page,每次迭代处理一个 section。在 sparse_index_init 函数中,会为 struct mem_section 数组分配空间,数组成员数量为 SECTIONS_PER_ROOT,即一页能够容纳的 struct mem_section 对象数量。

memory_present 整体执行流程示意如下:

c 复制代码
// file: mm/sparse.c
/* Record a memory area against a node. */
void __init memory_present(int nid, unsigned long start, unsigned long end)
{
	unsigned long pfn;

   /* 
    * start 是起始页帧号,页帧号由两部分组成:section 偏移和 section 索引。
    * 与 PAGE_SECTION_MASK 按位与后,会将页帧号的 section 偏移位置零,仅保留 section 索引
    * 相当于向下对齐到 section number
    */
	start &= PAGE_SECTION_MASK;
    
   /* 验证 start 和 end 均不能大于最大页帧号*/  
	mminit_validate_memmodel_limits(&start, &end);
   /* 
    * PAGES_PER_SECTION 表示每个 section 包含的 page 数量
    * 每次迭代后,pfn 增加 PAGES_PER_SECTION,跨越一个 section
    */
	for (pfn = start; pfn < end; pfn += PAGES_PER_SECTION) {
          /* pfn_to_section_nr 将页帧号转换成 section number */
		unsigned long section = pfn_to_section_nr(pfn);
		struct mem_section *ms;
		
          /* 
           * 为 struct mem_section 数组分配空间 
           * 我们在"4.4 "小节提到过,seciton 对象是保存在一个二维数组里的
           * 对于非常稀疏(启用了 CONFIG_SPARSEMEM_EXTREME)的内存来说,这个二维数组是动态分配的
           * **************************************************
           * struct mem_section *mem_section[NR_SECTION_ROOTS]
           *		____cacheline_internodealigned_in_smp;
           * **************************************************
           * 在 sparse_index_init 函数中,会为二级 struct mem_section 数组分配空间
           * 二级数组由 SECTIONS_PER_ROOT 个 struct mem_section 对象组成
           * 然后将分配的内存地址写入 mem_section[NR_SECTION_ROOTS] 数组中
           * 
           */
		sparse_index_init(section, nid);
        
          /* 在 x86-64 架构中为空函数 */
		set_section_nid(section, nid);
		
          /* __nr_to_section 将 section number 转换成结构体 */
		ms = __nr_to_section(section);
        
           /* 
            * 在系统初始化早期,section_mem_map 中保存的是节点 id 
            * 同时设置 MARKED_PRESENT 标志
            */
		if (!ms->section_mem_map)
			ms->section_mem_map = sparse_encode_early_nid(nid) |
							SECTION_MARKED_PRESENT;
	}
}
6.3.1.1 mminit_validate_memmodel_limits

mminit_validate_memmodel_limits 函数检查起始页帧 start_pfn 和结束页帧 end_pfn 是否超出稀疏内存模型所能表示的最大页帧号 max_sparsemem_pfn。如果超出范围,则打印警告信息,并对 start_pfnend_pfn 进行修正。

c 复制代码
// file: mm/sparse.c
/* Validate the physical addressing limitations of the model */
void __meminit mminit_validate_memmodel_limits(unsigned long *start_pfn,
						unsigned long *end_pfn)
{
	/* 计算稀疏内存模型的最大页帧号 */
    unsigned long max_sparsemem_pfn = 1UL << (MAX_PHYSMEM_BITS-PAGE_SHIFT);

	/*
	 * Sanity checks - do not allow an architecture to pass
	 * in larger pfns than the maximum scope of sparsemem:
	 */
	/* 如果起始页帧超出最大范围,则打印警告信息并同时修改起始和结束页帧号 */
	if (*start_pfn > max_sparsemem_pfn) {
		mminit_dprintk(MMINIT_WARNING, "pfnvalidation",
			"Start of range %lu -> %lu exceeds SPARSEMEM max %lu\n",
			*start_pfn, *end_pfn, max_sparsemem_pfn);
		WARN_ON_ONCE(1);
		*start_pfn = max_sparsemem_pfn;
		*end_pfn = max_sparsemem_pfn;
        
    /* 如果只有结束页帧超出最大范围,则打印警告信息并修结束页帧号 */
	} else if (*end_pfn > max_sparsemem_pfn) {
		mminit_dprintk(MMINIT_WARNING, "pfnvalidation",
			"End of range %lu -> %lu exceeds SPARSEMEM max %lu\n",
			*start_pfn, *end_pfn, max_sparsemem_pfn);
		WARN_ON_ONCE(1);
		*end_pfn = max_sparsemem_pfn;
	}
}
6.3.1.2 sparse_index_init

在此函数中,会调用 sparse_index_alloc 函数为二级(struct mem_section)数组分配内存。二级数组共有 SECTIONS_PER_ROOT 个成员,该值是由页大小除以 struct mem_section 对象的大小后得到的。换句话说,每个二级数组占用一个页大小。

c 复制代码
#define SECTIONS_PER_ROOT       (PAGE_SIZE / sizeof (struct mem_section))

需要注意的是,由于对每一个 section 都会调用该函数,而在 sparse_index_alloc 函数中每次为一组共 SECTIONS_PER_ROOT 个 struct mem_section 对象分配内存,所以此处需要判断 mem_section[root] 是否已经设置。如果已经设置则说明该 section 所在的一组内存已经全部分配过了,不用再次分配了。

c 复制代码
// file: mm/sparse.c
static int __meminit sparse_index_init(unsigned long section_nr, int nid)
{
	/* SECTION_NR_TO_ROOT 获取 section number 在一级数组中的索引 */
    unsigned long root = SECTION_NR_TO_ROOT(section_nr);
	struct mem_section *section;
	int ret = 0;
	
    /* 
     * sparse_index_alloc 每次分配一组共 SECTIONS_PER_ROOT 个 struct mem_section 对象
     * 但是对于每个 section 都会调用该函数 
     * 如果 mem_section[root] 为真,说明当前 section 所在的那一组 struct mem_section 对象已经分配过了
     * 直接略过后续步骤
     */
	if (mem_section[root])
		return -EEXIST;
    
	/* 为 struct mem_section 数组分配内存,每组 SECTIONS_PER_ROOT 个成员,占用一页内存 */
	section = sparse_index_alloc(nid);
	if (!section)
		return -ENOMEM;
    
	/* 将分配好的内存地址写入对应的 mem_section 成员中 */
	mem_section[root] = section;

	return ret;
}
6.3.1.3 sparse_index_alloc

在指定节点为 struct mem_section 数组分配内存,数组包含 SECTIONS_PER_ROOT 个 struct mem_section 对象。此时处于系统初始化早期,slab 分配器还未就绪,只能使用 memblock 分配器来分配内存。

c 复制代码
static struct mem_section noinline __init_refok *sparse_index_alloc(int nid)
{
	struct mem_section *section = NULL;
	unsigned long array_size = SECTIONS_PER_ROOT *
				   sizeof(struct mem_section);

	if (slab_is_available()) {
......
	} else {
		section = alloc_bootmem_node(NODE_DATA(nid), array_size);
	}

	return section;
}

6.4 sparse_init

memory_present 函数中,完成了 struct mem_section 数组的分配。从内核的函数名称来看,是完成了 "sparse 索引" 的初始化。在 sparse_init 函数中,则会为物理页对应的 struct page 分配内存,并将 section 和 page 关联起来。在"经典稀疏 "模型下,不需要为 struct page 建立页表,因为其虚拟内存位于直接映射区 ,该区的页表在此之前已经全部映射好了。但是对于"稀疏 vmemmap "模型,其虚拟地址位于虚拟内存映射区,需要新建页表进行映射。

struct mem_section 结构体还有一个字段 pageblock_flags,这是一个 unsigned long 类型的指针。 在 sparse_init 函数中,也需要为该指针指向的unsigned long 类型数据分配内存。pageblock_flags 的实际空间可能会大于 unsigned long 类型,换句话说,pageblock_flags可能指向一个数组。

c 复制代码
// file: include/linux/mmzone.h
struct mem_section {
	unsigned long section_mem_map;

	/* See declaration of similar field in struct zone */
	unsigned long *pageblock_flags;
......
};

概括一下,sparse_init 函数主要做了以下几件事:

  • mem_section->pageblock_flags 所指向的数据分配空间;
  • mem_section->section_mem_map 所指向的 struct page 数组分配空间
  • 将上述两步分配好的空间与 section 相关联

其主要完成的功能如下图所示:

在为 struct page 数组分配内存时,又分为集中分配(CONFIG_SPARSEMEM_ALLOC_MEM_MAP_TOGETHER)和单独分配两种方式。在集中分配的情况下,内核会一次性为节点所有页帧的 struct page 分配空间,也就是说一个节点只分配一次,这样可以保证每个节点的 struct page 在物理地址上是连续的;而在单独分配的情况下,每次只为一个 section 的 struct page 数组分配空间,一个节点要分配多次,只能保证单个 section 的 struct page 在物理地址上是连续的。

c 复制代码
// file: mm/sparse.c
/*
 * Allocate the accumulated non-linear sections, allocate a mem_map
 * for each and record the physical to section mapping.
 */
void __init sparse_init(void)
{
	unsigned long pnum;
	struct page *map;
	unsigned long *usemap;
	unsigned long **usemap_map;
	int size;
	int nodeid_begin = 0;
	unsigned long pnum_begin = 0;
	unsigned long usemap_count;
    
/* 集中分配时需要的参数 */
#ifdef CONFIG_SPARSEMEM_ALLOC_MEM_MAP_TOGETHER
	unsigned long map_count;
	int size2;
	struct page **map_map;
#endif

	/* Setup pageblock_order for HUGETLB_PAGE_SIZE_VARIABLE */
    /* 
     * 只有支持 HUGETLB_PAGE_SIZE_VARIABLE 的系统(IA64,powerpc)才需要该函数
     * 对于 x86-64 系统,这是个空函数
     */
	set_pageblock_order();

	/*
	 * map is using big page (aka 2M in x86 64 bit)
	 * usemap is less one page (aka 24 bytes)
	 * so alloc 2M (with 2M align) and 24 bytes in turn will
	 * make next 2M slip to one more 2M later.
	 * then in big system, the memory will have a lot of holes...
	 * here try to allocate 2M pages continuously.
	 *
	 * powerpc need to call sparse_init_one_section right after each
	 * sparse_early_mem_map_alloc, so allocate usemap_map at first.
	 */
    /*
     * usemap_map 是个临时 buffer,用于临时保存 pageblock_flags
     * 系统会一次性为所有 section 分配指向 pageblock_flags 数据的指针,空间大小为 size
     * 在后续为 pageblock_flags 数据分配空间后,pageblock_flags 数据会临时保存在 usemap_map 中
     * 在 sparse_init_one_section 函数中,会将 mem_section->pageblock_flags 指向 pageblock_flags 数据
     * 随后,这些临时 buffer 就没用了,在函数最后释放掉
     */
	size = sizeof(unsigned long *) * NR_MEM_SECTIONS;
	usemap_map = alloc_bootmem(size);
	if (!usemap_map)
		panic("can not allocate usemap_map\n");
	
    /*
     * 找到第一个存在的 section 编号 pnum_begin 以及所属的节点 nodeid_begin
     */
	for (pnum = 0; pnum < NR_MEM_SECTIONS; pnum++) {
		struct mem_section *ms;

		if (!present_section_nr(pnum))
			continue;
		ms = __nr_to_section(pnum);
		nodeid_begin = sparse_early_nid(ms);
		pnum_begin = pnum;
		break;
	}
    
    /*
     * usemap_count 统计节点的 section 数量
     * usemap_map 是临时 buffer,用于保存 pageblock_flags 数据
     * 每当发现节点变化时,为上一个节点批量分配 pageblock_flags 数据所需内存,
     * 同时重置 nodeid_begin、pnum_begin、usemap_count
     */
	usemap_count = 1;
	for (pnum = pnum_begin + 1; pnum < NR_MEM_SECTIONS; pnum++) {
		struct mem_section *ms;
		int nodeid;

		if (!present_section_nr(pnum))
			continue;
		ms = __nr_to_section(pnum);
		nodeid = sparse_early_nid(ms);
		if (nodeid == nodeid_begin) {
			usemap_count++;
			continue;
		}
		/* ok, we need to take cake of from pnum_begin to pnum - 1*/
        /* 
         * 为编号 pnum_begin 到 pnum - 1 的 section 分配 pageblock_flags 空间
         * 这些 section 属于同一个节点
         */
		sparse_early_usemaps_alloc_node(usemap_map, pnum_begin, pnum,
						 usemap_count, nodeid_begin);
		/* new start, update count etc*/
        /* 初始化下一个节点的 node id,起始 section id,section 数量 */ 
		nodeid_begin = nodeid;
		pnum_begin = pnum;
		usemap_count = 1;
	}
    
	/* ok, last chunk */
    /* 为最后一个节点中所有 section 的 pageblock_flags 数据分配内存 */
	sparse_early_usemaps_alloc_node(usemap_map, pnum_begin, NR_MEM_SECTIONS,
					 usemap_count, nodeid_begin);

/* 
 * CONFIG_SPARSEMEM_ALLOC_MEM_MAP_TOGETHER 决定了是否将节点中所有的 struct page 分配在一起
 */   
#ifdef CONFIG_SPARSEMEM_ALLOC_MEM_MAP_TOGETHER
    /*
     * map_map 是个临时 buffer,用于临时保存 struct page 数组
     * 系统会一次性为所有 seciton 分配指向 struct page 的指针,空间大小为 size2
     * 在后续为 struct page 分配空间后,struct page 会临时保存在 map_map 中
     * 在 sparse_init_one_section 函数中,会将 mem_section->section_mem_map 指向 struct page 数组
     * 随后,这些临时 buffer 就没用了,在函数最后释放掉
     */
	size2 = sizeof(struct page *) * NR_MEM_SECTIONS;
	map_map = alloc_bootmem(size2);
	if (!map_map)
		panic("can not allocate map_map\n");
	
    /*
     * 找到第一个存在的 section 编号 pnum_begin 以及所属的节点 nodeid_begin
     */
	for (pnum = 0; pnum < NR_MEM_SECTIONS; pnum++) {
		struct mem_section *ms;

		if (!present_section_nr(pnum))
			continue;
		ms = __nr_to_section(pnum);
		nodeid_begin = sparse_early_nid(ms);
		pnum_begin = pnum;
		break;
	}
    
    /*
     * map_count 统计节点的 section 数量
     * 每当发现节点变化时,为上一个节点批量分配 struct page 所需内存,
     * 即为节点中所有的 section 所需的 struct page 分配内存;
     * 同时重置 nodeid_begin、pnum_begin、usemap_count
     */
	map_count = 1;
	for (pnum = pnum_begin + 1; pnum < NR_MEM_SECTIONS; pnum++) {
		struct mem_section *ms;
		int nodeid;

		if (!present_section_nr(pnum))
			continue;
		ms = __nr_to_section(pnum);
		nodeid = sparse_early_nid(ms);
		if (nodeid == nodeid_begin) {
			map_count++;
			continue;
		}
		/* ok, we need to take cake of from pnum_begin to pnum - 1*/
        /* 初始化下一个节点的 node id,起始 section id,section 数量*/
		sparse_early_mem_maps_alloc_node(map_map, pnum_begin, pnum,
						 map_count, nodeid_begin);
		/* new start, update count etc*/
		nodeid_begin = nodeid;
		pnum_begin = pnum;
		map_count = 1;
	}
	/* ok, last chunk */
    /* 为最后一个节点中所有 section 的 struct page 分配内存 */
	sparse_early_mem_maps_alloc_node(map_map, pnum_begin, NR_MEM_SECTIONS,
					 map_count, nodeid_begin);
#endif
	/* 
	 * 每个 section 的 struct page 数组和 pageblock_flags 数据已经分配好了 
	 * 将它们写入 struct mem_section 对象对应的索引处就 ok 了
	 */
	for (pnum = 0; pnum < NR_MEM_SECTIONS; pnum++) {
        /* 跳过不存在的 section */
		if (!present_section_nr(pnum))
			continue;
		/* 
		 * usemap_map[pnum] 是编号为 pnum 的 section 的 pageblock_flags 数据地址
		 * 为 NULL 的话,说明该 section 不存在,直接跳过
		 */
		usemap = usemap_map[pnum];
		if (!usemap)
			continue;

/* 
 * 如果是集中分配方式,那么每个 section 的 struct page 数组已经分配并保存在 map_map 中了,
 * 此处可以直接获取;
 * 否则,通过 sparse_early_mem_map_alloc 函数单独分配
 * 如果 map 为 NULL,说明该 section 不存在
 */
#ifdef CONFIG_SPARSEMEM_ALLOC_MEM_MAP_TOGETHER
		map = map_map[pnum];
#else
		map = sparse_early_mem_map_alloc(pnum);
#endif
		if (!map)
			continue;

        /* 
         * 对单个 section 进行初始化 
         * 即设置 struct mem_section 对象的 section_mem_map 字段和 pageblock_flags 字段
         */
		sparse_init_one_section(__nr_to_section(pnum), pnum, map,
								usemap);
	}
    
	/* 
	 * 最后一次打印节点信息,仅适用于用巨页填充的情况,可参考 vmemmap_populate_hugepages 函数中的打印代码。
	 */
	vmemmap_populate_print_last();

/* 释放 buffer */  
#ifdef CONFIG_SPARSEMEM_ALLOC_MEM_MAP_TOGETHER
	free_bootmem(__pa(map_map), size2);
#endif
	free_bootmem(__pa(usemap_map), size);
}

6.4.1 sparse_early_usemaps_alloc_node

sparse_early_usemaps_alloc_node 函数为指定节点的所有 section 的 pageblock_flags 数据分配空间。

该函数接收 5 个参数:

  • @usemap_map:buffer 地址
  • @pnum_begin:节点的起始 section number
  • @pnum_end:节点的结束 section number
  • @usemap_count:节点中存在的 section 数量
  • @nodeid:节点id
c 复制代码
// file: mm/sparse.c
static void __init sparse_early_usemaps_alloc_node(unsigned long**usemap_map,
				 unsigned long pnum_begin,
				 unsigned long pnum_end,
				 unsigned long usemap_count, int nodeid)
{
	void *usemap;
	unsigned long pnum;
    /* 计算每个 section 中 pageblock_flags 数据占用的字节数 */
	int size = usemap_size();

    /*
     * 为节点中所有 section 的 pageblock_flags 数据集中分配内存,usemap 为内存的起始地址
     * size 是每个 section 的 pageblock_flags 数据占用的空间
     * usemap_count 是当前节点中 section 的数量
     * 所以,size * usemap_count 就是整个节点所有的 pageblock_flags 数据的总大小
     */
	usemap = sparse_early_usemaps_alloc_pgdat_section(NODE_DATA(nodeid),
							  size * usemap_count);
    
	/* 如果 usermap 为 0,说明分配失败,打印错误信息并返回 */
    if (!usemap) {
		printk(KERN_WARNING "%s: allocation failed\n", __func__);
		return;
	}

    /*
     * pnum_begin 是当前节点的起始 section nr,pnum_end 是当前节点的结束 section nr(不包含);
     * present_section_nr 判断 pnum 对应的 section 是否存在(利用 section_mem_map 字段中的 MARKED_PRESENT 位,位 0),
     * 如果不存在,则跳到下一个 section 继续执行;
     * 如果存在,则将为当前 section 设置 pageblock_flags 数据地址,
     * 并将 usemap 增加 size 大小,使其指向为下一个 section 分配的 pageblock_flags 数据地址。
     * 
     * 最后,通过 check_usemap_section_nr 检查每个 section 的 pageblock_flags 是否与 struct pglist_data 实例分配到同一个 section;
     * 如果不一致,那么可能产生循环依赖,导致内存无法释放或移动,此时需要打印出提示信息。
     */
	for (pnum = pnum_begin; pnum < pnum_end; pnum++) {
		if (!present_section_nr(pnum))
			continue;
		usemap_map[pnum] = usemap;
		usemap += size;
		check_usemap_section_nr(nodeid, usemap_map[pnum]);
	}
}
6.4.1.1 usemap_size

usemap_size 函数计算每个 section 的 pageblock_flags 数据所占用的字节数。其中,宏 SECTION_BLOCKFLAGS_BITS 指示pageblo ck_flags 数据实际占用的比特位数量。计算过程中,先将 SECTION_BLOCKFLAGS_BITS 向上圆整到整字节数,再向上圆整到 unsigned long 类型的倍数。

c 复制代码
unsigned long usemap_size(void)
{
	unsigned long size_bytes;
	size_bytes = roundup(SECTION_BLOCKFLAGS_BITS, 8) / 8;
	size_bytes = roundup(size_bytes, sizeof(unsigned long));
	return size_bytes;
}

宏 SECTION_BLOCKFLAGS_BITS 指示 pageblock_flags 数据实际占用的位数,其扩展如下:

c 复制代码
// file: include/linux/mmzone.h
#define SECTION_BLOCKFLAGS_BITS \
	((1UL << (PFN_SECTION_SHIFT - pageblock_order)) * NR_PAGEBLOCK_BITS)

其中 PFN_SECTION_SHIFT 指示在 PFN 中获取 section id 需要偏移的位数;pageblock_order 表示巨页的秩(即巨页的大小为 <math xmlns="http://www.w3.org/1998/Math/MathML"> 2 p a g e b l o c k _ o r d e r 2^{pageblock\_order} </math>2pageblock_order​),该宏最终扩展为 PMD_SHIFT - PAGE_SHIFT。

1UL << (PFN_SECTION_SHIFT - pageblock_order) 表示每个 section 最多容纳的巨页数量;NR_PAGEBLOCK_BITS 表示每个巨页需要的标志位数量;两者相乘就得到每个 section 中所有巨页所需要的标志位总数量。

pageblock_order
c 复制代码
// file: include/linux/pageblock-flags.h
/* Huge pages are a constant size */
#define pageblock_order		HUGETLB_PAGE_ORDER
c 复制代码
// file: arch/x86/include/asm/page_types.h
#define HPAGE_SHIFT		PMD_SHIFT
#define HUGETLB_PAGE_ORDER	(HPAGE_SHIFT - PAGE_SHIFT)

pageblock_order 示意图如下:

NR_PAGEBLOCK_BITS
c 复制代码
// file: include/linux/pageblock-flags.h
/* Bit indices that affect a whole block of pages */
enum pageblock_bits {
	PB_migrate,
	PB_migrate_end = PB_migrate + 3 - 1,
			/* 3 bits required for migrate types */
#ifdef CONFIG_COMPACTION
	PB_migrate_skip,/* If set the block is skipped by compaction */
#endif /* CONFIG_COMPACTION */
	NR_PAGEBLOCK_BITS
};
6.4.1.2 sparse_early_usemaps_alloc_pgdat_section

sparse_early_usemaps_alloc_pgdat_section 函数在指定节点分配 size 大小的内存。pgdat 是节点对应的结构体指针,size 是要分配的内存大小。具体的内存分配工作委托给了 ___alloc_bootmem_node_nopanic 函数,参见"5.1 内存分配接口"小节。

c 复制代码
// file: mm/sparse.c
#ifdef CONFIG_MEMORY_HOTREMOVE
static unsigned long * __init
sparse_early_usemaps_alloc_pgdat_section(struct pglist_data *pgdat,
					 unsigned long size)
{
	unsigned long goal, limit;
	unsigned long *p;
	int nid;
	/*
	 * A page may contain usemaps for other sections preventing the
	 * page being freed and making a section unremovable while
	 * other sections referencing the usemap retmain active. Similarly,
	 * a pgdat can prevent a section being removed. If section A
	 * contains a pgdat and section B contains the usemap, both
	 * sections become inter-dependent. This allocates usemaps
	 * from the same section as the pgdat where possible to avoid
	 * this problem.
	 */
    /*
     * goal 计算的是节点的 struct pglist_data 实例所在的 section 的起始地址
     * limit 计算的是 goal 所在 section 的下一个相邻 section 的起始地址
     * 也就是说,从物理地址 goal 到 limit - 1 之间的内存都属于同一个 section
     * 计算 goal 和 limit 的目的,是为了将同一个节点的所有 pageblock_flags 与 struct pglist_data 实例分配到同一个 section
     * 否则,如果它们分处两个不同的 section,可能会形成循环依赖,影响内存页的释放
     */
	goal = __pa(pgdat) & (PAGE_SECTION_MASK << PAGE_SHIFT);
	limit = goal + (1UL << PA_SECTION_SHIFT);
    
    /*
     * goal >> PAGE_SHIFT 得到 section 的起始页帧号
     * early_pfn_to_nid 函数获取页帧号对应的 node id
     * 在初始化早期,section_mem_map 中保存了 node id,直接获取就可以
     */
	nid = early_pfn_to_nid(goal >> PAGE_SHIFT);
    
again:
    /*
     * 为指定节点的 pageblock_flags 数据分配内存 
     * 分配时指定了节点 nid、内存大小 size、对齐边界、候选地址的下限 goal 和上限 limit
     * 换句话说,要在节点 nid 中分配 size 大小的内存,分配的内存地址要位于 goal 和 limit 之间,
     * 且起始地址必须是 SMP_CACHE_BYTES 的整数倍
     */
	p = ___alloc_bootmem_node_nopanic(NODE_DATA(nid), size,
					  SMP_CACHE_BYTES, goal, limit);
    
    /*
     * 如果 p 为 NULL 说明内存分配失败,系统内存不足
     * 此时如果指定了分配上限 limit,那么将 limit 设置为 0,重新分配内存
     * 当 limit 为 0 时,说明没有地址上限的要求,可分配空间更大
     * 
     * 需要说明的是,虽然指定了节点 id,但如果该节点内存不足的话,会降级使用其它节点来分配
     */
	if (!p && limit) {
		limit = 0;
		goto again;
	}
	return p;
}
___alloc_bootmem_node_nopanic

___alloc_bootmem_node_nopanic 函数内部会调用 __alloc_memory_core_early 函数来分配内存。首先,会在指定的节点 pgdat->node_id 分配内存,如果分配失败,说明指定节点的内存不足;此时,会再次调用__alloc_memory_core_early 函数,第二次的节点为 MAX_NUMNODES,指示可以在任意节点分配内存。

c 复制代码
// file: mm/nobootmem.c
void * __init ___alloc_bootmem_node_nopanic(pg_data_t *pgdat,
						   unsigned long size,
						   unsigned long align,
						   unsigned long goal,
						   unsigned long limit)
{
	void *ptr;

again:
	ptr = __alloc_memory_core_early(pgdat->node_id, size, align,
					goal, limit);
	if (ptr)
		return ptr;

	ptr = __alloc_memory_core_early(MAX_NUMNODES, size, align,
					goal, limit);
	if (ptr)
		return ptr;

	if (goal) {
		goal = 0;
		goto again;
	}

	return NULL;
}
6.4.1.3 check_usemap_section_nr

check_usemap_section_nr 检查 section 的 pageblock_flags 数据是否与 struct pglist_data 实例分配到同一个 section; 如果不一致,那么可能产生循环依赖,导致内存无法释放或移动,此时需要打印出提示信息。

usemap 为指向 pageblock_flags 数据的指针。

c 复制代码
// file: mm/sparse.c
static void __init check_usemap_section_nr(int nid, unsigned long *usemap)
{
	unsigned long usemap_snr, pgdat_snr;
	static unsigned long old_usemap_snr = NR_MEM_SECTIONS;
	static unsigned long old_pgdat_snr = NR_MEM_SECTIONS;
	struct pglist_data *pgdat = NODE_DATA(nid);
	int usemap_nid;

    /* 计算出 pageblock_flags 和 struct pglist_data 所在的 section number */
	usemap_snr = pfn_to_section_nr(__pa(usemap) >> PAGE_SHIFT);
	pgdat_snr = pfn_to_section_nr(__pa(pgdat) >> PAGE_SHIFT);
	if (usemap_snr == pgdat_snr)
		return;

	if (old_usemap_snr == usemap_snr && old_pgdat_snr == pgdat_snr)
		/* skip redundant message */
		return;
    
	/* 程序运行到这里,说明 pageblock_flags 和 struct pglist_data 实例不在同一个 section */
	old_usemap_snr = usemap_snr;
	old_pgdat_snr = pgdat_snr;
	
    /* 计算出 pageblock_flags 所在的节点 id */
	usemap_nid = sparse_early_nid(__nr_to_section(usemap_snr));
    
    /* 
     * 如果 pageblock_flags 和 struct pglist_data 不在同一个节点
     * 打印信息并返回
     */
	if (usemap_nid != nid) {
		printk(KERN_INFO
		       "node %d must be removed before remove section %ld\n",
		       nid, usemap_snr);
		return;
	}
	/*
	 * There is a circular dependency.
	 * Some platforms allow un-removable section because they will just
	 * gather other removable sections for dynamic partitioning.
	 * Just notify un-removable section's number here.
	 */
    /* 
     * 程序运行到这里,说明 pageblock_flags 和 struct pglist_data 在同一个节点
     * 但不在同一个 section,此时有可能产生循环依赖
     * 打印信息并返回
     */
	printk(KERN_INFO "Section %ld and %ld (node %d)", usemap_snr,
	       pgdat_snr, nid);
	printk(KERN_CONT
	       " have a circular dependency on usemap and pgdat allocations\n");
}

6.4.2 sparse_early_mem_maps_alloc_node

sparse_early_mem_maps_alloc_node 函数用于为节点对应的 struct page 实例分配空间,每个物理页都有一个 struct page 实例与之对应。

内部将实现委托给 sparse_mem_maps_populate_node 函数。对于"经典稀疏 "和"稀疏 vmemmap " 模型来说,两者使用的 sparse_mem_maps_populate_node 函数是不同的,本文只介绍"稀疏 vmemmap" 模型使用到的函数。

c 复制代码
// file: mm/sparse.c
static void __init sparse_early_mem_maps_alloc_node(struct page **map_map,
				 unsigned long pnum_begin,
				 unsigned long pnum_end,
				 unsigned long map_count, int nodeid)
{
	sparse_mem_maps_populate_node(map_map, pnum_begin, pnum_end,
					 map_count, nodeid);
}

6.4.3 sparse_mem_maps_populate_node

sparse_mem_maps_populate_node 函数为整个节点分配 struct page 实例。

c 复制代码
// file: mm/sparse-vmemmap.c
void __init sparse_mem_maps_populate_node(struct page **map_map,
					  unsigned long pnum_begin,
					  unsigned long pnum_end,
					  unsigned long map_count, int nodeid)
{
	unsigned long pnum;
    /* 
     * PAGES_PER_SECTION 表示每个 section 中包含的物理页数量;
     * size 计算每个 section 对应的 struct page 实例的总空间。
     */
	unsigned long size = sizeof(struct page) * PAGES_PER_SECTION;
	void *vmemmap_buf_start;

	/* 将 size 对齐到 PMD_SIZE, 即 2MB */
    size = ALIGN(size, PMD_SIZE);
    
    /* 
     * 一次性为整个节点分配 struct page 内存
     * map_count 表示节点中的 section 数量,size 表示每个 section 的 struct page 实例占用的空间
     * size * map_count 则是整个节点所有 struct page 实例占用的空间
     * 分配内存地址要求对齐到 PMD_SIZE,并且不小于 DMA 区域的最大地址,即 __pa(MAX_DMA_ADDRESS),
     * 换句话说,不能占用 DMA 区域,要在 DMA 区域之上的地址分配内存
     */
	vmemmap_buf_start = __earlyonly_bootmem_alloc(nodeid, size * map_count,
			 PMD_SIZE, __pa(MAX_DMA_ADDRESS));

    /* 
     * 如果 vmemmap_buf_start 为真,说明内存分配成功 
     * 将已分配内存保存到 buffer 中, vmemmap_buf 指向内存的起始地址,vmemmap_buf_end 指向内存的结束地址
     * vmemmap_buf 和 vmemmap_buf_end 是文件域静态变量
     * *************************************************
     *  // file: mm/sparse-vmemmap.c
     *  static void *vmemmap_buf;
     *	static void *vmemmap_buf_end;
     * *************************************************
     */
	if (vmemmap_buf_start) {
		vmemmap_buf = vmemmap_buf_start;
		vmemmap_buf_end = vmemmap_buf_start + size * map_count;
	}

    /* 
     * 整个节点所需要的 struct page 内存已经分配好了,现在需要将这些内存分配给每个 section,并为这些 struct page 实例建立页表
     * pnum_begin 是节点的起始 section number, pnum_end 是节点的结束 section nubmer(不包含)
     * 遍历节点的每个 section,计算其 struct page 数组的起始地址,并为 struct page 数组其建立页表,
     * 这些是在 sparse_mem_map_populate 函数中执行的
     * struct page 数组的起始地址会被放置到 map_map[pnum] 中,供 sparse_init_one_section 函数使用
     */
	for (pnum = pnum_begin; pnum < pnum_end; pnum++) {
		struct mem_section *ms;
        
		/* 不存在的 section 无需处理,直接跳过 */
		if (!present_section_nr(pnum))
			continue;
        
		/* 
		 * sparse_mem_map_populate 会计算 section 对应的 struct page 数组的起始地址
		 * 并为其建立页表
		 */
		map_map[pnum] = sparse_mem_map_populate(pnum, nodeid);
        
		/* 
		 * map_map[pnum] 为真,说明该页表建立成功,处理下一个 section
		 * 否则,说明页表建立失败,打印错误信息,并将 section_mem_map 设置为 0,指示该 section 无效
		 */
		if (map_map[pnum])
			continue;
		ms = __nr_to_section(pnum);
		printk(KERN_ERR "%s: sparsemem memory map backing failed "
			"some memory will not be available.\n", __func__);
		ms->section_mem_map = 0;
	}
	
   /* 释放临时 buffer */
	if (vmemmap_buf_start) {
		/* need to free left buf */
		free_bootmem(__pa(vmemmap_buf), vmemmap_buf_end - vmemmap_buf);
		vmemmap_buf = NULL;
		vmemmap_buf_end = NULL;
	}
}
6.4.3.1 sparse_mem_map_populate

sparse_mem_map_populate 函数接收 2 个参数:

  • @pnum:section nubmer
  • @nid:节点id

该函数会为 section 的 struct page 实例建立页表,并返回 struct page 数组的起始地址。

c 复制代码
// file: mm/sparse-vmemmap.c
struct page * __meminit sparse_mem_map_populate(unsigned long pnum, int nid)
{
	unsigned long start;
	unsigned long end;
	struct page *map;
	
   /* 
    * pnum 表示 section nubmer,PAGES_PER_SECTION 表示每个 section 中包含的 page 数量
    * pnum * PAGES_PER_SECTION 获得 section 的起始页帧号
    * pfn_to_page 将页帧号转换成 struct page 的虚拟地址
    * 由于采用的是 "稀疏 vmemmap" 模型,所以 pfn_to_page 返回的是虚拟映内存射区(virtual memory map)的地址
    *
    * start 表示 section 对应的 struct page 数组的起始地址,end 表示结束地址
    */
	map = pfn_to_page(pnum * PAGES_PER_SECTION);
	start = (unsigned long)map;
	end = (unsigned long)(map + PAGES_PER_SECTION);

	 /* 为 start 到 end 之间的虚拟内存区间建立页表 */
    if (vmemmap_populate(start, end, nid))
		return NULL;

	return map;
}
6.4.3.2 vmemmap_populate

vmemmap_populate 函数为 start 到 end 之间的虚拟内存区间建立页表。

c 复制代码
// file: arch/x86/mm/init_64.c
int __meminit vmemmap_populate(unsigned long start, unsigned long end, int node)
{
	int err;
	
   /*
    * cpu_has_pse 指示 cpu 是否具有页大小扩展(Page Size Extensions)功能,
    * 这是通过 cpu 探测功能发现的(通过 cpuid 指令)
    * 如果 cpu 支持该功能,那么就支持巨页(2MB页),否则只支持 4KB 页
    * 当 cpu 支持 pse 功能时,调用 vmemmap_populate_hugepages 来建立映射
    * 否则,通过 vmemmap_populate_basepages 函数来建立映射
    */
	if (cpu_has_pse)
		err = vmemmap_populate_hugepages(start, end, node);
	else
		err = vmemmap_populate_basepages(start, end, node);
    
   /* 
    * 由于修改了内核空间页表,需要将修改同步到所有的用户进程,
    * 只同步 start ~ end 区间的全局页目录 
    */
	if (!err)
		sync_global_pgds(start, end - 1);
	return err;
}
6.4.3.3 vmemmap_populate_basepages

使用 4KB 页为 struct page 实例构建页表。

由于页表必须从全局页目录(Page Global Directory,PGD)、上层页目录(Page Upper Directory,PUD)、中层页目录(Page Middle Directory,PMD)、页表(Page Table,PT)依次建立,所以在该函数中依次调用 vmemmap_pgd_populatevmemmap_pud_populatevmemmap_pte_populatevmemmap_pte_populate 来构建各级页表。

c 复制代码
// file: mm/sparse-vmemmap.c
int __meminit vmemmap_populate_basepages(unsigned long start,
					 unsigned long end, int node)
{
	unsigned long addr = start;
	pgd_t *pgd;
	pud_t *pud;
	pmd_t *pmd;
	pte_t *pte;

	for (; addr < end; addr += PAGE_SIZE) {
		pgd = vmemmap_pgd_populate(addr, node);
		if (!pgd)
			return -ENOMEM;
		pud = vmemmap_pud_populate(pgd, addr, node);
		if (!pud)
			return -ENOMEM;
		pmd = vmemmap_pmd_populate(pud, addr, node);
		if (!pmd)
			return -ENOMEM;
		pte = vmemmap_pte_populate(pmd, addr, node);
		if (!pte)
			return -ENOMEM;
		vmemmap_verify(pte, node, addr, addr + PAGE_SIZE);
	}

	return 0;
}
6.4.3.4 vmemmap_pgd_populate

vmemmap_pgd_populate 函数用来填充全局页目录项。

c 复制代码
// file: mm/sparse-vmemmap.c
pgd_t * __meminit vmemmap_pgd_populate(unsigned long addr, int node)
{
	 /* 获取全局目录项的指针 */
    pgd_t *pgd = pgd_offset_k(addr);
    
   /* 
    * 判断全局页目录项是否映射过
    * 如果未映射过,则分配一页内存用作上层页目录
    * 然后将上层页目录的物理地址连同页标志一起填充到全局页目录项(通过 pgd_populate 函数)
    */
	if (pgd_none(*pgd)) {
		void *p = vmemmap_alloc_block(PAGE_SIZE, node);
		if (!p)
			return NULL;
		pgd_populate(&init_mm, pgd, p);
	}
	return pgd;
}

pgd_offset_k 函数获取虚拟地址 addr 对应的全局页目录项指针;pgd_none 用于检测全局页目录项是否为 0。如果全局页目项不为 0,说明该全局页目项已经映射过了,不需要再做任何设置,直接使用即可;否则,说明该全局页目录项还没有映射过,则需要先分配一页内存作为上层目录(通过 vmemmap_alloc_block 函数),然后将上层页目录的物理地址连同页标志一起填充到全局页目录项(通过 pgd_populate 函数)。

笔者在 Linux Kernel:内存管理之分页(Paging) 一文中详细介绍过 pgd_offset_kpgd_nonepgd_populate 这三个函数的实现,此处不再赘述。

需要说明的是,init_mm 是内核空间的内存描述符对象,所以该函数填充的是内核空间的全局页目录项。

c 复制代码
// file: mm/init-mm.c
struct mm_struct init_mm = {
	.mm_rb		= RB_ROOT,
	.pgd		= swapper_pg_dir,
	.mm_users	= ATOMIC_INIT(2),
	.mm_count	= ATOMIC_INIT(1),
	.mmap_sem	= __RWSEM_INITIALIZER(init_mm.mmap_sem),
	.page_table_lock =  __SPIN_LOCK_UNLOCKED(init_mm.page_table_lock),
	.mmlist		= LIST_HEAD_INIT(init_mm.mmlist),
	INIT_MM_CONTEXT(init_mm)
};
6.4.3.5 vmemmap_alloc_block
c 复制代码
void * __meminit vmemmap_alloc_block(unsigned long size, int node)
{
	/* If the main allocator is up use that, fallback to bootmem. */
	if (slab_is_available()) {
		......
	} else
		return __earlyonly_bootmem_alloc(node, size, size,
				__pa(MAX_DMA_ADDRESS));
}

vmemmap_alloc_block 函数在指定节点分配 size 大小的内存。当 slab 内存分配器可用时,会调用 slab 分配器相关接口来分配内存。但此时,正处于内核启动阶段,slab 分配器还没有就绪,所以只能使用 memblock 分配器。

6.4.3.6 vmemmap_pud_populate、vmemmap_pmd_populate

这两个函数的工作原理与 vmemmap_pgd_populate 函数类似,都是先计算出各级页表项的指针,然后判断页表项是否映射过。如果已经映射过,则直接使用,否则,先为该页表项指向的下一级页表分配内存,然后通过 *_populate 函数将两者关联起来。

其中使用到的 pud_offsetpud_populatepmd_offsetpmd_populate_kernel 函数,笔者在 Linux Kernel:内存管理之分页(Paging) 一文中做过详细介绍,此处不再赘述。

c 复制代码
// file: mm/sparse-vmemmap.c
pud_t * __meminit vmemmap_pud_populate(pgd_t *pgd, unsigned long addr, int node)
{
	 /* 获取上层目录项的指针 */
    pud_t *pud = pud_offset(pgd, addr);
    
   /* 
    * 判断上层页目录项是否映射过
    * 如果未映射过,则分配一页内存用作中层页目录
    * 然后将中层页目录的物理地址连同页标志一起填充到上层页目录项
    */
	if (pud_none(*pud)) {
		void *p = vmemmap_alloc_block(PAGE_SIZE, node);
		if (!p)
			return NULL;
		pud_populate(&init_mm, pud, p);
	}
	return pud;
}
c 复制代码
// file: mm/sparse-vmemmap.c
pmd_t * __meminit vmemmap_pmd_populate(pud_t *pud, unsigned long addr, int node)
{
   /* 获取中层目录项的指针 */
	pmd_t *pmd = pmd_offset(pud, addr);
    
   /* 
    * 判断中层页目录项是否映射过
    * 如果未映射过,则分配一页内存用作页表
    * 然后将页表的物理地址连同页标志一起填充到中层页目录项
    */
	if (pmd_none(*pmd)) {
		void *p = vmemmap_alloc_block(PAGE_SIZE, node);
		if (!p)
			return NULL;
		pmd_populate_kernel(&init_mm, pmd, p);
	}
	return pmd;
}
6.4.3.7 vmemmap_pte_populate

vmemmap_pte_populate 函数的实现原理与以上三个函数类似。稍微有些不同的是,在分配内存时使用的是 vmemmap_alloc_block_buf 而不是 vmemmap_alloc_block 函数。这两个函数的主要不同点是:vmemmap_alloc_block_buf 会优先从内存 buffer(vmemmap_buf)中分配内存,如果 buffer 中没有内存或者内存空间不足,再调用 vmemmap_alloc_block 函数从 memblock 分配器中获取内存。

我们在 sparse_mem_maps_populate_node 函数中介绍过,在为每一个节点的 struct page 数组分配内存时,会先将内存放置到 buffer 里, buffer 的起始地址为 vmemmap_buf ,结束地址为 vmemmap_buf_end而 vmemmap_alloc_block_buf 会优先从 buffer 中分配内存,也就是说此时我们实际是将页表项映射到 strcuct page 数组所在的物理内存

c 复制代码
// file: mm/sparse-vmemmap.c
pte_t * __meminit vmemmap_pte_populate(pmd_t *pmd, unsigned long addr, int node)
{
    /* 获取页表项的指针 */
	pte_t *pte = pte_offset_kernel(pmd, addr);
    
    /* 
     * 判断中层页目录项是否映射过
     * 如果未映射过,则分配一页内存进行映射
     * vmemmap_alloc_block_buf 函数会优先从 vmemmap_buf 中分配内存
     * vmemmap_buf 到 vmemmap_buf_end 之间保存着
     * 在 sparse_mem_maps_populate_node 函数中为 struct page 数组分配的内存
     * 所以实际会从 buffer 中获取内存
     */
	if (pte_none(*pte)) {
		pte_t entry;
		void *p = vmemmap_alloc_block_buf(PAGE_SIZE, node);
		if (!p)
			return NULL;
        
           /* 将物理页的地址和页标志组合成页表项 entry */
		entry = pfn_pte(__pa(p) >> PAGE_SHIFT, PAGE_KERNEL);
        
           /* 将 entry 写入页表项中 */
		set_pte_at(&init_mm, addr, pte, entry);
	}
	return pte;
}
6.4.3.8 vmemmap_alloc_block_buf

vmemmap_alloc_block_buf 会优先从 buffer (vmemmap_buf)中分配内存,如果 buffer 中没有内存或者内存空间不足,再调用 vmemmap_alloc_block 函数从 memblock 分配器中获取内存。

c 复制代码
// file: mm/sparse-vmemmap.c
/* need to make sure size is all the same during early stage */
void * __meminit vmemmap_alloc_block_buf(unsigned long size, int node)
{
	void *ptr;
	
   /* buffer 中没有内存,即 buffer 分配失败时,使用 vmemmap_alloc_block 分配内存*/
	if (!vmemmap_buf)
		return vmemmap_alloc_block(size, node);

	/* take the from buf */
   /* buffer 中内存不足时,使用 vmemmap_alloc_block 分配内存*/
	ptr = (void *)ALIGN((unsigned long)vmemmap_buf, size);
	if (ptr + size > vmemmap_buf_end)
		return vmemmap_alloc_block(size, node);

	vmemmap_buf = ptr + size;

	return ptr;
}

其中,vmemmap_bufvmemmap_buf_end 是 buffer 的起始和结束地址。它们是文件域静态变量,定义如下:

c 复制代码
// file: mm/sparse-vmemmap.c
static void *vmemmap_buf;
static void *vmemmap_buf_end;
6.4.3.9 vmemmap_verify

vmemmap_verify 函数用于验证页表项所引用的物理页和指定节点是否一致,如果不一致,则打印警告信息。

pte 是页表项指针,通过 *pte可以获取到页表项的值,然后通过 pte_pfn 提取出页表项中的页帧号。然后,通过 early_pfn_to_nid 函数,获取页帧号对应的实际节点。最后,通过 node_distance 计算出实际节点和指定节点的距离,如果该值大于节点到自身的距离 LOCAL_DISTANCE,则说明这两个节点不一致,则打印警告信息。

c 复制代码
void __meminit vmemmap_verify(pte_t *pte, int node,
				unsigned long start, unsigned long end)
{
	unsigned long pfn = pte_pfn(*pte);
	int actual_node = early_pfn_to_nid(pfn);

	if (node_distance(actual_node, node) > LOCAL_DISTANCE)
		printk(KERN_WARNING "[%lx-%lx] potential offnode "
			"page_structs\n", start, end - 1);
}

node_distance 函数用于计算节点之间的距离,详见笔者以前的文章 Linux Kernel:NUMA 节点探测

宏 LOCAL_DISTANCE 表示节点到自身的距离,扩展为 10:

c 复制代码
// file: include/linux/topology.h
 /* Conform to ACPI 2.0 SLIT distance definitions */
 #define LOCAL_DISTANCE      10
6.4.3.10 vmemmap_populate_hugepages

当 cpu 支持 pse(Page Size Extentions) 特征时,会优先使用巨页(2MB页)而不是 4KB 页进行映射,vmemmap_populate_hugepages 函数处理使用巨页进行映射的情况。

c 复制代码
// file: arch/x86/mm/init_64.c
static int __meminit vmemmap_populate_hugepages(unsigned long start,
						unsigned long end, int node)
{
	unsigned long addr;
	unsigned long next;
	pgd_t *pgd;
	pud_t *pud;
	pmd_t *pmd;

  /*
   * 为虚拟地址空间 start ~ end 建立页表,每次映射 PMD_SIZE 大小的空间
   * 在此之前,要映射的物理内存(struct page 数组)已经分配完成,
   * 并存储在 buffer (vmemmap_buf ~ vmemmap_buf_end)中了
   */
	for (addr = start; addr < end; addr = next) {
          /* 获取下一次映射的起始地址 */
		next = pmd_addr_end(addr, end);

           /* 填充全局页目录项 */ 
		pgd = vmemmap_pgd_populate(addr, node);
		if (!pgd)
			return -ENOMEM;

           /* 填充上层页目录项 */ 
		pud = vmemmap_pud_populate(pgd, addr, node);
		if (!pud)
			return -ENOMEM;
		
          /*
           * 开始填充中层页目录项,由于映射的是巨页,所以中层页目录项直接映射到 2MB 的页
           */ 
		pmd = pmd_offset(pud, addr);
        
           /* pmd_none 检查中层页目项是否被映射过,如果为真,说明未映射过,需要映射 */ 
		if (pmd_none(*pmd)) {
			void *p;
            
		/*
			* vmemmap_alloc_block_buf 会优先从 buffer(vmemmap_buf ~ vmemmap_buf_end)中分配内存
			* 此前,struct page 数组已经分配到 buffer 中了,所以直接从 buffer 获取
			*/  
			p = vmemmap_alloc_block_buf(PMD_SIZE, node);
			if (p) {
				pte_t entry;
                
				/* 将页帧号和页标志组合成需要的中层页目录项格式 */ 
				entry = pfn_pte(__pa(p) >> PAGE_SHIFT,
						PAGE_KERNEL_LARGE);
                
                           // 将组合好的中层页目录项内容写入中层页目录中
				set_pmd(pmd, __pmd(pte_val(entry)));

				/* check to see if we have contiguous blocks */
                           /* 
                            * 检查映射的内存是否连续,当内存不连续或节点变化时,会打印出调试信息
                            * addr_start、addr_end、p_start、p_end、node_start 都是文件域静态变量
                            * ******************************************************************
                            * 	// file: arch/x86/mm/init_64.c
                            *	static long __meminitdata addr_start, addr_end;
                            *	static void __meminitdata *p_start, *p_end;
                            *	static int __meminitdata node_start;
                            * ********************************************************************
                            * 检查过程如下:
                            * 1、第一次循环,由于各静态变量的初始值为 0,if 条件成立但 p_start 为 0,
                            * 		所以不会打印调试信息,但会给 addr_start、node_start 以及 p_start 赋初值
                            *		然后,在 if 语句外,会给 addr_end 和 p_end 赋值,它们都等于下一次映射的理论起始地址
                            * 2、以后每次循环,都会判断理论起始地址(p_end)和实际起始地址(p)是否一致,以及节点是否发生变化
                            *		如果 p_end != p 说明上次映射的内存块和当前的内存块之间地址不连续,
                            *		如果 node_start != node, 说明节点发生了变化
                            *		这两种情况都会打印调试信息,并重置 addr_start、node_start 和 p_start
                            */
				if (p_end != p || node_start != node) {
					if (p_start)
						printk(KERN_DEBUG " [%lx-%lx] PMD -> [%p-%p] on node %d\n",
						       addr_start, addr_end-1, p_start, p_end-1, node_start);
					addr_start = addr;
					node_start = node;
					p_start = p;
				}

				addr_end = addr + PMD_SIZE;
				p_end = p + PMD_SIZE;
				continue;
			}
           /* pmd_large 判断中层页目录项是否直接映射到 2MB 的页 */
		} else if (pmd_large(*pmd)) {
            
                   /* 检查映射的物理页的实际节点是否等于指定节点,不一致则打印警告信息 */
			vmemmap_verify((pte_t *)pmd, node, addr, next);
			continue;
		}
          /* 
           * 当 vmemmap_alloc_block_buf 分配失败时,会执行到此处
           * 此时说明内存比较紧张,直接分配 2MB 内存失败了,那么则降级使用 4KB 页来映射
           */
		pr_warn_once("vmemmap: falling back to regular page backing\n");
		if (vmemmap_populate_basepages(addr, next, node))
			return -ENOMEM;
	}
	return 0;
}
6.4.3.11 pmd_addr_end

pmd_addr_end 用于获取当前 pmd 的结束地址,也是下一个 pmd 的起始地址,并防止地址回绕。

c 复制代码
// file: include/asm-generic/pgtable.h
/*
 * When walking page tables, get the address of the next boundary,
 * or the end address of the range if that comes earlier.  Although no
 * vma end wraps to 0, rounded up __boundary may wrap to 0 throughout.
 */
#define pmd_addr_end(addr, end)						\
({	unsigned long __boundary = ((addr) + PMD_SIZE) & PMD_MASK;	\
	(__boundary - 1 < (end) - 1)? __boundary: (end);		\
})

6.4.4 sparse_init_one_section

sparse_init_one_section 函数对 struct mem_section 对象进行初始化,主要是给两个字段赋值:section_mem_map 以及 pageblock_flags。该函数的四个参数说明如下:

  • @ms:需要初始化的 section 对应的结构体指针
  • @pnum:section number
  • @mem_map :section 对应的 struct page 数组地址
  • @pageblock_bitmap :section 对应的 pageblock_flags 指针

如果 section 不存在,直接返回错误码 -EINVAL;否则,对 section_mem_mappageblock_flags 字段赋值,并返回 1。

我们在 "4.3 小节 " 中介绍过,SECTION_MAP_MASK 的低 2 位是 0,那么 ~SECTION_MAP_MASK 的低 2 位为 1,其余位都为 0。section_mem_map~SECTION_MAP_MASK 进行按位与操作后,只会保留低 2 位的标志位,而将其它位零化,这样就清除了早期保存在 section_mem_map 中的节点 id。然后,通过 sparse_encode_mem_map 函数将 struct page 数组地址进行编码,并设置 SECTION_HAS_MEM_MAP 标志位,然后更新 section_mem_map 的值。还记得吗, memory_present 函数中,在 section_mem_map 中保存节点 id 时,已经将 SECTION_MARKED_PRESENT 置位了。至此,section_mem_map 中的两个特殊标志位已经全部置位了。

c 复制代码
// file: mm/sparse.c
static int __meminit sparse_init_one_section(struct mem_section *ms,
		unsigned long pnum, struct page *mem_map,
		unsigned long *pageblock_bitmap)
{
	if (!present_section(ms))
		return -EINVAL;

	ms->section_mem_map &= ~SECTION_MAP_MASK;
	ms->section_mem_map |= sparse_encode_mem_map(mem_map, pnum) |
							SECTION_HAS_MEM_MAP;
 	ms->pageblock_flags = pageblock_bitmap;

	return 1;
}

6.4.5 vmemmap_populate_print_last

最后一次打印节点信息,仅适用于使用巨页填充的情况,我们在 vmemmap_populate_hugepages 中见到过。

c 复制代码
void __meminit vmemmap_populate_print_last(void)
{
	if (p_start) {
		printk(KERN_DEBUG " [%lx-%lx] PMD -> [%p-%p] on node %d\n",
			addr_start, addr_end-1, p_start, p_end-1, node_start);
		p_start = NULL;
		p_end = NULL;
		node_start = 0;
	}
}

参考资料

1、Symmetric multiprocessing

2、Uniform memory access

3、Non-uniform memory access

4、Intel QuickPath Interconnect

5、x86: 64-bit, make sparsemem vmemmap the only memory model

6、Physical Memory Model

7、Memory: the flat, the discontiguous, and the sparse

相关推荐
maosheng11468 小时前
RHCSA的第一次作业
linux·运维·服务器
wifi chicken8 小时前
Linux 端口扫描及拓展
linux·端口扫描·网络攻击
旺仔.2919 小时前
Linux 信号详解
linux·运维·网络
放飞梦想C9 小时前
CPU Cache
linux·cache
Hoshino.419 小时前
基于Linux中的数据库操作——下载与安装(1)
linux·运维·数据库
播播资源11 小时前
CentOS系统 + 宝塔面板 部署 OpenClaw源码开发版完整教程
linux·运维·centos
源远流长jerry12 小时前
在 Ubuntu 22.04 上配置 Soft-RoCE 并运行 RDMA 测试程序
linux·服务器·网络·tcp/ip·ubuntu·架构·ip
lay_liu12 小时前
Linux安装redis
linux·运维·redis
寂柒13 小时前
序列化与反序列化
linux·网络
lay_liu13 小时前
ubuntu 安装 Redis
linux·redis·ubuntu