Sparse内存模型的理解

解决什么问题?

内存管理以page为单位进行管理。 所以会把物理内存逻辑上切成很多的page。

而每个page 都需要有一个struct page的结构来管理这个page的使用情况。而这些struct page本身也是需要占用内存空间的。

如果物理内存是连续的还好,可以使用一个连续的struct page的数组来对它进行一一映射。 之前CONFIG_FLATMEM就是这么设计的。

但是现实的物理内存非常有可能是不连续的,如果还是完整分配一个数组来存储struct page。 这样一来存储这个数组本身就需要很大的物理内存,中间有些空洞的部分也需要分配struct page,这些分配是没有意义的。

这也就是为什么要引入Sparse内存模型。

怎么解决

两种方案:

  1. 继续使用线性地址来分配struct page的数组,对应配置 CONFIG_SPARSEMEM
  2. 使用连续的虚拟地址来分配struct page的数组,对应配置 CONFIG_SPARSEMEM + CONFIG_SPARSEMEM_VMEMMAP

上面两种方案有一个共性,如果系统的物理内存有空洞,那么对于这些空洞的物理内存,系统是不需要给这些物理内存对应的struct page的数组分配空间的,也就不需要给这些struct page的数组分配物理内存。 虚拟地址分配是无所谓的。

Linux中引入一个section的,在同一个section里面的物理内存(512M(64k page)/128M(4/16k page)),要么都给这些物理内存对应的struct page数组分配内存,要么全都不分配。

每个section都有一个状态,如果这个section是SECTION_MARKED_PRESENT状态,就需要给他们对应的struct page数组分配内存,否则就不需要。

这样那些不存在的物理内存就不需要分配对应的struct page。

如何组织和管理section

上面说的采用section的机制,就是类似于MMU页表的分级机制,MMU页表输入的是VA。 而这里输入的是PFN.

Page Frame Number(PFN)就是物理地址对应的页号。 因为系统里面的物理地址是唯一,所以这个PFN也是唯一的。

拿到一个PA,可以通过:

Page Frame Number (pfn) = PA >> PAGE_SHIFT

PAGE_SHIFT = CONFIG_PAGE_SHIFT 根据选择的page size 来决定。

这样就可以通过PA获取到 pfn。参考:

#define PFN_UP(x)       (((x) + PAGE_SIZE-1) >> PAGE_SHIFT)
#define PFN_DOWN(x)     ((x) >> PAGE_SHIFT)

有了PFN之后,这里就可以把PFN分成两段,高的字段是section number, 低字段是section 里面的偏移,类似于页表的机制。 在同一个section里面的物理内存,要不全都分配struct page,要不全都不分配struct page。

PFN可以通过如下方式来获取section number:

section number (sec) = PFN >> PFN_SECTION_SHIFT

参考:

static inline unsigned long pfn_to_section_nr(unsigned long pfn)
{
        return pfn >> PFN_SECTION_SHIFT;
}
#define PFN_SECTION_SHIFT       (SECTION_SIZE_BITS - PAGE_SHIFT)

SECTION_SIZE_BITS 的大小根据就是你section 的大小,4k的 page为128M,64k的 page为512M。

这里最大的section number是多少,就要分配多少的section 数组。这个数组的大小可以通过NR_MEM_SECTIONS来计算:

#define NR_MEM_SECTIONS         (1UL << SECTIONS_SHIFT)
#define SECTIONS_SHIFT      (MAX_PHYSMEM_BITS - SECTION_SIZE_BITS)
#define MAX_PHYSMEM_BITS        CONFIG_ARM64_PA_BITS

可以看出这个section 数组的大小根据PA的地址大小有关系。

如果这个NR_MEM_SECTIONS 这个数组比较小,是可以给它直接分配一个一维连续的数组就可以了。

举4k page size的例子,CONFIG_ARM64_PA_BITS 是48, 而SECTION_SIZE_BITS = 27 这样:

NR_MEM_SECTIONS = 1 << 21

也是一个非常大的数,如果这里直接分配这么大的section 数组,也是需要耗费巨大的内存。

