从零到负一

【LM06】bootmem_init()和Linux的内存模型

2023/03/12

接上一篇笔记,在上一篇笔记【LM05】paging_init()以及内存的布局中,Linux内核已经完成了页表的初始化(memblock以及内核都完成了VA->PA的映射),swapper_pg_dir也取代了init_pg_dirmemblock中的内存空间也完成了映射。在这些完成后,接下来我们要实现一个功能,就是通过PFN能找到相应的page;同样,通过page也能找到对应的PFN

什么是PFN?简单来说就是Linux将管理的物理内存按照PAGE_SIZE的大小进行划分,这样就形成了一个又一个的物理页。从起始地址开始,对这些物理页进行编号,这个编号就是PFN。有了PFN,那么如何通过PFN找到对应的page呢?这就牵涉这篇笔记的一个重要的知识点 - Linux的内存模型。根据模型的不同,PFNpage间的转换也不同。

Linux的三种内存模型

我们先来看看Linux内核在哪里完成对内存模型的初始化,在setup_arch() -> bootmem_init()中,Linux完成了对内存模型的初始化。具体的代码我们等会儿再看,接下来先看看Linux的三种内存模型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// ./arch/arm64/mm/init.c

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

// memblock.memory.regions[0].base
min = PFN_UP(memblock_start_of_DRAM());
// idx = memblock.memory.cnt - 1;
// return (memblock.memory.regions[idx].base + memblock.memory.regions[idx].size);
max = PFN_DOWN(memblock_end_of_DRAM());

early_memtest(min << PAGE_SHIFT, max << PAGE_SHIFT);

max_pfn = max_low_pfn = max;

arm64_numa_init();
/*
* Sparsemem tries to allocate bootmem in memory_present(), so must be
* done after the fixed reservations.
*/
arm64_memory_present();

sparse_init();
zone_sizes_init(min, max);

memblock_dump_all();
}

这里我就直接引用网上的资料了,
1. 从pfn_to_page/page_to_pfn看linux SPARSEMEM内存模型 - 温暖的电波 - 博客园
2. RISC-V Linux SPARSEMEM 介绍与分析 - 泰晓科技
3. Memory: the flat, the discontiguous, and the sparse
目前Linux内核使用的是sparse memory模型,因此,我就直接从这种模型开始介绍。

Sparse Memory模型

我们先看经典的sparsemem模型再看增强型的sparsemem模型。通过对比,看看经典sparsemem的缺点以及如何克服这些缺点。
下面来看看一些常用的宏定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// ./arch/arm64/include/asm/sparsemem.h
// 这些宏定义看上去很复杂,我这里从大的方向的总结下这些宏定义
// 1. 首先我们需要定义物理内存有多大,也就是 [MAX_PHYSMEM_BITS] = 48,
// 2. 然后我们需要定义一个section有多大,也就是 [SECTION_SIZE_BITS] = 30
// 3. 根据这两个我们就知道整个内存空间可以有多少个section,48 - 30 = 18, 2 ^ 18
// 4. 同时,根据页的大小(12bit),我们也可以知道一个section有多少物理页,30 - 12, 2 ^ 18
// 5. 因为用二维数组(经典模型相当于一维数组)来保存mem_section的信息,因此我们需要知道
// 5.1 第一个维度,我们用页来保存mem_section的信息,因此需要知道一个页可以放多少mem_section
// 5.2 第二个维度,在第一个维度的基础下,我们需要知道一共需要多少页,这就是[NR_SECTION_ROOTS]
// 有了上面这些信息,基本就能搞懂这些宏的定义了

#ifdef CONFIG_SPARSEMEM
// MAX_PHYSMEM_BITS = 48
#define MAX_PHYSMEM_BITS CONFIG_ARM64_PA_BITS
// 1GB
#define SECTION_SIZE_BITS 30
#endif

// ./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

#define SECTION_NR_TO_ROOT(sec) ((sec) / SECTIONS_PER_ROOT)
#define NR_SECTION_ROOTS DIV_ROUND_UP(NR_MEM_SECTIONS, SECTIONS_PER_ROOT)
#define SECTION_ROOT_MASK (SECTIONS_PER_ROOT - 1)

