0. 前言

[linux内存管理] 第004篇 内存架构和内存模型一文中简单描述了处理器中对于内存共享技术的架构 NUMAUMA,以及基于这两个架构衍生的内存模型 FLATMEM、DISCONTIGMEM、SPARSEMEM,其中 FLATMEMDISCONTIGMEM 因为各自的缺点而被放弃, SPARSEMEM 是 Linux 中最多用途的内存模型,也是唯一一个支持多种高级features 的内存模型,例如物理内存的 hot-plughot-remove、 非易失性内存 设备的替代内存映射、延迟大型系统内存映射的初始化。

在 《内存架构和内存模型简述》 一文中只是简单阐述了 SPARSEMEM 的内存模型基本概念,本文将结合代码,详细地剖析 SPARSEMEM 模型的创建过程。

本文将继续承接上文,继续启动流程的分析,本文将详细分析 bootmem_init

1. bootmem_init

void __init bootmem_init(void)
{
	unsigned long min, max;

	//获取最小,最大页帧号
	min = PFN_UP(memblock_start_of_DRAM());
	max = PFN_DOWN(memblock_end_of_DRAM());

	///如果开启memtest,内核会对没有使用的free memory做memtest,检测出异常的dram
	//将这些dram通过reserve_bad_mem保留不用,从而保证系统正常boot
	early_memtest(min << PAGE_SHIFT, max << PAGE_SHIFT);

	max_pfn = max_low_pfn = max;
	min_low_pfn = min;

	///一些numa的初始化工作
	arch_numa_init();

	/*
	 * must be done after arch_numa_init() which calls numa_init() to
	 * initialize node_online_map that gets used in hugetlb_cma_reserve()
	 * while allocating required CMA size across online nodes.
	 */
#if defined(CONFIG_HUGETLB_PAGE) && defined(CONFIG_CMA)
	arm64_hugetlb_cma_reserve();
#endif

	dma_pernuma_cma_reserve();

	kvm_hyp_reserve();

	/*
	 * sparse_init() tries to allocate memory from memblock, so must be
	 * done after the fixed reservations
	 */
	///sparse内存模型初始化;
	sparse_init();

	///初始化zone数据结构
	zone_sizes_init(min, max);

	/*
	 * Reserve the CMA area after arm64_dma_phys_limit was initialised.
	 */
	dma_contiguous_reserve(arm64_dma_phys_limit);

	/*
	 * request_standard_resources() depends on crashkernel's memory being
	 * reserved, so do it here.
	 */
	reserve_crashkernel();

	memblock_dump_all();
}

botmem_init() 函数中共做了四件事情:

  • memblock_present()SPARSEMEM 模型的关键变量 mem_section 进行内存分配和初始化,该全局变量用以管理整个 SPARSEMEM 模型;
  • sparse_init() 进一步对 SPARSEMEM 初始化,包括对其管理的每个 page 进行映射;
  • zone_sizes_init() 将是 buddy 系统创建的第一步
  • memblock_dump_all()memblock_debug 使能的情况下,dump 现在 memblock 分布;

2. memblocks_present

Sparse Memory模型中,section是管理内存 online/offline的最小内存单元,在 ARM64中,section的大小为1G,而在Linux内核中,通过一个全局的二维数组 struct mem_section **mem_section来维护映射关系。
memblocks_present() 函数是 sparse内存模型 mem_section 的创建接口,意思是有一个新的内存块上线,不管是初始化还是热插拔内存都会进入到该函数里面。

///所有memblocks标记为present
static void __init memblocks_present(void)
{
	unsigned long start, end;
	int i, nid;

	///遍历memblock.memory中所有的块,获得起始地址,结束地址,nid
	for_each_mem_pfn_range(i, MAX_NUMNODES, &start, &end, &nid)
		memory_present(nid, start, end);
}

2.1 for_each_mem_pfn_range

#define for_each_mem_pfn_range(i, nid, p_start, p_end, p_nid)		\
	for (i = -1, __next_mem_pfn_range(&i, nid, p_start, p_end, p_nid); \
	     i >= 0; __next_mem_pfn_range(&i, nid, p_start, p_end, p_nid))