为了解决这个section 大数组的问题,又加入一个root的概念,类似把页表多加了一级。 这也就是CONFIG_SPARSEMEM_EXTREME这个配置的来源,如果物理内存大小不是很大,可以考虑不开这个配置。

那一个root里面放多少section,跟MMU页表一样,每一级页表的内容都应该能够在一个页里面存储。 这里也一样,一个root里面的section数组也必须能够在一页里面放下,所以就有:

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

有了section 的number, 就可以计算出它对应的root,公式:

root = ((sec) / SECTIONS_PER_ROOT)

参考:

#define SECTION_NR_TO_ROOT(sec) ((sec) / SECTIONS_PER_ROOT)

这样就解决怎么通过PA找到对应的root和对应的section。 这样就可以定义:

#ifdef CONFIG_SPARSEMEM_EXTREME
extern struct mem_section **mem_section;
#else
extern struct mem_section mem_section[NR_SECTION_ROOTS][SECTIONS_PER_ROOT];
#endif

当配置了CONFIG_SPARSEMEM_EXTREME 之后,mem_section会首先分配NR_SECTION_ROOTS的一维指针,然后mem_section[i]只有等到它需要的时候才给他分配1个page的内存。

不需要就可以不分配,这样就解决了section数组占用空间过多的问题。

section数组的分配

先不关注struct mem_section的具体细节,前面有提到当CONFIG_SPARSEMEM_EXTREME使能时,这里就不再是静态分配了。

在函数sparse_init-> memblocks_present -> memory_present 会根据启动阶段已经解析的memblock信息来分配section数组。

  1. 第一步就是分配section root 相关的数组,系统需要多少个root,就分配多少struct mem_section * :
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;

                size = sizeof(struct mem_section *) * NR_SECTION_ROOTS;
                align = 1 << (INTERNODE_CACHE_SHIFT);
                mem_section = memblock_alloc(size, align);
                if (!mem_section)
                        panic("%s: Failed to allocate %lu bytes align=0x%lx\n",
                              __func__, size, align);
        }
#endif
  1. 第二步根据memblock的信息,找到它所对应的root,如mem_section[i],给它分配这个root里面SECTIONS_PER_ROOT个struct mem_section,即:

sparse_init-> memblocks_present -> memory_present -> sparse_index_init -> sparse_index_alloc

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

可以看出,当slab没有好的时候,就直接使用memblock来分配,并且返回的是线性地址。

物理地址如何索引struct page

前面说过拿到一个PA,可以很容易获取PFN。 这里先看一下section这个结构体:

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;

section_mem_map 这个值是struct page数组所在的位置起始地址,这里面还还有别的信息。这里先不管它。

这个起始地址可以是线性地址,也可以是vmemmap地址。 这个就回到前面说的两种管理方式。

struct page数组在哪里分配

在linux boot到sparse_init -> sparse_init_nid ,有代码:

static void __init sparse_init_nid(int nid, unsigned long pnum_begin,
                                   unsigned long pnum_end,
                                   unsigned long map_count)
{
        //..
        for_each_present_section_nr(pnum_begin, pnum) {

                //1. 这里负责分配 struct page 对应的数组
                map = __populate_section_memmap(pfn, PAGES_PER_SECTION,
                                nid, NULL, NULL);
                //...

                //2. 这里负责把这个struct page 对应的数组的基地址跟section中的section_mem_map建立关系
                sparse_init_one_section(__nr_to_section(pnum), pnum, map, usage,
                                SECTION_IS_EARLY);

        }
        //....
}

__populate_section_memmap 负责分配struct page 对应的数组的内存,就在这里就有两种分配的方式。

  • 线性地址

    先通过memblock给它分配物理地址,再通过phys_to_virt把它转成线性地址。 参考函数:memblock_alloc_internal

  • vmemmap地址

    当开启CONFIG_SPARSEMEM_VMEMMAP配置之后,先给它分配vmemmap里面的虚拟地址,然后再通过memblock给它分配物理地址,再把这个虚拟地址和物理地址在MMU页表里面给映射上。

    而这个vmemmap虚拟地址可以通过PFN来1:1 映射。 参考函数:

struct page * __meminit __populate_section_memmap(unsigned long pfn,
                unsigned long nr_pages, int nid, struct vmem_altmap *altmap,
                struct dev_pagemap *pgmap)
{
        //vmemmap里面找到对应的虚拟地址,这里是1:1映射的
        unsigned long start = (unsigned long) pfn_to_page(pfn);

        //这两个函数里面会给它分配真正的物理地址,在把这个vmemmap 给mapping 上
        if (vmemmap_can_optimize(altmap, pgmap))
                r = vmemmap_populate_compound_pages(pfn, start, end, nid, pgmap);
        else
                r = vmemmap_populate(start, end, nid, altmap);
        //返回的是vmemmap
        return pfn_to_page(pfn);
}

因为一个section里面对应的struct page数组是比较大的,可能会在MMU中会使用block mapping 的方式,所以这里也就是为什么有如下定义的原因,这下面这些size可以改,但是改了之后就不是最优了,就没办法利用的block 的mapping,以达到节省TLB的目的:

/*
 * Section size must be at least 512MB for 64K base
 * page size config. Otherwise it will be less than
 * MAX_PAGE_ORDER and the build process will fail.
 */
#ifdef CONFIG_ARM64_64K_PAGES
#define SECTION_SIZE_BITS 29

#else

/*
 * Section size must be at least 128MB for 4K base
 * page size config. Otherwise PMD based huge page
 * entries could not be created for vmemmap mappings.
 * 16K follows 4K for simplicity.
 */
#define SECTION_SIZE_BITS 27
#endif /* CONFIG_ARM64_64K_PAGES */

再来看下这些struct page 数组具体的分配细节,在sparse_init -> sparse_init_nid -> sparse_buffer_init 会预先给每个NUMA Node分配一个大的memblock内存,这个分配的memblock内存正好可以放下这个Node里面对应的所有struct page 数组。 这个内存的大小的计算方法:

size = 这个NUMA node下面 总的 section number * section_map_size()

#ifdef CONFIG_SPARSEMEM_VMEMMAP
static unsigned long __init section_map_size(void)
{
        return ALIGN(sizeof(struct page) * PAGES_PER_SECTION, PMD_SIZE);
}

#else
static unsigned long __init section_map_size(void)
{
        return PAGE_ALIGN(sizeof(struct page) * PAGES_PER_SECTION);
}

在section_map_size 里面的对齐就跟SECTION_SIZE_BITS = 27是有一些关系的。 所以说当CONFIG_SPARSEMEM_VMEMMAP打开时,这里会使用 blocking mapping。 而CONFIG_SPARSEMEM_VMEMMAP没有使能时,它最后使用的是线性地址,都是一个page一个page mapping的,所以也就是page 对齐就可以啦。

这里不管使用的是线性地址还是vmemmap地址分配struct page数组是,不管怎么样,都是需要物理内存来保存这些数据, 所以这部分内存都是要从memblock里面分配。

在这些分配好了之后,接下来就是怎么把这些分给一个一个section,让它的section_mem_map 指向对应的地方。

1. section_mem_map 为线性地址

在没有使能CONFIG_SPARSEMEM_VMEMMAP时,函数sparse_init -> sparse_init_nid -> __populate_section_memmap 会去到上面的分配的那个buffer 里面去分配这个section对应的struct page数组对应的内存,这个地址就是线性地址。

struct page __init *__populate_section_memmap(unsigned long pfn,
                unsigned long nr_pages, int nid, struct vmem_altmap *altmap,
                struct dev_pagemap *pgmap)
{
        unsigned long size = section_map_size();
        struct page *map = sparse_buffer_alloc(size);
        这里这个map就是对应一个section的struct page map的基地址
        //....

        return map;
}

获得了这些struct page数组的基地址之后,在 sparse_init_one_section -> sparse_encode_mem_map

section_mem_map = 这个struct page的基地值 减去这个section所映射的起始PFN。

