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

相关推荐
一颗星的征途5 小时前
宝塔-Linux模板常用命令-centos7
linux·运维·服务器
打打打劫5 小时前
Linux字符设备驱动
linux
cssl-虞老师5 小时前
Ubuntu安装Docker
linux·ubuntu·docker
Xu-小安安6 小时前
linux less命令详解
linux·less
huhy~6 小时前
PDI-kettle工具连接本地虚拟机Ubuntu上的数据库
linux·数据库·ubuntu
w_outlier6 小时前
gcc/g++的四步编译
linux·c++·gcc·g++
AllenLeungX6 小时前
Linux 安装 sftp
linux·运维·服务器
lendq7 小时前
k8s-第九节-命名空间
linux·容器·kubernetes
陪我养猪吧7 小时前
Linux 服务器环境搭建
linux·服务器·redis·mysql·nginx·jdk·maven
踩着阴暗的自己向上爬7 小时前
Day02-Jenkins与集成案例
linux·运维·servlet·jenkins