void __init_memblock __next_mem_pfn_range(int *idx, int nid,
				unsigned long *out_start_pfn,
				unsigned long *out_end_pfn, int *out_nid)
{
	struct memblock_type *type = &memblock.memory;
	struct memblock_region *r;
	int r_nid;

	//遍历 memblock.memory 中的所有区域,type->regions 是一个 memblock_region 数组
	while (++*idx < type->cnt) {
		r = &type->regions[*idx];
		//使用 memblock_get_region_node 获取当前区域的 NUMA 节点编号
		r_nid = memblock_get_region_node(r);
		//确保当前区域的物理页帧范围有效(base 对齐到上页帧,base + size 对齐到下页帧)
		if (PFN_UP(r->base) >= PFN_DOWN(r->base + r->size))
			continue;
		//若 nid 为 MAX_NUMNODES,则不检查 NUMA 节点;否则需匹配目标节点
		if (nid == MAX_NUMNODES || nid == r_nid)
			break;
	}
	//如果遍历完数组未找到符合条件的区域,将索引设置为 -1
	if (*idx >= type->cnt) {
		*idx = -1;
		return;
	}
	//根据需要更新输出参数,分别是起始页帧号、结束页帧号和 NUMA 节点编号
	if (out_start_pfn)
		*out_start_pfn = PFN_UP(r->base);
	if (out_end_pfn)
		*out_end_pfn = PFN_DOWN(r->base + r->size);
	if (out_nid)
		*out_nid = r_nid;
}

参数

  • int *idx: 当前遍历的索引,输入输出参数。
  • int nid: 目标 NUMA 节点,MAX_NUMNODES 表示不限制节点。
  • unsigned long *out_start_pfn: 输出的范围起始页帧号。
  • unsigned long *out_end_pfn: 输出的范围结束页帧号。
  • int *out_nid: 输出的 NUMA 节点编号。

主要功能是 遍历物理内存区域的页帧范围,并在遍历过程中根据指定的条件(如NUMA节点)过滤内存区域
假设我们当前在这个 for_each_mem_pfn_range中加入打印

for_each_mem_pfn_range(idx, MAX_NUMNODES, &start_pfn, &end_pfn, &nid) {
    printk("Region %d: Node %d, Start PFN: %lu, End PFN: %lu\n",
           idx, nid, start_pfn, end_pfn);
}

我们可能就会得到如下的打印日志:

Region 0: Node 0, Start PFN: 0, End PFN: 256
Region 1: Node 1, Start PFN: 256, End PFN: 512	//UMA只有一个node,在我司机器上不会显示Node 1
Region 2: Node 1, Start PFN: 512, End PFN: 768
...

2.2 memory_present

/* Record a memory area against a node. */
///1.分配mem_section的空间;
///2.为一个已知内存块(来自memblock.memory记录),分配所需的section,并且填充每个section的标记位,
//	 注意并未分配struct page
static void __init memory_present(int nid, unsigned long start, unsigned long end)
{
	unsigned long pfn;

#ifdef CONFIG_SPARSEMEM_EXTREME
	if (unlikely(!mem_section)) {
		unsigned long size, align;

		///需要NR_SECTION_ROOTS个一级指针,完整表达46/52位物理内存
		//ARM64默认预设了一个全局数组
		size = sizeof(struct mem_section *) * NR_SECTION_ROOTS;
		align = 1 << (INTERNODE_CACHE_SHIFT);
		///分配mem_section空间
		mem_section = memblock_alloc(size, align);
		if (!mem_section)
			panic("%s: Failed to allocate %lu bytes align=0x%lx\n",
			      __func__, size, align);
	}
#endif

	start &= PAGE_SECTION_MASK;
	mminit_validate_memmodel_limits(&start, &end);
	///以section为单位(一趟循环建立一个section)建立mem_section结构体内存空间,
	//这里并不分配struct page所占内存空间
	for (pfn = start; pfn < end; pfn += PAGES_PER_SECTION) {
		///计算pfn对应的全局mem_section下标
		unsigned long section = pfn_to_section_nr(pfn);
		struct mem_section *ms;

		///如果section下标对应的一级指针没有分配空间,则在node上分配一页空间
		sparse_index_init(section, nid);
		///nid同时保存在section_to_node_table全局数组
		set_section_nid(section, nid);

		///获取section下标对应的mem_section结构体
		ms = __nr_to_section(section);
		if (!ms->section_mem_map) {
			/*
			 * 初始化前期,section_mem_map记录nid信息及标记信息,节省字段
			 *
			 * ms->section_mem_map = nid << 6 | 1 << 2
			 * section_mem_map格式:
			 * 			nid  |SECTION_IS_ONLINE|SECTION_MARKED_PRESENT|
			 */
			ms->section_mem_map = sparse_encode_early_nid(nid) |
							SECTION_IS_ONLINE;
			__section_mark_present(ms, section);
		}
	}
}