#define PA_SECTION_SHIFT (SECTION_SIZE_BITS)
#define PFN_SECTION_SHIFT (SECTION_SIZE_BITS - PAGE_SHIFT)
#define SECTIONS_SHIFT (MAX_PHYSMEM_BITS - SECTION_SIZE_BITS) = 48 - 30 = 18
#define NR_MEM_SECTIONS (1UL << SECTIONS_SHIFT)
#define PAGES_PER_SECTION (1UL << PFN_SECTION_SHIFT)
#define PAGE_SECTION_MASK (~(PAGES_PER_SECTION-1))

// ./mm/sparse.c

#ifdef CONFIG_SPARSEMEM_EXTREME
struct mem_section **mem_section;
#else
// 等价于mem_section[NR_MEM_SECTIONS][1]
struct mem_section mem_section[NR_SECTION_ROOTS][SECTIONS_PER_ROOT]
____cacheline_internodealigned_in_smp;
#endif

下面我引用LoyenWang的图做一个补充,通过这幅图可以更方便地理解上面的宏定义,

mem_section结构体

这个结构体最重要的就是section_mem_map,它除了存储section对应的page外(参考上图 page[0]),还会用于保存部分flag。因为section_mem_mappage对齐的,因此最后12bit都是0,因此可以用来存储这些flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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;
#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.
*/
};

下面我们进入bootmem_init()内部看看它做了什么。

arm64_memory_present()

首先我们要做的就是将memblock中的内存空间都添加到section中并标记成present

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// ./arch/arm64/mm/init.c

#ifndef CONFIG_SPARSEMEM
static void __init arm64_memory_present(void)
{
}
#else
static void __init arm64_memory_present(void)
{
struct memblock_region *reg;

// ---------------------------------------------------------------------- (1)
for_each_memblock(memory, reg) {
int nid = memblock_get_region_node(reg);
// memblock saves physical address - address in reg is physical address
// ------------------------------------------------------------------ (2)
memory_present(nid, memblock_region_memory_base_pfn(reg), memblock_region_memory_end_pfn(reg));
}
}
#endif

(1) 遍历所有memblock中的memory区域;
(2) 将每个memory区域添加入section并标记成present

memory_present()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// ./mm/sparse.c

/* Record a memory area against a node. */
void __init memory_present(int nid,
unsigned long start, // 开始的PFN
unsigned long end) // 结束的PFN
{
unsigned long pfn;

// CONFIG_SPARSEMEM_EXTREME = 0
#ifdef CONFIG_SPARSEMEM_EXTREME
if (unlikely(!mem_section)) {
unsigned long size, align;
size = sizeof(struct mem_section*) * NR_SECTION_ROOTS;
align = 1 << (INTERNODE_CACHE_SHIFT);
mem_section = memblock_alloc(size, align);
}
#endif

// #define PAGE_SECTION_MASK (~(PAGES_PER_SECTION-1))
// ---------------------------------------------------------- (1)
start &= PAGE_SECTION_MASK;

// Validate the physical addressing limitations of the model
// ---------------------------------------------------------- (2)
mminit_validate_memmodel_limits(&start, &end);
for (pfn = start; pfn < end; pfn += PAGES_PER_SECTION) {
// ------------------------------------------------------ (3)
// pfn >> PFN_SECTION_SHIFT
// 用连续的pfn / section的数量 = section的索引,nr可以理解为mem_section中的索引
unsigned long section = pfn_to_section_nr(pfn);
struct mem_section *ms;

// ------------------------------------------------------ (4)
sparse_index_init(section, nid);
set_section_nid(section, nid);

// ------------------------------------------------------ (5)
// 通过section这个索引在mem_section中找到对应的mem_section *
ms = __nr_to_section(section);
if (!ms->section_mem_map) {
// -------------------------------------------------- (6)
ms->section_mem_map = sparse_encode_early_nid(nid) | SECTION_IS_ONLINE;
section_mark_present(ms);
}
}
}

(1) 确保start是从section对应的第一个页开始;
(2) 查看startend是否超过max_sparsemem_pfn
(3) 参考注释;
(4) 在经典sparsemem中该函数为空;
(5) 获取section中的mem_section的地址,源码如下;

1
2
3
4
5
6
7
8
9
10
11
12
13
// ./include/linux/mmzone.h

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;
// 在经典的sparsemem中等于&mem_section[nr][1]
return &mem_section[SECTION_NR_TO_ROOT(nr)][nr & SECTION_ROOT_MASK];
}

