Linux 内存管理:Sparse 内存模型简述

1. 前言

限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。

2. Sparse 内存模型

笔者在 Linux 内存管理 (4):buddy 管理系统的建立 分析了 不连续内存模型(Discontiguous Memory Model) 下最终建立内存管理数据图,如下:

Linux 内存管理数据结构

本文分析 Sparse 内存模型(CONFIG_SPARSEMEM=y),是基于前面的博文:只理出 不连续内存模型(Discontiguous Memory Model)Sparse 内存模型 的差异部分,两者相同的部分可参考前面提到的博文。

不连续内存模型(Discontiguous Memory Model)Sparse 内存模型 的差异在于物理页面管理数据(即 memmap)结构不同。非 Sparse 内存模型下(也包括不连续内存模型(Discontiguous Memory Model)),物理页面管理数据(即 memmap) 不再来自 pglist_data::node_mem_map

// include/linux/mmzone.h

typedef struct pglist_data {
	...
#ifdef CONFIG_FLAT_NODE_MEM_MAP	/* means !SPARSEMEM */
	/*
	 * NUMA 节点 @node_id 实际 起始物理页面 的 struct page :
	 * NUMA 节点 的每物理页面管理数据 struct page, 是按将
	 * NUMA 节点 起始物理页面页框号 @node_start_pfn 向下对齐到 
	 * (1 << MAX_ORDER) 后进行分配的, 也就是说, 为 NUMA 节点 分配
	 * 的每物理页面管理数据 struct page , 头部有一些是为对齐到 (1 << MAX_ORDER)
	 * 页面数 而分配的多余无用的 struct page , 这时候用 @node_mem_map
	 * 指向实际的 NUMA 节点 第1个起始物理页面 (@node_start_pfn) 的 struct page .
	 */
	struct page *node_mem_map;
#ifdef CONFIG_PAGE_EXTENSION
	struct page_ext *node_page_ext;
#endif
#endif /* CONFIG_FLAT_NODE_MEM_MAP */
	...
};

而是来自 mem_section

// mm/sparse.c

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.
	 */
	/*
	 * 系统启动初期 (early boot) ,用来记录 NUMA node id;
	 * 启动结束后,被赋予 mem_map 值,指向页表管理数据 (struct page) 。
	 */
	unsigned long section_mem_map;

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

/*
 * 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);

mem_section 的构建实在系统初始化早期,以 ARM64 + Linux 4.14.111 为例:

start_kernel()
	setup_arch()
		/*
		 * 初始化 内核空间页表 (swapper_pg_dir[]):
		 * - 在 swapper_pg_dir[] PGD 页表中重建内核映射
		 * - 在 swapper_pg_dir[] PGD 页表中建立系统 RAM 映射
		 */
		paging_init()
		...
		bootmem_init()
			...
			arm64_numa_init()
			/*
			 * Sparsemem tries to allocate bootmem in memory_present(), so must be
			 * done after the fixed reservations.
			 */
			arm64_memory_present()
			sparse_init()
			...
		...
// mm/sparse.c

/*
 * Allocate the accumulated non-linear sections, allocate a mem_map
 * for each and record the physical to section mapping.
 */
/*
 * Sparse Memory Model 初始化.
 * 为所有 NUMA 节点内的 present section 分配:
 * . pageblock flag 管理数据空间 (mem_section::pageblock_flags)
 */
void __init sparse_init(void)
{
	unsigned long pnum;
	struct page *map;
	unsigned long *usemap;
	unsigned long **usemap_map;
	int size;
#ifdef CONFIG_SPARSEMEM_ALLOC_MEM_MAP_TOGETHER
	int size2;
	struct page **map_map;
#endif

	/* see include/linux/mmzone.h 'struct mem_section' definition */
	BUILD_BUG_ON(!is_power_of_2(sizeof(struct mem_section)));

	/* Setup pageblock_order for HUGETLB_PAGE_SIZE_VARIABLE */
	set_pageblock_order(); /* 设置 HUGE 页面的 page 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.
	 */
	size = sizeof(unsigned long *) * NR_MEM_SECTIONS;
	usemap_map = memblock_virt_alloc(size, 0);
	if (!usemap_map)
		panic("can not allocate usemap_map\n");
	alloc_usemap_and_memmap(sparse_early_usemaps_alloc_node,
							(void *)usemap_map);

	/*
	 * CONFIG_SPARSEMEM_ALLOC_MEM_MAP_TOGETHER 试图将一个
	 * NUMA 节点内所有 mem_map 管理数据分配在连续的空间内.
	 */
#ifdef CONFIG_SPARSEMEM_ALLOC_MEM_MAP_TOGETHER
	/*
	 * 分配每 section 的 memmap 指针数组 struct page *map_map[]:
	 * 用来指向各自的 struct page[].
	 *
	 * 注意, 这里没有分配每 section 的 struct page 数据,
	 * 仅仅是每 section 的 struct page * 指针.
	 */
	size2 = sizeof(struct page *) * NR_MEM_SECTIONS;
	map_map = memblock_virt_alloc(size2, 0);
	if (!map_map)
		panic("can not allocate map_map\n");
	/* 为所有 NUMA 节点内的 present section 分配 mem_map 管理数据(struct page) */
	alloc_usemap_and_memmap(sparse_early_mem_maps_alloc_node,
							(void *)map_map);
#endif

	/*
	 * 配置所有 NUMA 节点所有 present section :
	 * . pageblock flag 管理数据空间;
	 * . mem_map 管理数据空间(struct page).
	 */
	for_each_present_section_nr(0, pnum) {
		usemap = usemap_map[pnum];
		if (!usemap)
			continue;

#ifdef CONFIG_SPARSEMEM_ALLOC_MEM_MAP_TOGETHER
		map = map_map[pnum]; /* 使用前面提前分配好的 mem_map 管理数据(struct page) */
#else
		map = sparse_early_mem_map_alloc(pnum); /* 为 section @pnum 分配 mem_map 管理数据(struct page) */
#endif
		if (!map)
			continue;

		sparse_init_one_section(__nr_to_section(pnum), pnum, map,
								usemap);
	}

	vmemmap_populate_print_last();

	/*
	 * 释放存放 pageblock flag 和 mem_map 指针的空间:
	 * 这些信息已经记录到 section 的管理数据 struct mem_section 了.
	 */
#ifdef CONFIG_SPARSEMEM_ALLOC_MEM_MAP_TOGETHER
	memblock_free_early(__pa(map_map), size2);
#endif
	memblock_free_early(__pa(usemap_map), size);
}