static unsigned long sparse_encode_mem_map(struct page *mem_map, unsigned long pnum)
{
        unsigned long coded_mem_map =
                (unsigned long)(mem_map - (section_nr_to_pfn(pnum)));
        BUILD_BUG_ON(SECTION_MAP_LAST_BIT > PFN_SECTION_SHIFT);
        BUG_ON(coded_mem_map & ~SECTION_MAP_MASK);
        return coded_mem_map;
}

有了这些关系之后,当有一个PA即PFN的时候,就可以很轻松的找到它对应的struct page的地址,反过来也是一样

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

2. section_mem_map 为vmemmap地址

这种情况下是要打开配置CONFIG_SPARSEMEM_VMEMMAP, 为什么要加这个配置呢?

在不打开CONFIG_SPARSEMEM_VMEMMAP的情况下,从上面可以看出__page_to_pfn/__pfn_to_page 的转换是需要经过不少步骤,而这两个定义在kernel中是会非常的频繁调用。

为了优化上面的转换关系,Linux就添加了CONFIG_SPARSEMEM_VMEMMAP 这个配置,一般Arm 平台上是打开这个配置的。

section_mem_map 不再是线性地址,所以就需要一个vmemmap地址(VA)和struct page数组所在的物理地址绑定。

下面先看下vmemmap地址的产生办法:

  1. vmemmap 定义
#define vmemmap                 ((struct page *)VMEMMAP_START - (memstart_addr >> PAGE_SHIFT))

这里为什么要减去memstart_addr >> PAGE_SHIFT 是跟 PHYS_OFFSET 定义是一一对应的

/* PHYS_OFFSET - the physical address of the start of memory. */
#define PHYS_OFFSET             ({ VM_BUG_ON(memstart_addr & 1); memstart_addr; })

也就是说我们看到的线性地址和物理地址的对应是有一个OFFSET,即 VA:0xFFFF000000000000 --> PA:0x0000000080000000

>mmu memory-map
Virtual Range                              | Physical Range                           | Type         | AP | C     | S     | X
---------------------------------------------------------------------------------------------------------------------------------
EL2N:0x0000000000000000-0x0000FFFFFFFFFFFF | <unmapped>                               |              |    |       |       |
EL2N:0xFFFF000000000000-0xFFFF00000020FFFF | NP:0x0000000080000000-0x000000008020FFFF | Normal       | RW | True  | True  | False
EL2N:0xFFFF000000210000-0xFFFF000001D6FFFF | NP:0x0000000080210000-0x0000000081D6FFFF | Normal       | RW | True  | True  | False

所以vmemmap 也一样有个offset。

  1. PA 和struct page的对应关系
/* memmap is virtually contiguous.  */
#define __pfn_to_page(pfn)      (vmemmap + (pfn))
#define __page_to_pfn(page)     (unsigned long)((page) - vmemmap)

因为PA地址从0开始,正好vmemmap减去了memstart_addr所包含的的struct page,所以__pfn_to_page 就可以直接加PFN

在函数sparse_init -> sparse_init_nid -> __populate_section_memmap,这里会去调用对应的方法产生vmemmap的VA:

struct page * __meminit __populate_section_memmap(unsigned long pfn,
                unsigned long nr_pages, int nid, struct vmem_altmap *altmap,
                struct dev_pagemap *pgmap)
{
        // 这里就是这个section 所对应的vmemmap的地址
        unsigned long start = (unsigned long) pfn_to_page(pfn);
        unsigned long end = start + nr_pages * sizeof(struct page);
        //........

        // 这里就是这个section 所对应的vmemmap的地址
        return pfn_to_page(pfn);
}

接下来就要找到每个section 对应map的struct page数组的基地址,也就是PA,这个肯定要先在上面分配的那个大buffer里面去分配。 参考函数:

sparse_init -> sparse_init_nid -> __populate_section_memmap-> vmemmap_populate -> vmemmap_populate_hugepages -> vmemmap_alloc_block_buf

这里可以看出,如果使用的是4k page,就使用2M的页表进行mapping,这个上面已经提到过多次。 这里调用vmemmap_alloc_block_buf 分配出来的是线性地址,所以在把它写到页表之前还要转换成PA

void __meminit vmemmap_set_pmd(pmd_t *pmdp, void *p, int node,
                               unsigned long addr, unsigned long next)
{
        pmd_set_huge(pmdp, __pa(p), __pgprot(PROT_SECT_NORMAL));
}