(6) 将flag嵌入section_mem_map

1
2
3
4
5
6
7
8
9
(nid << SECTION_NID_SHIFT) | SECTION_IS_ONLINE | SECTION_MARKED_PRESENT

#define SECTION_MARKED_PRESENT (1UL<<0)
#define SECTION_IS_ONLINE (1UL<<2)
#define SECTION_NID_SHIFT 3

#define SECTION_MAP_LAST_BIT (1UL<<3)
// flag在[0, 2], [3, X]是nid, MASK是X000最低3位
#define SECTION_MAP_MASK (~(SECTION_MAP_LAST_BIT-1))

至此,所有memblockmemory区域的内存空间都加入section并被标记为present了。

sparse_init_nid()

这个函数除了对sparse section进行初始化外,还初始化了usemap。目前我还不清楚这部分是干什么的,并且这篇笔记主要还是记录sparsemem,因此这里我只介绍sparsemem相关内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// ./mm/sparse.c

/*
* Initialize sparse on a specific node. The node spans [pnum_begin, pnum_end)
* And number of present sections in this node is map_count.
*/
static void __init sparse_init_nid(int nid, unsigned long pnum_begin,
unsigned long pnum_end,
unsigned long map_count)
{
unsigned long pnum, usemap_longs, *usemap;
struct page *map;

usemap_longs = BITS_TO_LONGS(SECTION_BLOCKFLAGS_BITS);
usemap = sparse_early_usemaps_alloc_pgdat_section(NODE_DATA(nid),
usemap_size() *
map_count);
if (!usemap) {
pr_err("%s: node[%d] usemap allocation failed", __func__, nid);
goto failed;
}
// ------------------------------------------------------------- (1)
// section_map_size() = PAGE_ALIGN(sizeof(struct page) * PAGES_PER_SECTION)
sparse_buffer_init(map_count * section_map_size(), nid);
for_each_present_section_nr(pnum_begin, pnum) {
if (pnum >= pnum_end)
break;
// --------------------------------------------------------- (2)
map = sparse_mem_map_populate(pnum, nid, NULL);
if (!map) {
pr_err("%s: node[%d] memory map backing failed. Some memory will not be available.", __func__, nid);
pnum_begin = pnum;
goto failed;
}
check_usemap_section_nr(nid, usemap);
// --------------------------------------------------------- (3)
sparse_init_one_section(__nr_to_section(pnum), pnum, map, usemap);
usemap += usemap_longs;
}
// ------------------------------------------------------------- (4)
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;
}
}

(1) sparse_buffer_*()函数主要用于分配、释放管理section的内存空间(也就是上图中包含page[x]的部分)。(1) 处通过memblock分配map_count * section_map_size()大小的内存空间,注意,这里计算的是所有section需要的内存空间。关于这几个函数,参考下面源码的注释。

sparse_buffer_*()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// ./mm/sparse.c

// sparsemap_buf 和 sparsemap_buf_end是全局变量,这个函数确定它们的起始地址
static void __init sparse_buffer_init(unsigned long size, int nid)
{
WARN_ON(sparsemap_buf); /* forgot to call sparse_buffer_fini()? */
sparsemap_buf = memblock_alloc_try_nid_raw(size, PAGE_SIZE,
__pa(MAX_DMA_ADDRESS),
MEMBLOCK_ALLOC_ACCESSIBLE, nid);
sparsemap_buf_end = sparsemap_buf + size;
}

// 这个函数用于更新sparsemap_buf的地址
void * __meminit sparse_buffer_alloc(unsigned long size)
{
void *ptr = NULL;

if (sparsemap_buf) {
ptr = PTR_ALIGN(sparsemap_buf, size);
if (ptr + size > sparsemap_buf_end)
ptr = NULL;
else
sparsemap_buf = ptr + size;
}
return ptr;
}

// 这个函数释放多余的空间,如果sparsemap_buf还是比sparsemap_buf_end小,那么就
// 释放这部分内存空间
static void __init sparse_buffer_fini(void)
{
unsigned long size = sparsemap_buf_end - sparsemap_buf;

if (sparsemap_buf && size > 0)
memblock_free_early(__pa(sparsemap_buf), size);
sparsemap_buf = NULL;
}