首先在分析这个函数之前,需要了解 memblock的初始化,这个需要先查看下博文[linux内存管理] 第008篇 memblock子系统详解
通过 for_each_mem_pfn_range() 遍历所有的 memblock.memory, memblock为在启动阶段早期的内存管理模块,主要记录已经上线的所有内存块,此时 sparse内存模型数据还未初始化,memblock 主要记录每个内存 block 的物理内存大小,pfn等信息。另外注意,这个函数的第一个参数是 memory ,也即使需要轮询 memblock 中 memblock_type 是 memory 中的所有regions ,该宏定义在include/linux/memblock.h 中;

memory_present实际是pfn范围为start~end的物理页帧号按照section进行划分,并创建对应的mem_section结构体,且标记为present状态。

2.2.1 struct mem_section

这里引用[linux内存管理] 第004篇 内存架构和内存模型中的一部分内容

{% tip success %}
SPARSEMEM 稀疏内存模型的核心思想就是对粒度更小的连续内存块进行精细的管理,用于管理连续内存块的单元被称作 section 。物理页大小为 4k 的情况下, section 的大小为 128M ,物理页大小为 16k 的情况下, section 的大小为 512M。
{% endtip %}

在内核中用 struct mem_section 结构体表示 SPARSEMEM 模型中的 section。

struct mem_section {
        unsigned long section_mem_map;
		struct mem_section_usage *usage;
        ...
}

SPARSEMEM 模型使用一个 struct mem_section **mem_section 的二维数组来记录内存布局:

// 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;
#else
struct mem_section mem_section[NR_SECTION_ROOTS][SECTIONS_PER_ROOT]
	____cacheline_internodealigned_in_smp;
#endif
EXPORT_SYMBOL(mem_section);

数组中每一个一级指针都指向一片的物理内存空间,section 的size 和section 的最大个数,由MAX_PHYSMEM_BITSSECTION_SIZE_BITS 计算出,而这两个宏定义受支持 SPARSEMEM 模型的框架决定,例如对于ARM64 平台架构:

#ifdef CONFIG_SPARSEMEM
#define MAX_PHYSMEM_BITS	CONFIG_ARM64_PA_BITS

#if defined(CONFIG_ARM64_4K_PAGES) || defined(CONFIG_ARM64_16K_PAGES)
#define SECTION_SIZE_BITS 27
#else
#define SECTION_SIZE_BITS 29
#endif /* CONFIG_ARM64_4K_PAGES || CONFIG_ARM64_16K_PAGES */

#endif /* CONFIG_SPARSEMEM*/

比如我司手机配置

CONFIG_ARM64_PA_BITS=48
CONFIG_ARM64_4K_PAGES=y
# CONFIG_ARM64_16K_PAGES is not set
# CONFIG_ARM64_64K_PAGES is not set

这里的 MAX_PHYSMEM_BITS则为48,SECTION_SIZE_BITS为27

物理内存大小支持为2^48,区间为[0, 2^48)。每个 section 大小为 2^27,区间为[0, 2^27),也就是每个section 可以囊括地址范围为 128M,所以section 的数量则为 2^(48-27)

所以,系统定义的 SECTIONS_SHIFT 为:

#define SECTIONS_SHIFT	(MAX_PHYSMEM_BITS - SECTION_SIZE_BITS)

SPARSEMEM 稀疏内存模型

SPARSEMEM中,由于是全局的 mem_section 结构,故可以将所有的 mem_section 结构体从下标为 0 开始编号,一直累加到最后一个。

该结构体中其实重点维护两个成员变量:

  • section_mem_map
  • mem_section_usage
2.2.1.1 section_mem_map

section_mem_map 是一个多功能的变量,但在系统中意义在不同的阶段是不同的。

  • early boot阶段的section_mem_map

