# 3.2.3 物理内存模型

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

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

## 3.2.3.1 FLATMEM <a href="#orgdf37a45" id="orgdf37a45"></a>

理想情况下，物理内存是一块地址连续的存储空间，这样物理页帧的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 <a href="#org3c6fbec" id="org3c6fbec"></a>

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 <a href="#org8c32168" id="org8c32168"></a>

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>