Sparse 内存模型下最终建立如下图所示的物理页面管理数据(即 memmap)

Linux sparse memory model.drawio

对上图做一下简要解释,ARM64 下使用 48-bit 的物理地址(PA),其中 PA[47:12] 划分为 3 部分,这 3 部分也划分了 mem_section 对物理页面的 3 级管理:

  • section root #: 物理页面所在的 section root 编号
  • section #: 物理页面所在的 section 在对应 section root X 内的编号
  • page #: 物理页面在 section X 内的编号

也可以这样来理解:

sparse 内存模型 memmap = section-root 0, [section-root 1, [..., [section-root X]]]
section-root X = section 0, [section 1, [..., [section X]]]
section X = page 0, [page 1, [..., [page X]]]

可见,在 Sparse 内存模型下,memmap 数据不再像非 Sparse 内存模型的其它模式,呈现一维数据的“平坦”结构,而是呈现了层级树状结构,这很符合 Sparse 内存模型的特点,毕竟 Sparse 内存模型设计用来管理物理内存物理地址不连续,同时结构位置不单一的 NUMA 内存结构。

我们知道,memmap 数据的作用是物理页框号 PFN 和 关联的 struct page 数据的相互转换,即 page_to_pfn()pfn_to_page() 操作,看一下 Linux 支持的各种内存模型下这两个宏的定义,就可以知道它们之间的差异:

// include/asm-generic/memory_model.h

#if defined(CONFIG_FLATMEM)

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

#elif defined(CONFIG_DISCONTIGMEM)

#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

#endif /* CONFIG_DISCONTIGMEM */

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

#define __pfn_to_page(pfn)	(mem_map + ((pfn) - ARCH_PFN_OFFSET)) /* 返回 页框号 @pfn 对应的 struct page */
/* 返回 struct page 对应的 物理页面页框号 */
#define __page_to_pfn(page)	((unsigned long)((page) - mem_map) + \
				 ARCH_PFN_OFFSET)
#elif defined(CONFIG_DISCONTIGMEM)

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

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

#elif defined(CONFIG_SPARSEMEM_VMEMMAP)

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

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

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

/*
 * Convert a physical address to a Page Frame Number and back
 */
#define	__phys_to_pfn(paddr)	PHYS_PFN(paddr)
#define	__pfn_to_phys(pfn)	PFN_PHYS(pfn) /* 页框号 ==> 物理地址 */

#define page_to_pfn __page_to_pfn /* struct page ==> 页框号 PFN */
#define pfn_to_page __pfn_to_page /* 页框号 PFN ==> struct page */

几种内存模型下,page_to_pfn()pfn_to_page() 的实现一目了然,这里展开下Sparse 内存模型下这两个宏,它们处于 CONFIG_SPARSEMEM 配置分支下。

先看下 page_to_pfn() -> __page_to_pfn() 转换 struct page 到 PFN 的过程:

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

#ifdef CONFIG_SPARSEMEM_EXTREME
#define SECTIONS_PER_ROOT       (PAGE_SIZE / sizeof (struct mem_section))
#else
#define SECTIONS_PER_ROOT	1
#endif

#define SECTION_NR_TO_ROOT(sec)	((sec) / SECTIONS_PER_ROOT) /* 计算 section 所在的 ROOT 编号 */
#define NR_SECTION_ROOTS	DIV_ROUND_UP(NR_MEM_SECTIONS, SECTIONS_PER_ROOT)
#define SECTION_ROOT_MASK	(SECTIONS_PER_ROOT - 1)

/* 获取编号为 @nr 的 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;
	return &mem_section[SECTION_NR_TO_ROOT(nr)][nr & SECTION_ROOT_MASK];
}

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

#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)));	\
})

再看下 pfn_to_page() -> __pfn_to_page() 转换 PFN 到 struct page 的过程(重复的代码部分见上面):

static inline unsigned long pfn_to_section_nr(unsigned long pfn)
{
	return pfn >> PFN_SECTION_SHIFT;
}

static inline struct mem_section *__pfn_to_section(unsigned long pfn)
{
	return __nr_to_section(pfn_to_section_nr(pfn));
}

#define __pfn_to_page(pfn)				\
({	unsigned long __pfn = (pfn);			\
	struct mem_section *__sec = __pfn_to_section(__pfn);	\
	__section_mem_map_addr(__sec) + __pfn;		\
})
posted @ 2026-03-21 17:19  JiMoKuangXiangQu  阅读(11)  评论(0)    收藏  举报