在 early boot 的阶段,在 section_mem_map 在用于确切的 mem_map 之前,会用该成员变量存储section 所属的nid。当然,nid 针对的是 NUMA,UMA 架构该值默认为0;
section_mem_map 的低 4 位用来记录当前 mem_section 的状态,可以表示 4 种情况:

#define	SECTION_MARKED_PRESENT	(1UL<<0)   //present 是否完成
#define SECTION_HAS_MEM_MAP	(1UL<<1)       //mem_map 是否设置
#define SECTION_IS_ONLINE	(1UL<<2)       //mem_section 是否在线
#define SECTION_IS_EARLY	(1UL<<3)       //是否在早期初始化阶段
#define SECTION_MAP_LAST_BIT	(1UL<<4)
#define SECTION_MAP_MASK	(~(SECTION_MAP_LAST_BIT-1))

此时 section_mem_map 的内容划分如下:

|-------------|-----------|------------|-----------------|--------------------|
|---Node id---|---EARLY---|---ONLINE---|---HAS_MEM_MAP---|---MARKED_PRESENT---|
|-------------|-----------|------------|-----------------|--------------------|
  • sparse_init之后的section_mem_map

memory_present() 函数中可以清晰看到 mem_sectionpresent 的时候,section_mem_map 只是简单初始化,此刻处于 early boot 阶段。
而在 sparse_init() 函数之后,将会为 mem_map 分配物理内存,early 阶段将结束,之后 section_mem_map 的内容划分是这样:

|--------------|-----------|------------|-----------------|--------------------|
|---map_base---|---EARLY---|---ONLINE---|---HAS_MEM_MAP---|---MARKED_PRESENT---|
|--------------|-----------|------------|-----------------|--------------------|

section_mem_map 的配置位于 sparse_init_one_section() 函数中,详细可以查看下文。

2.2.1.2 mem_section_usage
struct mem_section_usage {
#ifdef CONFIG_SPARSEMEM_VMEMMAP
	DECLARE_BITMAP(subsection_map, SUBSECTIONS_PER_SECTION);
#endif
	/* See declaration of similar field in struct zone */
	unsigned long pageblock_flags[0];
};

这里定义了两个变量:

  • unsigned long subsection_map[] 数组变量,计算下来数组长度为8,即 占用64 bytes,计算见下方(1<<(27-21) = 64)
  • unsigned log pageblock_flags[0] 表示动态分配内存
#define SUBSECTION_SHIFT 21
///一个mem_section对应的物理地址范围,128MB
#define SECTION_SIZE_BITS 27
#define SUBSECTIONS_PER_SECTION (1UL << (SECTION_SIZE_BITS - SUBSECTION_SHIFT))

pageblock_flags 这里定义在后面 sparse_early_usemaps_alloc_pgdat_section() 函数中会调用 mem_section_usage_size() 进行计算, 占用 128 bytes 。

2.2.1.3 mem_section相关的宏定义

为了后面对于mem_section有个比较全面的理解,这里将相关的宏定义集中说明。

参数解释
SECTION_SIZE_BITS27代表一个mem_section对应的物理地址范围为128MB
SECTIONS_SHIFT21用以标记最大 mem_section 的偏移位数
NR_MEM_SECTIONS1<<21= 2^21支持的最大 mem_section 数量,如果SECTION_SIZE_BITS为128M,那最大支持的物理内存为 2^21*128M = 256T
PFN_SECTION_SHIFT1<<(27-12)=2^15一个mem_section结构体对应的struct page结构体个数
SECTIONS_PER_ROOT4kb/(struct mem_section)=4kb/16=256表示一个页面中最多能存储的 struct mem_section 的数量,即每个根节点中可以管理的内存节数量

2.2.2 正式分析memory_present

正式来分析下 memory_present(),参数:
nid: memblock 中 region 所在节点,UMA架构下 nid=1
start: region 的起始 pfn;
end: region 的结束 pfn;

代码逻辑比较多,我们这里将其分成 11个部分 进行剖析

  1. CONFIG_SPARSEMEM_EXTREME

这个宏主要是确定 mem_section 为动态创建!
如果没有使能这个宏,则意味着 mem_section 使用的是静态二维数组:

mm/sparse.c
 
