本文从 Linux 内核的内存管理关键的数据结构出发,结合内核源码中的注释,说明 Linux 的内存管理用到的数据结构的初始化流程。本文以 x86-64 架构为例,假设系统类型为 NUMA , sparse memory model 。

1. Memory Model

内存模型一部分内容主要来自网上Documentation 中没有找到相关的内容。

内存模型是从 CPU 的角度看,系统中物理内存的分布情况;在 Linux 内核中,使用何种方式来管理这些物理内存。

Linux 支持三种内存模型, flat memory , discontiguous memory 和 sparse memory 。

本文假设所有的CPU共享同一段物理地址空间。

1.1. Flat Memory Model

从系统的任意一个 CPU 来看,访问物理内存的时候,物理地址空间是连续的,没有空洞的地址空间,这种计算机系统的内存模型就是 flat memory model 。

这种情况下,节点数据 pg_data_t 只有一个,物理页框号和 struct page *mem_map 可以通过一个偏移量互相转化。将 mem_map 放在内存的直接映射区域,操作系统就不需要再为内存建立页表。

1
2
3
// 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)

1.2. Discontiguous Memory Model

如果物理内存的地址空间有空洞,这种内存模型就是 discontiguous memory model 。

这种情况下,节点数据 pg_data_t 有多个,每个节点管理的物理内存都保存在 pg_data_t 中的 node_mem_map ( 类似于flat模型中的 mem_map ) 成员中。从物理页框号转化为 struct page 需要先从 PFN 中得到节点 ID ,然后找到对应的 pg_data_t ,就可以像 flat 模型一样获得 struct page 数组。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 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;                   \
})

1.2.1. __pfn_to_page

x86 下,只有 32 位 ( 选择 32 位内核才会出现 flat memory model ) 系统 pfn_to_nid 有定义:

1
2
3
4
5
6
7
8
static inline int pfn_to_nid(unsigned long pfn)
{
#ifdef CONFIG_NUMA
    return((int) physnode_map[(pfn) / PAGES_PER_SECTION]);
#else
    return 0;
#endif
}

本文主要着眼于 64 位系统, 32 位的有关内容不再详细介绍。

1.3. Sparse Memory Model

sparse 模型用来解决内存的热插拔可能导致的内存节点内的 mem_map 不连续的问题。这种模型将连续的地址空间按照 section ( x86_64 NUMA 架构下为 128M )分段,每一个 section 都是 hotplug 的。

整个物理内存的地址空间通过指针 struct mem_section * 数组来描述,每个 mem_section * 指向一个 page , page 中包含若干个 struct mem_section 对象,每个对象描述一个 section 。

每一个 section 内部,内存地址都是连续的。因此, mem_map 的 page 数组依赖于 section 结构。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#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;      \
})

如果开启了 CONFIG_SPARSEMEM_VMEMMAP 选项 ( 默认开启 ) , PFN 和 struct page * 之间的转化十分简单:

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

1.3.1. __page_to_pfn

如果开启了 CONFIG_SPARSEMEM ,但是没有开启 CONFIG_SPARSEMEM_VMEMMAP ,就会开启 SECTION_IN_PAGE_FLAGS 选项,即在页表中包含 section 信息,据此实现 page_to_section

__nr_to_section 的实现也很简单,但是需要了解变量 mem_section 的定义:

1
2
3
4
5
6
7
8
#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
EXPORT_SYMBOL(mem_section);

取得 section 的 index 后,进行二维数组的访问操作即可获得对应的 section 结构体:

1
2
3
4
5
6
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];
}

从 section 中获取对应的 struct page * 的首地址后,用要查找的 struct page * 减去 section 的首地址,即可获得对应的 PFN 。

内核代码中有 __page_to_pfn 函数的注释如下:

setion’s mem_map is encoded to reflect its start_pfn.

section[i].section_mem_map = mem_map’s address = start_pfn.

1.3.2. __pfn_to_page

同理,由 PFN 可以得到所在的 section 的 index ,然后通过 __nr_to_section 获得 section ,再根据 section 中保存的 struct page * 的起始地址,获取 PFN 对应的 struct page *

1.3.3. VMEMMAP

如果开启 CONFIG_SPARSEMEM_VMEMMAP ,所有的 struct page * 都保存在连续的地址空间中,起始地址为 VMEMMAP_START ,x86架构定义在 arch/x86/include/asm/pgtable_64_types.h ,为 0xffffea0000000000UL 。

小结

内核的内存模型是为了描述物理内存,完成内存的物理页和 struct page* 之间的转换工作。

2. mem_section

