Linux 内存管理:Sparse 内存模型简述
1. 前言
限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。
2. Sparse 内存模型
笔者在 Linux 内存管理 (4):buddy 管理系统的建立 分析了 不连续内存模型(Discontiguous Memory Model) 下最终建立内存管理数据图,如下:

本文分析 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):
对上图做一下简要解释,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; \
})

浙公网安备 33010602011771号