#ifdef CONFIG_SPARSEMEM_EXTREME
struct mem_section **mem_section;
#else
struct mem_section mem_section[NR_SECTION_ROOTS][SECTIONS_PER_ROOT]
	____cacheline_internodealigned_in_smp;
#endif
EXPORT_SYMBOL(mem_section);

此时内存管理还没有初始化完成,所以,kmalloc()/vmalloc() 等函数还无法使用,只能使用 memblock_alloc() 申请 mem_section 一维空间。

对于动态mem_section,全局变量 mem_section 初始值为0,所以,第一次进入 memory_present() 函数时,需要通过 memblock_alloc() 创建第一维内存空间,内存空间的大小为 NR_SECTION_ROOTS * sizeof(struct mem_section*)

备注:我司机器使能 CONFIG_SPARSEMEM_EXTREME,即使用了动态创建。

  1. 全局变量mem_section

在 step1 中其实已经说明了该全局变量。如果是动态创建的,那该值默认为0,第一次调用 memory_present() 函数时会进入case 创建第一维内存空间。

  1. 2.2.2.3 align 的确定

内存对齐方式是按照 INTERNODE_CACHE_SHIFT ,而该值用的是 L1_CACHE_SHIFT,所以应该是为了加快 mem_section 访问速度。

当对齐方式确定后,就会调用 memblock_alloc() 进行内存申请。

  1. mminit_validate_memmodel_limits

在每一次进入 memory_present() 函数中,都会传入region 的起始 pfn 和结束pfn,而其实 pfn 都需要 PAGE_SECTION_MASK 对齐。
接着,调用通过 mminit_validate_memmodel_limits() 函数对 start pfn、end pfn 进行check,要求传入的 pfn 不能超过最大值 max_sparsemem_pfn

unsigned long max_sparsemem_pfn = 1UL << (MAX_PHYSMEM_BITS-PAGE_SHIFT);

memory_present() 接收的形参 start、end 应该是region 的起始 pfn和结束pfn,如果超过 max_sparsemem_pfn ,则会进行调整,如下面代码:

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;
	}
}

主要功能是对内核内存管理模型的页帧范围(PFN,Page Frame Number)进行校验,确保其不超过稀疏内存模型(SPARSEMEM)的最大支持范围

  1. for 循环将该 memblock region 切分mem_section
for (pfn = start; pfn < end; pfn += PAGES_PER_SECTION) {
	//...
}

通过循环创建二维的 mem_section,每个section 可以容纳 PAGES_PER_SECTION 个 pages,所以,每一次计算 pfn 都是偏移 PAGES_PER_SECTION

  1. pfn_to_section_nr
unsigned long section = pfn_to_section_nr(pfn);

pfn_to_section_nr主要功能就是将页帧号转化为mem_section下标
假设一个mem_section表示128M内存空间,所有mem_section是连续的下标,对任意一个pfn,默认4K的页面,其mem_section下标是

pfn*4k/128M = pfn/(128m/4k) = pfn/2^(27-12) = pfn >> PFN_SECTION_SHIFT

  1. sparse_index_init

如果section下标对应的一级指针没有分配空间,则在node上分配一页空间。

在稀疏内存模型(SPARSEMEM)中,物理内存被划分为多个内存节(section),每个节表示一段连续的物理内存。为了高效管理这些内存节,内核将它们按组划分到根节点(root node)中,每个根节点管理一组内存节。

  • section_nr:
    表示一个内存节的全局编号(Section Number)。
    它是根据物理地址划分出的逻辑编号。
  • SECTION_NR_TO_ROOT(section_nr):
    用于将全局编号 section_nr 映射到该节所属的根节点编号。
///进一步动态创建
static int __meminit sparse_index_init(unsigned long section_nr, int nid)
{
	unsigned long root = SECTION_NR_TO_ROOT(section_nr);
	struct mem_section *section;

	/*
	 * An existing section is possible in the sub-section hotplug
	 * case. First hot-add instantiates, follow-on hot-add reuses
	 * the existing section.
	 *
	 * The mem_hotplug_lock resolves the apparent race below.
	 */
	if (mem_section[root])
		return 0;

	section = sparse_index_alloc(nid);
	if (!section)
		return -ENOMEM;

	mem_section[root] = section;

	return 0;
}