在执行完这个之后,可以发现MMU页表里面多了不少L2 block mapping

+ 0xFFFFFDFFC0000000   | Level 1 Table  | NP:0x00000008FF6FA000 |                       | APTable=0x0, UXNTable=1, PXNTable=0
 - 0xFFFFFDFFC0000000  | Level 2 Block  |                       | NP:0x00000008FB600000 | UXN=1, PXN=1, Contiguous=0, DBM=1, GP=0, nG=0, AF=1, SH=0x3, AP=0x0, AttrIndx=0x0
 - 0xFFFFFDFFC0200000  | Level 2 Block  |                       | NP:0x00000008FB800000 | UXN=1, PXN=1, Contiguous=0, DBM=1, GP=0, nG=0, AF=1, SH=0x3, AP=0x0, AttrIndx=0x0
 - 0xFFFFFDFFC0400000  | Level 2 Block  |                       | NP:0x00000008FBA00000 | UXN=1, PXN=1, Contiguous=0, DBM=1, GP=0, nG=0, AF=1, SH=0x3, AP=0x0, AttrIndx=0x0
 - 0xFFFFFDFFC0600000  | Level 2 Block  |                       | NP:0x00000008FBC00000 | UXN=1, PXN=1, Contiguous=0, DBM=1, GP=0, nG=0, AF=1, SH=0x3, AP=0x0, AttrIndx=0x0
 - 0xFFFFFDFFC0800000  | Level 2 Block  |                       | NP:0x00000008FBE00000 | UXN=1, PXN=1, Contiguous=0, DBM=1, GP=0, nG=0, AF=1, SH=0x3, AP=0x0, AttrIndx=0x0

这样就完成了vmemmap地址到struct page 数组的PA之间的映射。接下来有一个PA就可以直接用 __pfn_to_page 找到它对应的 struct page了。

subsection

struct mem_section 有如下字段:

struct mem_section_usage {
        struct rcu_head rcu;
#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];
};

struct mem_section {
        //....
        struct mem_section_usage *usage;
        //....
}

在 Linux 内核中引入 subsection_map 的目的是为了提供更细粒度的内存管理,从而更有效地利用内存资源. 在某些系统中,特别是具有内存热插拔功能的服务器系统中,内存模块可能会在系统运行时被添加或移除。

一个section中,这里会给每个struct page 分配内存,但是是不是每个page 都对应有效的物理内存,这里还是不一定的。 这个就可以通过subsection_map,如果这个subsection_map的bit 设置了,那么对应这个子段的所有struct page都有对应的物理内存和他对应。

__add_pages -> sparse_add_section -> section_activate -> fill_subsection_map 设置这个bit

__remove_pages -> sparse_remove_section -> section_deactivate -> clear_subsection_map 清除这个bit

pfn_valid -> pfn_section_valid 这里会去check 这个page 在这个section中的subsection_map的bit 是不是设置了,设置了就时合法的page。否则认为这个page是不存在的。

memory Hotplug

Add 可以参考__add_pages -> sparse_add_section

int __meminit sparse_add_section(int nid, unsigned long start_pfn,
                unsigned long nr_pages, struct vmem_altmap *altmap,
                struct dev_pagemap *pgmap)
{
        unsigned long section_nr = pfn_to_section_nr(start_pfn);
        struct mem_section *ms;
        struct page *memmap;
        int ret;

        //先看root中section 存不存在,不存在给root指针分配空间
        ret = sparse_index_init(section_nr, nid);
        if (ret < 0)
                return ret;

        //获取线性地址或者vmemmap地址,并且把vmemmap地址和struct page的物理地址在MMU给映射上
        memmap = section_activate(nid, start_pfn, nr_pages, altmap, pgmap);
        if (IS_ERR(memmap))
                return PTR_ERR(memmap);
        /....
}

Remove 可以参考:

__remove_pages -> sparse_remove_section -> section_deactivate

比较大的一个区别可能就是之前的内存使用memblock分配,这里需要使用slab直接分配。

Comments !