内存管理-18-sparsemem内存模型-初探

一、简介

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() 的转换效率要尽量高。


二、三种内存模型

1. FLATMEM (flat memory model)

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

配置选项 CONFIG_FLATMEM,没有.c实现文件。

对于物理地址空间不存在空洞(holes)的计算机来说,FLATMEM 模型无疑是最优解。可物理地址中若是存在空洞的话,FLATMEM 就显得格外的浪费内存,#########因为 FLATMEM 不得不在 mem_map[] 数组中为所有的物理地址都创建一个 struct page,即使大块的物理地址是空洞,即不存在物理内存,也要创建。


2. DISCONTIGMEM (discontiguous memory model)

为了解决不连续物理内存带来的空洞问题,Linux社区提出了 DISCONTIGMEM 模型。

DISCONTIGMEM 模型引入了内存节点的概念,这仍然是 NUMA 内存管理的基础。每个节点都带有一个独立的内存管理子系统,它有自己的空闲页面列表、正在使用的页面列表、最近最少使用 (LRU) 信息和使用情况统计信息。在所有这些好东西中,由 struct pglist_data 表示的节点数据包含特定于节点的内存映射。假设每个节点都有连续的物理内存,每个节点都有一个页面结构数组可以解决平面内存映射中存在大漏洞的问题。

配置选项 CONFIG_DISCONTIGMEM,没有.c实现文件。

然而,DISCONTIGMEM 有一个弱点:内存热插拔和热移除。实际的 NUMA 节点粒度太粗,无法正确支持热插拔,而拆分节点会产生大量不必要的碎片和开销。请记住,每个节点都有一个独立的内存管理结构以及所有相关成本,进一步拆分节点会大大增加这些成本。

DISCONTIGMEM 稍纵即逝,倍后起之秀 SPARSEMEM 完全替代。


3. SPARSEMEM (sparse memory model)

SPARSEMEM 的引入解决了这一限制。该模型将内存映射抽象为由架构定义的任意大小的部分集合。每个部分由 struct mem_section 表示,里面包含指向 struct page 数组的指针。这​​些部分的数组表示一段物理内存,可以高效地以 SECTION_SIZE 粒度进行切割。为了在 PFN 和 struct page 之间进行高效地效转换,PFN 的几个高位用于索引部分数组。对于另一个方向,部分编号被编码在页面标志中。sparsemem内存模型支持存在空洞且支持内存的热插拔。

SPARSEMEM 将 PFN 拆分成了三个level,每个level分别对应:ROOT编号(二维数组下标)、ROOT内的section偏移(一维数组下标)、section内的page偏移。(可以类比多级页表来理解)

在 SPARSEMEM 引入 Linux 内核几个月后,被扩展为 SPARSEMEM_EXTREME,它适用于具有特别稀疏的物理地址空间的系统。SPARSEMEM_EXTREME 为 sections 数组添加了第二个维度(默认使能),变成一个二维数组。使用 SPARSEMEM_EXTREME 后,第一级变成指向 mem_section 结构数组的指针,并且根据实际物理内存动态分配。

2007 年,SPARSEMEM 又增加了一项增强功能,称为 "generic virtual memmap support for SPARSEMEM"(对SPARSEMEM 增加虚拟内存映射),配置宏为 SPARSEMEM_VMEMMAP。SPARSEMEM_VMEMMAP 背后的理念是,整个内存映射被映射到一个几乎连续的区域,但只有active部分才有物理页面支持。此模型不适用于 32 位系统,因为 32 位系统的物理内存大小可能接近甚至超过虚拟地址空间。但是,对于 64 位系统,SPARSEMEM_VMEMMAP 显然是一个优势。以额外的页表项为代价,page_to_pfn() 和 pfn_to_page() 变得与平面模型一样简单。

#if defined(CONFIG_FLATMEM) //没使能
    #define __pfn_to_page(pfn) ...
#elif defined(CONFIG_DISCONTIGMEM) //没使能
    #define __pfn_to_page(pfn) ...
#elif defined(CONFIG_SPARSEMEM_VMEMMAP) //默认使能,且优先级高
    #define __pfn_to_page(pfn) ...