根据 pfn 获取到被划分到的 mem_section 总数目section_nr ( pfn >> PFN_SECTION_SHIFT), 该总数目是就是 root * SECTIONS_PER_ROOT 个 section,因为每个 root 对应的sections 数量是固定的,所以根据该总数目就能得到 root 值:
这段怎么理解呢?我们假设
PAGE_SIZE = 4KB(4096 字节)
sizeof(struct mem_section) = 16B
则:SECTIONS_PER_ROOT = 4096 / 16 = 256
现在有以下内存节:section_nr = 300:全局内存节编号
计算根节点编号:
root = section_nr/SECTIONS_PER_ROOT=300/256=1
也就是说编号为 300 的内存节属于根节点 1。

static noinline struct mem_section __ref *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()) {
		section = kzalloc_node(array_size, GFP_KERNEL, nid);
	} else {
		section = memblock_alloc_node(array_size, SMP_CACHE_BYTES,
					      nid);
		if (!section)
			panic("%s: Failed to allocate %lu bytes nid=%d\n",
			      __func__, array_size, nid);
	}

	return section;
}

对于还没有申请过 mem_section 内存,会通过 sparse_index_alloc() 申请纵维的内存空间,大小为 SECTIONS_PER_ROOT * sizeof(struct mem_section)

当slab 初始化完成时,通过 kzalloc_node() 从slab 分配器中分配内存,如果是初始化初期,则通过 memblock_alloc_node() 从 memblock 中申请内存。之所以有两种方案,主要是兼容了 hotplug 的memory。
early boot 的时候还是使用 memblock_alloc_node() 申请内存空间,每个 mem_section 会申请 1 个page 的内存空间。

  1. set_section_nid

section_to_node_table[section_nr]主要存储该root 下的所有section 的 nid,方便查找。

  1. __nr_to_section

获取section下标对应的mem_section结构体

///所有root下标连续,每个root里的mem_section数组下标也是连续
///这里根据nr,可以直接找到某个root里的具体mem_section
static inline struct mem_section *__nr_to_section(unsigned long nr)
{
#ifdef CONFIG_SPARSEMEM_EXTREME
	if (!mem_section)
		return NULL;
#endif
	if (!mem_section[SECTION_NR_TO_ROOT(nr)])
		return NULL;
	///一维的偏移是nr/(PAGE/sizeof(struct mem_section))
	///SECTION_NR_TO_ROOT(nr):nr在第几个struct mem_section[]数组

	///二维偏移是nr&(PAGE/sizeof(mem_section*) - 1)
	///nr & SECTION_ROOT_MASK:nr在某个struct mem_section[]数组中的第几个struct mem_section
	return &mem_section[SECTION_NR_TO_ROOT(nr)][nr & SECTION_ROOT_MASK];
}
  1. sparse_encode_early_nid

在mem_setion 申请成功后,会进一步初始化其成员变量 section_mem_map。

  1. __section_mark_present
static void __section_mark_present(struct mem_section *ms,
		unsigned long section_nr)
{
	if (section_nr > __highest_present_section_nr)
		__highest_present_section_nr = section_nr;

	ms->section_mem_map |= SECTION_MARKED_PRESENT;
}

设置 mem_section->section_mem_map 为 SECTION_MARKED_PRESENT ,表示该 section root 中所有的 mem_section 创建完成,初始化初步完成;
另外,这里会一直更新 __highest_present_section_nr 值,该变量记录已经 present 的section nr 的最大值。

2.3 memblocks_present总结

3. sparse_init继续分析

在上面几节中我们剖析了 bootmem_init()中第一个关键部分 memblock_present() ,这里:
创建了 mem_section** 在memblock中申请物理内存块 ,并将 mem_section**成功的拆成了二维;
对每个 mem_section*section_mem_map进行初始化;
这只是sparse模型初始化的第一步,即创建了 mem_section** 所需要的内存空间,而其后通过 sparse_init()SPARSEMEM 模型进行初始化:

