Fork me on GitHub
侧边栏

Linux内存管理:SPARSEMEM模型

背景介绍:

内存对于OS来说就像我们生活中的水和电,这么重要的资源管理起来是很花心思的。我们知道Linux中的物理内存被按页框划分,每个页框都会对应一个struct page结构体存放元数据,也就是说每块页框大小的内存都要花费sizeof(struct page)个字节进行管理。

所以系统会有大量的struct page,在linux的历史上出现过三种内存模型去管理它们。依次是平坦内存模型(flat memory model)、不连续内存模型 (discontiguous memory model)和稀疏内存模型(sparse memory model)。新的内存模型的一次次被提出,无非因为是老的内存模型已不适应计算机硬件的新技术(例如:NUMA技术内存热插拔等)。

内存模型的设计则主要是权衡以下两点(空间与时间):

  1. 尽量少的消耗内存去管理众多的struct page
  2. pfn_to_page和page_to_pfn的转换效率。

Tips: 老惯例,文章基于ARM64、Linux5.0的内核展开叙述。

三种内存模型:

FLATMEM内存模型是Linux最早使用的内存模型,那时计算机的内存通常不大。Linux会使用一个struct page mem_map[x]的数组根据PFN去依次存放所有的strcut page,且mem_map也位于内核空间的线性映射区,所以根据PFN(页帧号)即可轻松的找到目标页帧的strcut page

对于物理地址空间不存在空洞(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/ 原文中阐明了它的三个优点:

  1. 可以解决内存空洞导致的内存浪费。
  2. 支持内存的热插拔(memory hotplug)。
  3. 支持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。

通过下图来可以很好的串联上面几个概念:
image

  • 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(虚拟地址范围)忽略即可。
posted @ 2025-04-08 11:07  yooooooo  阅读(126)  评论(0)    收藏  举报