使用 sparse 内存模型的 NUMA 系统,将所有的物理内存分成内存段,即 mem_section 。 mm/sparse.c 中定义的 struct mem_section *mem_section[NR_SECTION_ROOTS] 变量 ( x86 下默认开启 CONFIG_SPARSEMEM_EXTREME ) ,包含系统中所有的内存段。

根据定义可知, mem_section 变量是长度为 NR_SECTION_ROOTS 指针数组,而 NR_SECTION_ROOTS = 2K ,所以 mem_section 占用16B * 2K = 32KB的空间。

这个数组是静态的,无论对应的内存段是否存在。

mem_section 的初始化过程由 sparse_init 完成,函数的调用路径如下:

  • start_kernel ( init/main.c )
    • setup_arch ( arch/x86/kernel/setup.c )
      • x86_init.paging.pagetable_init = native_pagetable_init ( arch/x86/kernel/x86_init.c )
        • paging_init ( arch/x86/mm/init_64.c )
          • sparse_init ( arch/x86/mm/sparse.c )
          • zone_sizes_init ( arch/x86/mm/init.c ) 初始化 max_zone_pfns 数组,包含各个zone可以包含的最大的page数
            • free_area_init_nodes(max_zone_pfns) ( mm/page_alloc.c )
              • free_area_init_node ( mm/page_alloc.c )

这里先说明 arch/x86/mm/init_64.c 中的 paging_init 函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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();
}

paging_init 首先调用 sparse_memory_present_with_active_region 将系统内所有内存节点的物理页框通过 memroy_present 保存到 mem_section ,并且初始化 mem_section 数组的成员大小为 SECTION_PER_ROOT * sizeof(struct mem_section) ( CONFIG_SPARSEMEM_EXTREME 的情况 ) ;然后调用 sparse_init 重新设置 section_mem_map 成员;最后通过 zone_sizes_init 初始化内存区域。

需要说明的是, memory_presents 函数不但将节点包含的物理页框添加到 mem_section ,还会设置每个 mem_sectionsection_mem_map 成员为 ( 所属的节点ID « SECTION_NID_SHIFT | SECTION_MARKED_PRESENT)

2.1. sparse_init

sparse_init 主要设置 mem_sectionsection_mem_map 成员,将 sparse_memory_present_with_active_regions 函数保存的内容替换为对应的 PFN ,以便第一部分内存模型中介绍的 pfn_to_pagepage_to_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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
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();

    /*
     * 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 = 8B * 512K = 4MB 
     * 为每一个 section 分配一个指针所需的空间 
     */
    size = sizeof(unsigned long *) * NR_MEM_SECTIONS;

    /*
     * 上面这段注释的意思是说如果轮流分配 usemap 和 map 的内存
     * 会留下许多内存空洞。
     * memblock_virt_alloc 从 memblock 中分配内存空间
     */
    usemap_map = memblock_virt_alloc(size, 0);
    if (!usemap_map)
        panic("can not allocate usemap_map\n");

    /*sparse_early_usemaps_alloc_node从给定的*/
    alloc_usemap_and_memmap(sparse_early_usemaps_alloc_node,
                            (void *)usemap_map);

#ifdef CONFIG_SPARSEMEM_ALLOC_MEM_MAP_TOGETHER
    size2 = sizeof(struct page *) * NR_MEM_SECTIONS;
    map_map = memblock_virt_alloc(size2, 0);
    if (!map_map)
        panic("can not allocate map_map\n");
    alloc_usemap_and_memmap(sparse_early_mem_maps_alloc_node,
                            (void *)map_map);
#endif

    for (pnum = 0; pnum < NR_MEM_SECTIONS; pnum++) {
        if (!present_section_nr(pnum))
            continue;

        usemap = usemap_map[pnum];
        if (!usemap)
            continue;

#ifdef CONFIG_SPARSEMEM_ALLOC_MEM_MAP_TOGETHER
        map = map_map[pnum];
#else
        map = sparse_early_mem_map_alloc(pnum);
#endif
        if (!map)
            continue;

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

    vmemmap_populate_print_last();

#ifdef CONFIG_SPARSEMEM_ALLOC_MEM_MAP_TOGETHER
    memblock_free_early(__pa(map_map), size2);
#endif
    memblock_free_early(__pa(usemap_map), size);
}

sparse_init 函数的主要工作由 alloc_usemap_and_memmap 完成,后者负责遍历 mem_section 数组,实际的工作由参数 alloc_func 完成:

 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