#elif defined(CONFIG_SPARSEMEM) //默认使能
    #define __page_to_pfn(pg) ...
#endif

SPARSEMEM 内存模型的最后一个扩展是较新的(2016 年);它是由持久内存设备的使用增加所推动的。为了支持将内存映射直接存储在这些设备上而不是主内存中,虚拟内存映射可以使用 struct vmem_altmap,####### 它将在持久内存中提供页面结构。

2008 年,SPARSEMEM_VMEMMAP 成为 x86-64 唯一支持的内存模型,因为它仅比 FLATMEM 稍微重一点,但比 DISCONTIGMEM 更高效。最近的内存管理开发(例如内存热插拔、持久内存支持和各种性能优化)都针对 SPARSEMEM 模型。SPARSEMEM 模式也是较新Linux内核默认支持的一种内存模型。

SPARSEMEM 需要使能的配置宏和.c实现有:

CONFIG_SPARSEMEM_VMEMMAP
CONFIG_SPARSEMEM_VMEMMAP_ENABLE
CONFIG_SPARSEMEM
CONFIG_SPARSEMEM_MANUAL
CONFIG_SPARSEMEM_EXTREME
CONFIG_ARCH_SPARSEMEM_DEFAULT

msm-5.4/mm$ find ./ -name Makefile | xargs grep sparse
./Makefile:obj-$(CONFIG_SPARSEMEM)      += sparse.o
./Makefile:obj-$(CONFIG_SPARSEMEM_VMEMMAP) += sparse-vmemmap.o


三、数据结构

1. struct mem_section

SPARSEMEM 内存模型引入了section的概念,可以简单将它理解为 struct page 的集合(数组)。内核使用 struct mem_section 去描述section,定义如下:

//include/linux/mmzone.h

struct mem_section {
    unsigned long section_mem_map;
    struct mem_section_usage *usage;
#ifdef CONFIG_PAGE_EXTENSION
    struct page_ext *page_ext;
    unsigned long pad;
#endif
};

sparse_init_one_section() 中对其进行初始化。

section_mem_map: 存放的是 struct page 数组的地址,每个section可容纳 1<<PFN_SECTION_SHIFT 个 struct page. Arm64物理地址位宽为48bit时定义了每个section可囊括的地址范围是1GB(2^18个页帧,共1GB内存)。

usage:

page_ext:

pad:


2. struct mem_section_usage

struct mem_section_usage {
    DECLARE_BITMAP(subsection_map, SUBSECTIONS_PER_SECTION); //1<<9
    /* See declaration of similar field in struct zone */
    unsigned long pageblock_flags[0];
};


3. struct page_ext

struct page_ext {
    unsigned long flags;
};


四、全局变量

1. mem_section

内核中用 struct mem_section **mem_section 这个二级指针去管理section,我们可以简单理解为一个动态的二维数组。所谓二维即内核又将 SECTIONS_PER_ROOT 个section划分为一个ROOT,ROOT的个数不是固定的,根据系统实际的物理地址大小来分配。

2. section_to_node_table

static u8 section_to_node_table[NR_MEM_SECTIONS]; //1<<18  

由于没有定义 NODE_NOT_IN_PAGE_FLAGS,这是一个空实现。


五、vmemmap 区域

vmemmap 区域是一块起始地址是 VMEMMAP_START,范围是4GB的虚拟地址空间,位于kernel space。以section为单位来存放 strcut page 结构的虚拟地址空间,然后线性映射到物理内存。

//arch/arm64/include/asm/memory.h

#define VMEMMAP_START        (-VMEMMAP_SIZE - SZ_2M) //0xfffffffeffe00000
//(0xffffffc000000000 - 0xffffff8000000000) >> (12 - 6) = 0x100000000 = 4G
#define VMEMMAP_SIZE ((_PAGE_END(VA_BITS_MIN) - PAGE_OFFSET) >> (PAGE_SHIFT - STRUCT_PAGE_MAX_SHIFT))

//启动log打印:
vmemmap : 0xfffffffeffe00000 - 0xffffffffffe00000     (     4 GB maximum)

若是有16GB的物理内存,则需要 2^34 / 2^12 * 2^6 = 256MB 的空间存储page数据结构。

 

六、补充