(2) 在(1)中已经分配的内存空间中,获取当前section需要的section_map_size大小的空间;
(3) 这个函数很简单,但很巧妙,它将各种东西嵌入到section_mem_map中,下面我们来看看它的源码;

sparse_init_one_section()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
static void __meminit sparse_init_one_section(struct mem_section *ms,
unsigned long pnum, struct page *mem_map,
unsigned long *pageblock_bitmap)
{
// 清空除了最低3位标志位的所有位
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;
}

/*
* 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.
*/

// 将mem_map - (section_nr_to_pfn(pnum)嵌入section_mem_map中,它们两个都保证了不会
// 将标志位覆盖,参考如下,include/linux/mmzone.h
/*
* We use the lower bits of the mem_map pointer to store
* a little bit of information. The pointer is calculated
* as mem_map - section_nr_to_pfn(pnum). The result is
* aligned to the minimum alignment of the two values:
* 1. All mem_map arrays are page-aligned.
* 2. section_nr_to_pfn() always clears PFN_SECTION_SHIFT
* lowest bits. PFN_SECTION_SHIFT is arch-specific
* (equal SECTION_SIZE_BITS - PAGE_SHIFT), and the
* worst combination is powerpc with 256k pages,
* which results in PFN_SECTION_SHIFT equal 6.
* To sum it up, at least 6 bits are available.
*/

// 这样嵌入是为了方便的进行PFN和物理页之间的映射,之后会具体分析
static unsigned long sparse_encode_mem_map(struct page *mem_map, unsigned long pnum)
{
// 这里的mem_map是section对应的page连续空间的首地址, 也就是第一个元素的地址
// section_nr_to_pfn(pnum)获取的是section中第一个物理页的PFN
unsigned long coded_mem_map = (unsigned long)(mem_map - (section_nr_to_pfn(pnum)));
BUILD_BUG_ON(SECTION_MAP_LAST_BIT > (1UL<<PFN_SECTION_SHIFT));
BUG_ON(coded_mem_map & ~SECTION_MAP_MASK);
return coded_mem_map;
}

(4) 参考(1);
至此,这部分映射就建立完成了,接下来我就开始分析PFNpage之间的相互映射了。

经典SPARSEMEM模型PFN和物理页间相互映射

__pfn_to_page()

我们先来看PFN到物理页的映射,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// ./include/asm-generic/memory_model.h

#define __pfn_to_page(pfn) \
({ unsigned long __pfn = (pfn); \
struct mem_section *__sec = __pfn_to_section(__pfn); \
// ---------------------------------------------------------------- (1)
// [struct page[X+N]]
// ...
// [struct page[X+2]]
// [struct page[X+1]]
// [struct page[X+0]] <--- mem_map(struct page *类型指针), 对应PFN = section_nr_to_pfn(pnum)
//
// 这里要好好思考如何实现的,__section_mem_map_addr(__sec)获取的是mem_map - (section_nr_to_pfn(pnum))的地址,并且是(struct page *)类型.
// mem_map - (section_nr_to_pfn(pnum)) + __pfn就相当于mem_map数组前移(section_nr_to_pfn(pnum))再后移__pfn.
// 为什么这样正确?因为mem_map对应的section在绝大多数的情况下都不是从PFN = 0开始的, 而是从(section_nr_to_pfn(pnum))开始的
// 因此通过减去(section_nr_to_pfn(pnum))我们才能获得该mem_map的索引.
__section_mem_map_addr(__sec) + __pfn; \
})

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

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

__page_to_pfn()

1
2
3
4
5
6
7
8
#define __page_to_pfn(pg)                    \
({ \
const struct page *__pg = (pg); \
int __sec = page_to_section(__pg); \
// --------------------------------------------------------------------- (1)
// 这里相当于 __pg - (mem_map - (section_nr_to_pfn(pnum))),同样需要将PFN的偏移考虑进去,结果就显而易见了
(unsigned long)(__pg - __section_mem_map_addr(__nr_to_section(__sec))); \
})

增强SPARSEMEM模型