54
55
56
57
static void __init alloc_usemap_and_memmap(void (*alloc_func)
                    (void *, unsigned long, unsigned long,
                    unsigned long, int), void *data)
{
    unsigned long pnum;
    unsigned long map_count;
    int nodeid_begin = 0;
    unsigned long pnum_begin = 0;

    /* 遍历 mem_section 数组,寻找第一个标记为 present 的 section */
    for (pnum = 0; pnum < NR_MEM_SECTIONS; pnum++) {
        struct mem_section *ms;

        /* 略过没有标记为 present 的 section */
        if (!present_section_nr(pnum))
            continue;
        /*
         * 找到了标记为 present 的 section ,
         * 根据 section 的 index 获取对应的 section 指针 
         */
        ms = __nr_to_section(pnum);
        nodeid_begin = sparse_early_nid(ms);
        pnum_begin = pnum;
        break;
    }
    map_count = 1;
    /* 
     * 从 present 的 section 开始,为属于同一个节点的所有 section
     * 调用 alloc_func 
     */
    for (pnum = pnum_begin + 1; pnum < NR_MEM_SECTIONS; pnum++) {
        struct mem_section *ms;
        int nodeid;

        // 跳过没有 present 的 section 
        if (!present_section_nr(pnum))
            continue;
        ms = __nr_to_section(pnum);
        nodeid = sparse_early_nid(ms);

        // 当前 section 和起始 section 属于相同的节点,增加 map count
        if (nodeid == nodeid_begin) {
            map_count++;
            continue;
        }
        /* ok, we need to take cake of from pnum_begin to pnum - 1*/
        alloc_func(data, pnum_begin, pnum,
                        map_count, nodeid_begin);
        /* new start, update count etc*/
        nodeid_begin = nodeid;
        pnum_begin = pnum;
        map_count = 1;
    }
    /* ok, last chunk */
    alloc_func(data, pnum_begin, NR_MEM_SECTIONS,
                        map_count, nodeid_begin);
}

sparse_init 函数先后两次调用 alloc_usemap_and_memmap 函数,传入的 alloc_func 分别为 sparse_early_usemaps_alloc_nodesparse_early_mem_maps_alloc_nodedata 分别为保存 unsigned long * 对象的 usemap_mapstruct page * 对象的 map_map

2.1.1. sparse_early_usemaps_alloc_node

分配函数为 sparse_early_usemaps_alloc_node 时, data 参数为长度为 NR_MEM_SECTIONSunsigned long * 数组 usemap_map

 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
static void __init sparse_early_usemaps_alloc_node(void *data,
                 unsigned long pnum_begin,
                 unsigned long pnum_end,
                 unsigned long usemap_count, int nodeid)
{
    void *usemap;
    unsigned long pnum;
    unsigned long **usemap_map = (unsigned long **)data;
    int size = usemap_size();
    /* 
     * 从指定节点的memblock中分配所需的内存空间,
     * usemap_count 为存在的 section 的数量 
     */
    usemap = sparse_early_usemaps_alloc_pgdat_section(NODE_DATA(nodeid),
                              size * usemap_count);
    if (!usemap) {
        printk(KERN_WARNING "%s: allocation failed\n", __func__);
        return;
    }

    for (pnum = pnum_begin; pnum < pnum_end; pnum++) {

        // 跳过不存在的 section
        if (!present_section_nr(pnum))
            continue;

        // 设置 section 对应的 usemap_map 数组元素指向分配的 usemap
        usemap_map[pnum] = usemap;
        usemap += size;
        check_usemap_section_nr(nodeid, usemap_map[pnum]);
    }
}

2.1.2. sparse_early_mem_maps_alloc_node

分配函数为 sparse_early_mem_maps_alloc_node 时, data 参数为长度为 NR_MEM_SECTIONSstruct page * 数组 map_map

1
2
3
4
5
6
7
8
9
static void __init sparse_early_mem_maps_alloc_node(void *data,
                 unsigned long pnum_begin,
                 unsigned long pnum_end,
                 unsigned long map_count, int nodeid)
{
    struct page **map_map = (struct page **)data;
    sparse_mem_maps_populate_node(map_map, pnum_begin, pnum_end,
                     map_count, nodeid);
}

和 SPARSEMEM 相关的还有一个配置项,即前面说到的 CONFIG_SPARSEMEM_VMEMMAP ,开启此选项,系统中所有的 struct page * 对象保存在连续的内存地址空间中。对应的, sparse_mem_maps_populate_node 函数有两个定义。

2.1.2.1. non-vmemmap

non-vmemmap情况下 sparse_mem_maps_populate_node 函数定义在 mm/sparse.c 中:

 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