1. SPARSMEM补充介绍

SPARSEMEM是Linux中最通用的内存模型,它是唯一支持若干高级功能的内存模型, 如物理内存的热插拔、非易失性内存设备的替代内存图和较大系统的内存图的延迟初始化。

SPARSEMEM模型将物理内存显示为多个部分的集合。每一个区段用 struct mem_section 结构体表示,它包含一个 section_mem_map 成员,从逻辑上讲,它是一个指向 struct page 阵列的指针。然而,它里面同样存储一些其他的magic,以帮助分区管理。

区段的大小 和 最大区段数 是使用 SECTION_SIZE_BITS 和 MAX_PHYSMEM_BITS 常量来指定的,这两个常量是由支持SPARSEMEM的架构定义的。 MAX_PHYSMEM_BITS 是一个架构所支持的物理地址的实际宽度,而 SECTION_SIZE_BITS 是一个用户可指定的值

最大的段数表示为 NR_MEM_SECTIONS,即 mem_section[][] (实际是二级指针)中能最大容纳的条目数,定义为:

  NR_MEM_SECTIONS = 2^(MAX_PHYSMEM_BITS - SECTION_SIZE_BITS) //=2^(48-30) = 2^18

mem_section[][] 二维数组的最大idx是 NR_SECTION_ROOTS,一维数组的最大idx是 SECTIONS_PER_ROOT。

SECTIONS_PER_ROOT 由一个页面最大能容纳多少个 struct section结构决定,NR_SECTION_ROOTS 则是 NR_MEM_SECTIONS/SECTIONS_PER_ROOT 向上圆整的结果。

#define SECTIONS_PER_ROOT    (PAGE_SIZE / sizeof(struct mem_section)) //2^12 / 2^5 = 2^7
#define NR_SECTION_ROOTS    DIV_ROUND_UP(NR_MEM_SECTIONS, SECTIONS_PER_ROOT) //1<<18 / 2^7 = 2^11

架构设置代码应该调用 sparse_init() 来初始化内存区和内存映射。

通过SPARSEMEM,有两种可能的方式将PFN转换为相应的 struct page – ”classic sparse” 和 “sparse vmemmap”。选择是在构建时进行的,它由 CONFIG_SPARSEMEM_VMEMMAP 的值决定。

(1) Classic sparse 在 page->flags 中编码了一个页面的段号,并使用PFN的高位来访问映射该页框的段。在一个区段内,PFN是指向页数组的索引。

(2) Sparse vmemmap 使用虚拟映射的内存映射来优化 pfn_to_page 和 page_to_pfn 操作。有一个全局的 struct page *vmemmap 指针,指向一个虚拟连续的 struct page 对象阵列。PFN是该数组的一个索引,struct page 从 vmemmap 的偏移量是该页的PFN。

为了使用vmemmap,架构必须保留一个虚拟地址空间,以映射包含内存映射的物理页,并确保 vmemmap 指向该范围。此外,架构应该实现 vmemmap_populate() 方法,它将分配物理内存并为虚拟内存映射创建页表。如果一个架构对 vmemmap 映射没有任何特殊要求, 它可以使用通用内存管理提供的默认 vmemmap_populate_basepages()。

虚拟映射的内存映射允许将持久性内存设备的 struct page 对象存储在这些设备上预先分 配的存储中。这种存储用 vmem_altmap 结构表示,最终通过一长串的函数调用传递给 vmemmap_populate()。vmemmap_populate()实现可以使用 vmem_altmap 和 vmemmap_alloc_block_buf() 助手来分配持久性内存设备上的内存映射。


推测:应该是二级数组大小是预先分配的,一级大小是动态分配的。二级指针还是与二维数组有区别的。然后将这些保存struct page的一维数组映射到连续的虚拟地址上。

 

 

 

参考:
Memory: the flat, the discontiguous, and the sparse:https://lwn.net/Articles/789304/

官方文档:https://dri.freedesktop.org/docs/drm/translations/zh_CN/vm/memory-model.html //还有一个 ZONE_DEVICE 模型

Documentation/vm/memory-model.rst

 

posted on 2024-07-12 22:47  Hello-World3  阅读(276)  评论(0编辑  收藏  举报

导航