增强型sparsemem模型对经典sparsemem进行了一次优化。对于经典模型,我们需要静态分配一个二维数组,在64bit系统中,这个数组可能会很大(并且其实根本用不完)。因此,使用静态分配数组的方法是不可取的。在增强模型中,Linux内核使用动态数组的方式,只给实际存在的section分配map空间,因此可以省去很多内存空间。下面来看看哪些地方做了修改。

mem_section的定义

1
2
3
4
5
6
7
8
9
// ./mm/sparse.c

#ifdef CONFIG_SPARSEMEM_EXTREME
struct mem_section **mem_section;
#else
// 等价于mem_section[NR_MEM_SECTIONS][1]
struct mem_section mem_section[NR_SECTION_ROOTS][SECTIONS_PER_ROOT]
____cacheline_internodealigned_in_smp;
#endif

memory_present()

这个函数对mem_section进行第一个维度的分配空间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// ./mm/sparse.c

/* Record a memory area against a node. */
void __init memory_present(int nid,
unsigned long start, // 开始的PFN
unsigned long end) // 结束的PFN
{
unsigned long pfn;

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

// NR_SECTION_ROOTS = NR_MEM_SECTIONS / ((PAGE_SIZE / sizeof(struct mem_section)))
// 用所有的section除以每个page能放多少个mem_section得到需要多少个page,每个page的mem_section对应一个struct mem_section *
size = sizeof(struct mem_section *) * NR_SECTION_ROOTS; //
align = 1 << (INTERNODE_CACHE_SHIFT);
mem_section = memblock_alloc(size, align);
}
#endif

sparse_index_init()

这个函数对每个实际存在的section分配第二个维度的空间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// ./mm/sparse.c

static noinline struct mem_section __ref *sparse_index_alloc(int nid)
{
struct mem_section *section = NULL;

// 其实这里就是分配一个page
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);

return section;
}

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;

if (mem_section[root])
return -EEXIST;

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

mem_section[root] = section;

return 0;
}
#else /* !SPARSEMEM_EXTREME */
static inline int sparse_index_init(unsigned long section_nr, int nid)
{
return 0;
}
#endif

__pfn_to_page()和__page_to_pfn()

这部分和经典模型一样,唯一的区别就是一个通过二维数组,一个通过动态数组。通过PFN或者struct page *找到mem_section的过程不同,但接下来的过程就是一样的了。

关于PFN和物理页映射的思考

  1. 到目前为止,mem_map中的page都没有初始化,只是一个固定的值;
  2. PFN通过__pfn_to_page()是都可以找到mem_map中的page的,但这个page可能并不存在物理页与其对应 - 参考 系统中的物理页框在Linux内核中都有struct page与之对应么?
  3. 同样,page也都可以通过__page_to_pfn()找到PFN
  4. pfn_valid()可用于判断PFN是否有效,如果需要,在__pfn_to_page()前调用;

至此,PFN和物理联系起来了,下一步,我们就要看看如何分配这些内存空间了。在这之前,我们还需要先看看bootmem_init()接下来要完成的事情 -zone_sizes_init()。同时,这篇笔记还留下一部分没有完成,就是vmemmap相关的映射,这部分以后有机会再回来补充吧。

参考资料

  1. 【原创】(四)Linux内存模型之Sparse Memory Model
  2. RISC-V Linux SPARSEMEM 介绍与分析
  3. 从pfn_to_page/page_to_pfn看linux SPARSEMEM内存模型
  4. 系统中的物理页框在Linux内核中都有struct page与之对应么?
CATALOG
  1. 1. Linux的三种内存模型
  2. 2. Sparse Memory模型
    1. 2.1. mem_section结构体
    2. 2.2. arm64_memory_present()
      1. 2.2.1. memory_present()
    3. 2.3. sparse_init_nid()
      1. 2.3.1. sparse_buffer_*()
      2. 2.3.2. sparse_init_one_section()
  3. 3. 经典SPARSEMEM模型PFN和物理页间相互映射
    1. 3.1. __pfn_to_page()
    2. 3.2. __page_to_pfn()
  4. 4. 增强SPARSEMEM模型
    1. 4.1. mem_section的定义
    2. 4.2. memory_present()
    3. 4.3. sparse_index_init()
    4. 4.4. __pfn_to_page()和__page_to_pfn()
  5. 5. 关于PFN和物理页映射的思考
  6. 6. 参考资料