Linux内存管理:SPARSEMEM模型
背景介绍:
内存对于OS来说就像我们生活中的水和电,这么重要的资源管理起来是很花心思的。我们知道Linux中的物理内存被按页框划分,每个页框都会对应一个struct page结构体存放元数据,也就是说每块页框大小的内存都要花费sizeof(struct page)个字节进行管理。
所以系统会有大量的struct page,在linux的历史上出现过三种内存模型去管理它们。依次是平坦内存模型(flat memory model)、不连续内存模型 (discontiguous memory model)和稀疏内存模型(sparse memory model)。新的内存模型的一次次被提出,无非因为是老的内存模型已不适应计算机硬件的新技术(例如:NUMA技术、内存热插拔等)。
内存模型的设计则主要是权衡以下两点(空间与时间):
- 尽量少的消耗内存去管理众多的struct page
- pfn_to_page和page_to_pfn的转换效率。
Tips: 老惯例,文章基于ARM64、Linux5.0的内核展开叙述。
三种内存模型:
- FLATMEM (flat memory model)
FLATMEM内存模型是Linux最早使用的内存模型,那时计算机的内存通常不大。Linux会使用一个struct page mem_map[x]的数组根据PFN去依次存放所有的strcut page,且mem_map也位于内核空间的线性映射区,所以根据PFN(页帧号)即可轻松的找到目标页帧的strcut page
- DISCONTIGMEM (discontiguous memory model)
对于物理地址空间不存在空洞(holes)的计算机来说,FLATMEM无疑是最优解。可物理地址中若是存在空洞的话,FLATMEM就显得格外的浪费内存,因为FLATMEM会在mem_map数组中为所有的物理地址都创建一个struct page,即使大块的物理地址是空洞,即不存在物理内存。可是为这些空洞这些struct page完全是没有必要的。为了解决空洞的问题,Linux社区提出了DISCONTIGMEM模型。
DISCONTIGMEM是个稍纵即逝的内存模型,在SPARSEMEM出现后即被完全替代,且当前的Linux kernel默认都是使用SPARSEMEM,所以介绍DISCONTIGMEM的意义不大,感兴趣可以看这篇文章:https://lwn.net/Articles/789304/
- SPARSEMEM (sparse memory model)
稀疏内存模型是当前内核默认的选择,从2005年被提出后沿用至今,但中间经过几次优化,包括:CONFIG_SPARSEMEM_VMEMMAP和CONFIG_SPARSEMEM_EXTREME的引入,这两个配置通常是被打开的,下面的原理介绍也会基于它们开启的情况。
首次引入SPARSEMEM时的commit。https://lwn.net/Articles/134804/ 原文中阐明了它的三个优点:
- 可以解决内存空洞导致的内存浪费。
- 支持内存的热插拔(memory hotplug)。
- 支持nodes间的overlap。(我也不太清楚这是个啥...)
SPARSEMEM原理:
- section的概念:
SPARSEMEM内存模型引入了section的概念,可以简单将它理解为struct page的集合(数组)。内核使用struct mem_section去描述section,定义如下:
struct mem_section {
unsigned long section_mem_map;
/* See declaration of similar field in struct zone */
unsigned long *pageblock_flags;
};
其中的section_mem_map成员存放的是struct page数组的地址,每个section可容纳PFN_SECTION_SHIFT个struct page,arm64地址位宽为48bit时定义了每个section可囊括的地址范围是1GB。
- 全局变量mem_section
内核中用了一个二级指针struct mem_section **mem_section去管理section,我们可以简单理解为一个动态的二维数组。所谓二维即内核又将SECTIONS_PER_ROOT个section划分为一个ROOT,ROOT的个数不是固定的,根据系统实际的物理地址大小来分配。
- 物理页帧号PFN
SPARSEMEM将PFN差分成了三个level,每个level分别对应:ROOT编号、ROOT内的section偏移、section内的page偏移。(可以类比多级页表来理解)
vmemmap区域是一块起始地址是VMEMMAP_START,范围是2TB的虚拟地址区域,位于kernel space。以section为单位来存放strcut page结构的虚拟地址空间,然后线性映射到物理内存。(关于虚拟地址空间,参考文章:Linux内存管理:虚拟地址空间)
- 内存热插拔
SPARSEMEM中section是最小管理单元。内存热插拔也是以section为单位,下图中画的热插拔单位是一个section(ARM64是1GB),通常会大于等于一个section。
通过下图来可以很好的串联上面几个概念:
- PFN和struct page的转换:
SPARSEMEM中__pfn_to_page和__page_to_pfn的实现如下:
#define __pfn_to_page(pfn) (vmemmap + (pfn))
#define __page_to_pfn(page) (unsigned long)((page) - vmemmap)
#define vmemmap ((struct page *)VMEMMAP_START - (memstart_addr >> PAGE_SHIFT))
其中vmemmap指针指向VMEMMAP_START偏移memstart_addr的地址处,memstart_addr则是根据物理起始地址PHYS_OFFSET算出来的偏移,上图画出了三者之间的关系。
总结:
- 高效
通过上面__pfn_to_page的实现可以看出其仅用一步计算即可找到对应的struct page,十分高效,毫不逊色于flat memory model,很符合文章开始我们提到的高效的目标。
- 省内存
那另一个目标节省内存是如何做到的呢?答案就是按需分配,内存空洞或者被拔掉的内存条,我们不给它分配存放struct page的物理内存(只有虚拟内存)。
内存的多级页表也是利用了这个思想,通过将页表分成多级,未分配物理页帧的则不去为它建立PTE,上面将PFN拆成root、section是不是很像二级页表呢。
最新补充
sparse内存模型下struct page何时分配的呢,分两种情况 1.系统启动时上线的内存;2.系统启动后热插的内存。两种情况分配struct page的最终调用属兔同归,我们以dax kmem内存online作为第二种情况举例:
### DAX KMEM上线的调用路径
dev_dax_kmem_probe
add_memory_driver_managed
add_memory_resource //至此为memory hotplug的标准调用(详见该节). virtio mem也走这里
memblock_add_node //kenrel启动时从e820中创建memblock也是调用memblock_add函数实现
__try_online_node //分配一个numa node的内存结构pg_data_t *pgdat
hotadd_init_pgdat
free_area_init_core_hotplug //初始化pgdat的数据. 由于新分配的node中zone都是空的,后续online_pages会向zone添加页面
zone_init_internals
if(MHP_MEMMAP_ON_MEMORY)
1.此设备所需的struct page(即memmap)放在设备上 //参看cxl_dax_region_probe创建dax_dev时,默认设为了true
create_altmaps_and_memory_blocks
arch_add_memory
2.此设备所需的struct page(即memmap)放在系统内存上(若online内存太大,struct page可能将系统内存吃光)
arch_add_memory
### 创建struct page的调用路径
arch_add_memory
init_memory_mapping //先为即将热插的页面建立线性映射(起始PAGE_OFFSET)
add_pages //创建struct page
__add_pages
sparse_add_section
section_activate
populate_section_memmap
__populate_section_memmap //将pfn通过pfn_to_page转换成vmemmap中的虚拟地址,随后分配物理页和虚拟地址映射
vmemmap_populate
vmemmap_populate_basepages
vmemmap_populate_range
vmemmap_populate_address //将pfn对应的vmemmap虚拟地址建立映射关系
vmemmap_pgd_populate //若无vaddr对应的pgd页面则分配并映射
vmemmap_p4d_populate //若无vaddr对应的p4d页面则分配并映射
vmemmap_pud_populate //若无vaddr对应的pud页面则分配并映射
vmemmap_pmd_populate //若无vaddr对应的pmd页面则分配并映射
vmemmap_pte_populate //若无vaddr对应的pte页面则分配并映射,用映射struct page物理页面到pte中
vmemmap_alloc_block_buf
altmap_alloc_block_buf //若是将struct page存到设备内存上
vmemmap_alloc_block //若是将struct page存到系统内存上
set_pte_at
tips:代码路径中会有两种实现SPARSEMEM_VMEMMAP和SPARSEMEM,我们只看第一种,基于vmemmap的实现。而第二种相关的一些全局变
量sparsemap_buf和sparsemap_buf_end(虚拟地址范围)忽略即可。