54
55
56
57
58
59
60
61
62
63
64
65
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)
{
    void *map;
    unsigned long pnum;

    // size 为描述每个 section 包含的页框所需的内存大小 
    unsigned long size = sizeof(struct page) * PAGES_PER_SECTION;

    // x86 下alloc_remap 函数返回 NULL
    map = alloc_remap(nodeid, size * map_count);
    if (map) {
        for (pnum = pnum_begin; pnum < pnum_end; pnum++) {
            if (!present_section_nr(pnum))
                continue;
            map_map[pnum] = map;
            map += size;
        }
        return;
    }

    size = PAGE_ALIGN(size);
    /*
     * 从 memblock 中分配所需的内存,大小为描述节点内所有的
     * section 包含的页框所需的内存 
     */
    map = memblock_virt_alloc_try_nid(size * map_count,
                      PAGE_SIZE, __pa(MAX_DMA_ADDRESS),
                      BOOTMEM_ALLOC_ACCESSIBLE, nodeid);
    if (map) {
        for (pnum = pnum_begin; pnum < pnum_end; pnum++) {

            // 跳过不存在的section
            if (!present_section_nr(pnum))
                continue;

            /*
             * 将 map_map 中对应的元素指向该 section 包含的所有页框
             * 的 struct page 的地址,即 section 内第一个页面对应的
             * struct page 的地址
             */
            map_map[pnum] = map;
            map += size;
        }
        return;
    }

    /* fallback */
    // fallback只是再次执行上述相同的操作 
    for (pnum = pnum_begin; pnum < pnum_end; pnum++) {
        struct mem_section *ms;

        if (!present_section_nr(pnum))
            continue;
        map_map[pnum] = sparse_mem_map_populate(pnum, nodeid);
        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;
    }
}

可以看到, non-vmemmap 情况下,每次调用 sparse_mem_maps_populate_node 函数只是从 memblock 中分配所需的内存空间,分配的内存空间很有可能不连续。

2.1.2.2. vmemmap

配置 vmemmap 的情况下, sparse_mem_maps_populate_node 定义在 mm/sprase-vmemmap.c 中:

 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
54
55
56
57
58
59
60
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;
    unsigned long size = sizeof(struct page) * PAGES_PER_SECTION;
    void *vmemmap_buf_start;

    /* PMD_SIZE = 2MB,对齐到PMD_SIZE, 这里有个疑问,为什么
       要对齐到2MB */

    size = ALIGN(size, PMD_SIZE);

    // 从指定节点的 memblock 分配所需的内存空间
    vmemmap_buf_start = __earlyonly_bootmem_alloc(nodeid, size * map_count,
             PMD_SIZE, __pa(MAX_DMA_ADDRESS));

    /* 
     * 内存分配成功,保存起始地址和结束地址。
     * vmemmap_buf 用于分配下列操作中建立页表所需的内存空间,在
     * 页表建立完成后释放没有使用的缓冲区
     */
    if (vmemmap_buf_start) {
        vmemmap_buf = vmemmap_buf_start;
        vmemmap_buf_end = vmemmap_buf_start + size * map_count;
    }

    for (pnum = pnum_begin; pnum < pnum_end; pnum++) {
        struct mem_section *ms;

        // 跳过不存在的 section
        if (!present_section_nr(pnum))
            continue;

        /*
         * 为 section 中包含的所有页框对应的 struct page 建立页表。
         * 建立页表时会使用第一部分中定义的 pfn_to_page 宏,从而将
         * section 内的所有 pfn 对应的 struct page * 都保存在连续
         * 的虚拟地址空间中,并且返回 section 中首个 pfn 对应的
         * struct page *,保存在 map_map 中 
         */
        map_map[pnum] = sparse_mem_map_populate(pnum, nodeid);
        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;
    }

    // 释放没有用到的 vmemmap 缓冲区
    if (vmemmap_buf_start) {
        /* need to free left buf */
        memblock_free_early(__pa(vmemmap_buf),
                    vmemmap_buf_end - vmemmap_buf);
        vmemmap_buf = NULL;
        vmemmap_buf_end = NULL;
    }
}

至此,系统中内存段的初始化完成,可以通过 pfn_to_page 和 page_to_pfn 将 PFNstruct page * 相互转化。

3. 总结

memsection 初始化时,分配的内存都是从 memblock 中获取,后者是内核 boot 的早期阶段 slab 等内存分配器还没有初始化时使用的内存分配器。

memsection 是内存管理时经常使用的 struct page*PFN 之间相互转换的桥梁。