解决什么问题?
内存管理以page为单位进行管理。 所以会把物理内存逻辑上切成很多的page。
而每个page 都需要有一个struct page的结构来管理这个page的使用情况。而这些struct page本身也是需要占用内存空间的。
如果物理内存是连续的还好,可以使用一个连续的struct page的数组来对它进行一一映射。 之前CONFIG_FLATMEM就是这么设计的。
但是现实的物理内存非常有可能是不连续的,如果还是完整分配一个数组来存储struct page。 这样一来存储这个数组本身就需要很大的物理内存,中间有些空洞的部分也需要分配struct page,这些分配是没有意义的。
这也就是为什么要引入Sparse内存模型。
怎么解决
两种方案:
- 继续使用线性地址来分配struct page的数组,对应配置 CONFIG_SPARSEMEM
- 使用连续的虚拟地址来分配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数组。
- 第一步就是分配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
- 第二步根据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地址的产生办法:
- 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。
- 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 !