void __init sparse_init(void)
{
	unsigned long pnum_end, pnum_begin, map_count = 1;
	int nid_begin;

	///根据memblock.memory信息,初始化mem_section二级指针
	memblocks_present();

	///根据section_mem_map标记位判断,找到第一个存在mem_section的下标
	pnum_begin = first_present_section_nr();
	///找第一个存在的mem_section的nid,在早期初始化阶段,section_mem_map保存node id
	nid_begin = sparse_early_nid(__nr_to_section(pnum_begin));

	/* Setup pageblock_order for HUGETLB_PAGE_SIZE_VARIABLE */
	set_pageblock_order();

	///遍历所有存在的mem_section
	for_each_present_section_nr(pnum_begin + 1, pnum_end) {
		int nid = sparse_early_nid(__nr_to_section(pnum_end));

		///统计nid总共占多少个mem_ection
		if (nid == nid_begin) {
			map_count++;
			continue;
		}
		/* Init node with sections in range [pnum_begin, pnum_end) */
		///初始化mem_section[pnum_end,pnum_end)
		sparse_init_nid(nid_begin, pnum_begin, pnum_end, map_count);
		nid_begin = nid;
		pnum_begin = pnum_end;
		map_count = 1;
	}
	/* cover the last node */
	sparse_init_nid(nid_begin, pnum_begin, pnum_end, map_count);
	vmemmap_populate_print_last();
}
  • 首先通过 first_present_section_nr() 获取第一个 mem_section nr,并存入 pnum_begin;
  • 通过 sparse_early_nid() 获取第一个mem_section 的node id,与 memory_present() 中的sparse_encode_early_nid() 对应, UMA 架构默认值为 0
  • set_pageblock_order(),如果开启 CONFIG_HUGETLB_PAGE_SIZE_VARIABLE 功能,则设置 pageblock_order,后续为 buddy 系统使用,如果没有开启,则不做任何事情;
  • for_each_present_section_nr() 从pnum_begin 开始遍历每个 mem_section,准备初始化,但是从 memory_present() 我们知道 nid 在 UMA系统情况下,默认值都为0,也就是说 for 循环中不会调用 sparse_init_nid(),而只做了两件事:
    • 获取 pnum_end;
    • 获取 map_count,用以记录符合要求的 mem_section 数量;
  • sparse_init_nid() 初始化指定 section 范围,范围为 [ pnum_begion, pnum_end ]
  • 当然,如果存在多个 nid,那么for 循环会不断更新 下一个node 节点的 nid_begin、pnum_begin 以及 map_count;
  • for 循环完成后,初始化 最后一个node 节点的 mem_section。

sparse_init_nid() 函数是 sparse_init() 的核心处理函数,本文将单独一节进行剖析。

3.1 sparse_init_nid()

static void __init sparse_init_nid(int nid, unsigned long pnum_begin,
				   unsigned long pnum_end,
				   unsigned long map_count)
{
	struct mem_section_usage *usage;
	unsigned long pnum;
	struct page *map;

	///该node有map_count个mem_section,也就是有map_count个mem_section_usage
	usage = sparse_early_usemaps_alloc_pgdat_section(NODE_DATA(nid),
			mem_section_usage_size() * map_count);
	if (!usage) {
		pr_err("%s: node[%d] usemap allocation failed", __func__, nid);
		goto failed;
	}
	///为该节点申请struct page的内存
	//一个mem_section对应PAGES_PER_SECTION个struct page
	//[sparsemap_buf, sparsemap_buf_end]
	sparse_buffer_init(map_count * section_map_size(), nid);
	///初始化node的每一个mem_section
	for_each_present_section_nr(pnum_begin, pnum) {
		///计算section下标对应的页帧号,比如nr=1, 对应128M
		unsigned long pfn = section_nr_to_pfn(pnum);

		if (pnum >= pnum_end)
			break;

		///mem_section对应的struct page起始地址
		map = __populate_section_memmap(pfn, PAGES_PER_SECTION,
				nid, NULL);
		if (!map) {
			pr_err("%s: node[%d] memory map backing failed. Some memory will not be available.",
			       __func__, nid);
			pnum_begin = pnum;
			sparse_buffer_fini();
			goto failed;
		}
		check_usemap_section_nr(nid, usage);
		///初始化mem_section,设置section的section_mem_map,指向struct page数组起始地址
		sparse_init_one_section(__nr_to_section(pnum), pnum, map, usage,
				SECTION_IS_EARLY);
		usage = (void *) usage + mem_section_usage_size();
	}
	sparse_buffer_fini();
	return;
failed:
	/* We failed to allocate, mark all the following pnums as not present */
	for_each_present_section_nr(pnum_begin, pnum) {
		struct mem_section *ms;

		if (pnum >= pnum_end)
			break;
		ms = __nr_to_section(pnum);
		ms->section_mem_map = 0;
	}
}

