本文采用 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 想要访问内存都要经过总线,而且距离都是一样的,这种架构称为 SMP (Symmetric Multiprocessing,对称多处理器 )架构。在 SMP 架构下,任何处理器访问内存的距离是相同的,所以其访问内存的速度是一致的。这种架构也被成为基于 SMP 的 UMA (Uniform Memory Access,一致性内存访问)架构。
UMA 架构的特点是简单,但是有一个显著的缺点:由于所有处理器访问内存都要经过总线,当处理器数量很多时,总线就会成为整个系统的瓶颈。
2.2 NUMA 架构
鉴于 UMA 系统的上述缺点,它其实不适合构建大型的计算机系统。为了解决上述问题,提高 CPU 访问内存的性能和扩展性,于是引入了一种新的架构,即非一致性内存访问 (Non-uniform memory access ,NUMA )架构。在 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_pfn
与 pfn_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_map
在 alloc_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_OFFSET
在 include/asm-generic/page.h
文件中是有定义的,但其依赖于 PAGE_OFFSET
和 PAGE_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_section
和 valid_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_page
和 page_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_section
和 page_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_nid
和 set_section_nid
,这就会增加代码的复杂度。
- pfn 和 page 之间不能直接转换,还需要通过 section 做中转。
4.5.2.2 简单的转换算法
为了解决以上问题,引入了 "稀疏 vmemmap " 内存模型。该模型通过将 struct page
映射到一段连续的虚拟内存空间来优化 pfn_to_page
和 page_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,则不限制节点
该函数从可用内存中,在 start
到 end
范围内,查找指定节点的大小为 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_pfn
、 end_pfn
和 this_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
函数中完成的。
start
和 end
分别表示内存块的起始和结束页帧号,mminit_validate_memmodel_limits
函数检查 start
和 end
是否超出稀疏内存模型所能表示的最大页帧号 max_sparsemem_pfn
。如果超出范围,则打印出警告信息,并对 start
或 end
进行修正。
每个 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_pfn
或 end_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_populate
、vmemmap_pud_populate
、vmemmap_pte_populate
和 vmemmap_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_k
、pgd_none
、pgd_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_offset
、pud_populate
、pmd_offset
、pmd_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_buf
和 vmemmap_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_map
和 pageblock_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;
}
}
参考资料
4、Intel QuickPath Interconnect