3.2.3 物理内存模型

前面提到,系统管理物理内存的基本单位是页(Page),物理内存被划分成一个个大小固定的页帧(Page Frame),每个页帧都有一个唯一的编号,叫着PFN(Page Frame Number); 内核中使用数据结构 page 来封装每个物理页帧的状态,因此物理页帧与 page 之间存在着1:1的关系。系统必须提供两者之间相互转换的方式,即系统可以通过PFN 拿到对应的 page, 也可以通过 page 得到对应的 PFN, 内核中提供了两个宏来完成该转换,分别是 page_to_pfnpfn_to_page 。如何组织内存页帧,使得这种转换能够高效便捷地实现,是内核设计者需要解决的问题。

我们将组织管理物理页帧的方式叫物理内存模型,内存模型的结构与物理内存本身的结构息息相关,本节我们将简要介绍一下内核物理内存模型的演进过程,以及每个内存模型所适用的场景。

3.2.3.1 FLATMEM

理想情况下,物理内存是一块地址连续的存储空间,这样物理页帧的PFN也是连续的,因此最简单直接地方式是将所有 page 放在一个一维数组中,每个 page 的索引就是对应物理页帧的PFN. 这种内存模型叫着平坦内存模型(Flat Memory Model),Linux早期使用的就是这种内存模型,所有的 page 保存在全局变量 mem_map 中,PFN 与 page 相互转换的逻辑实现也很直接,参考如下代码:

/* file: include/asm-generic/memory_model.h */

#if defined(CONFIG_FLATMEM)
#define __pfn_to_page(pfn) (mem_map + ((pfn)-ARCH_PFN_OFFSET))
#define __page_to_pfn(page) ((unsigned long)((page)-mem_map) + ARCH_PFN_OFFSET)
#endif

ARCH_PFN_OFFSET 是页帧号的起始偏移量,总体来说两个宏的逻辑都是基于 mem_map 做地址运算,效率非常高。

3.2.3.2 DISCONTIGMEM

FLATMEM很适合用来管理连续的物理内存,但对于内存不连续的情况就不太友好,如果物理内存存在大块的不连续区间,由于 mem_map 使用PFN作为 page 的索引,那么这些不连续区间对应的PFN就也会占用 mem_map 中的位置,形成空洞(hole),造成大量的内存空间浪费。另外就是在NUMA架构下,每个 Node 都有自己单独的内存区域,使用全局的变量来追踪所有的物理内存也不合理。

为了解决该问题,内核提供了新的内存模型叫着 DISCONTIGMEM, 意在消除内存空洞对 mem_map 的资源浪费。为了简化,内核在实现时仅根据 NUMA 的内存节点进行了划分,依然将每个 node 的内存看着是连续的,FLATMEM 中的全局变量 mem_map 变成了 pglist_data 中的一个变量:

/* file: include/linux/mmzone.h */

typedef struct pglist_data {
#ifdef CONFIG_FLAT_NODE_MEM_MAP /* means !SPARSEMEM */
    struct page *node_mem_map;
#endif

该模型实际上是 FLATMEM 的扩展,PFN与 page 之间的转换比FLATMEM多了一步:通过PFN或者 page 确定所在的node. 其余逻辑就与FLATMEM 一样了。具体代码如下:

/* file: include/asm-generic/memory_model.h */

#if defined(CONFIG_DISCONTIGMEM)

#define __pfn_to_page(pfn)                              \
    ({                                                  \
        unsigned long __pfn = (pfn);                    \
        unsigned long __nid = arch_pfn_to_nid(__pfn);   \
        NODE_DATA(__nid)->node_mem_map +                \
            arch_local_page_offset(__pfn, __nid);       \
    })

#define __page_to_pfn(pg)                                           \
    ({                                                              \
        const struct page *__pg = (pg);                             \
        struct pglist_data *__pgdat = NODE_DATA(page_to_nid(__pg)); \
        (unsigned long)(__pg - __pgdat->node_mem_map) +             \
            __pgdat->node_start_pfn;                                \
    })
#endif

3.2.3.3 SPARSEMEM

DISCONTIGMEM 的本意是应对非连续的物理内存,但其又将NUMA架构下的每个 node 看着是连续的,这其实并不合理,特别是在支持内存热插拔的系统当中。系统需要一种机制能够更灵活地管理粒度更小的连续内存区块,这种内存模型叫着 SPARSEMEM, 即稀疏内存模型。

SPARSEMEM模型的核心思想是将每个连续的内存块都单独管理,每个连续的内存块叫着一个 mem_section, 该数据结构中的字段 section_mem_map 指向连续的 page 对象,所有的 memsection 存放在一个全局的数组中,并且每个 mem_section 都可以在系统运行时改变 offline/online 状态,以便支持内存的热插拔(hotplug)功能。此时,PFN 与 page 之间的相互转换需要先找到对应的块,然后在通过 section_mem_map 属性进行查找。

很明显SPARSEMEM已经完全覆盖了前两个内存模型的所有功能,特别是可以完全替代不够灵活的DISCONTIGMEM.

3.2.3.4 Resources

这里我们仅简单讨论了一下物理内存模型的设计思想,想要深入研究的读者可以参考如下资料:

  • https://docs.kernel.org/vm/memory-model.html

  • https://lwn.net/Articles/789304/

  • http://www.wowotech.net/memory_management/memory_model.html

Last updated