3.2 __populate_section_memmap

struct page * __meminit __populate_section_memmap(unsigned long pfn,
		unsigned long nr_pages, int nid, struct vmem_altmap *altmap)
{
	unsigned long start = (unsigned long) pfn_to_page(pfn);
	unsigned long end = start + nr_pages * sizeof(struct page);

	if (WARN_ON_ONCE(!IS_ALIGNED(pfn, PAGES_PER_SUBSECTION) ||
		!IS_ALIGNED(nr_pages, PAGES_PER_SUBSECTION)))
		return NULL;

	if (vmemmap_populate(start, end, nid, altmap))
		return NULL;

	return pfn_to_page(pfn);
}

sparse_buffer_init() 申请到 mem_sectioon 的 mem_map 空间后,进入 for 循环开始轮询每个 mem_section,并通过调用 __populate_section_memmap() 将mem_map 映射到 vmemmap 区域。

int __meminit vmemmap_populate(unsigned long start, unsigned long end, int node,
		struct vmem_altmap *altmap)
{
	unsigned long addr = start;
	unsigned long next;
	pgd_t *pgdp;
	p4d_t *p4dp;
	pud_t *pudp;
	pmd_t *pmdp;

	WARN_ON((start < VMEMMAP_START) || (end > VMEMMAP_END));
	//要对每个 mem_section 对应的空间做vmemmap,开启循环,每次映射的一个pmdsize
	do {
		//确定下一次映射的起始地址,偏移pmd size
		next = pmd_addr_end(addr, end);

		//获取需要映射的地址的pgd
		pgdp = vmemmap_pgd_populate(addr, node);
		if (!pgdp)
			return -ENOMEM;

		p4dp = vmemmap_p4d_populate(pgdp, addr, node);
		if (!p4dp)
			return -ENOMEM;

		//对于三级页表来说,pud就是pgd
		pudp = vmemmap_pud_populate(p4dp, addr, node);
		if (!pudp)
			return -ENOMEM;
		//确定该pmd 页表位置,并读取pmdp,确实是否存在 ptep
		pmdp = pmd_offset(pudp, addr);
		if (pmd_none(READ_ONCE(*pmdp))) {
			void *p = NULL;
			//从sparsemap_buf 中申请struct page空间
			p = vmemmap_alloc_block_buf(PMD_SIZE, node, altmap);
			if (!p) {
				if (vmemmap_populate_basepages(addr, next, node, altmap))
					return -ENOMEM;
				continue;
			}

			pmd_set_huge(pmdp, __pa(p), __pgprot(PROT_SECT_NORMAL));
		} else
			vmemmap_verify((pte_t *)pmdp, node, addr, next);
	} while (addr = next, addr != end);

	return 0;
}

4. 总结

可能上面所说的比较难以理解,此节以物理内存8G的UMA系统为例描述这个场景:

假设物理内存大小为 8GB,页面大小(PAGE_SIZE)为 4KB,每节大小(SECTION_SIZE)为 128MB

系统参数

  1. 物理内存总大小
    Total Memory=8 GB=8192MB
  2. 每节大小(SECTION_SIZE
    SECTION_SIZE = 128MB
  3. 每节包含的页面数
    PAGES_PER_SECTION =SECTION_SIZE/PAGE_SIZE = 32768
  4. 节的总数
    NR_MEM_SECTIONS} = 64
  5. UMA 环境下的 NUMA 节点
    • 所有 mem_section 都属于同一个节点 nid = 0
节编号 (pnum)起始地址结束地址节状态节所属节点
00x000000000x08000000present0
10x080000000x10000000present0
...............
630x1F8000000x20000000present0
  • 每个节大小为 128MB
  • 所有节都属于节点 nid = 0

struct page 描述符分配

  • 每节包含的页面数: PAGES_PER_SECTION = 32768
  • struct page 总数: Total Pages=64×32768= 2097152

然后将这2097152个page,映射到vmemmap区域!!

至此,内存模型 SPARSEMEM 初始化就分析完成了!