内存管理篇——内存模型
由于暑期无事,为了巩固自己对整个 Linux 内核的了解,决定记录一下学习内容,或是阅读文档,或是阅读代码,或是调试代码,都放置于此,现在开始 Linux 的内存管理模块。
1 物理内存
1.1 抽象概念
Linux 需要一个架构无关的抽象来描述物理内存,我们将内存中一块儿一块儿的存储区域称为 node,在 linux 当中这些 node 通过一个结构体 struct pglist_data(https://elixir.bootlin.com/linux/v7.0.10/source/include/linux/mmzone.h#L1381) 来表示。
在不同的机器上,我们可以将内存分为两种架构,NUMA 和 UMA 是由硬件结构决定的,操作系统只是一个识别者:
- NUMA: 有多个 node,对应多个
pg_data_t。不同的 CPU 拥有独立的内存。 - UMA: 只有一个 node,对应一个
contig_page_data(https://elixir.bootlin.com/linux/v7.0.10/source/mm/memblock.c#L107)。所有 CPU 共享一个集中的内存。
计算机的 CPU 是一个拓扑结构,在 NUMA 中我们抽象出了一个叫 “distance” 的概念,其实他所描述的就是不同的处理器访问不同的内存所需要的代价(具体所谓的代价应该是硬件层面决定的,这里理解不太深刻,TODO)。


node 被用来表示一个一个的存储块,更具体的来说,我们用一个称为 zone 的区域来表示 node 内部各个区域的属性。这些区域的被结构体 struct zone 来表示。
我们可以查看本设备的物理内存信息。
1 | $ lscpu | grep NUMA |
这里我的电脑上只有一个 Node, 于是我们可以查看该 Node 的相关信息。
1 | $ cat /proc/zoneinfo |
1.2 内存模型
Linux 有三种内存模型,但是我们只能使用一种,这是在内核编译的时候就确定了的。他们分别是:
FLATMEM: 理想情况下,物理内存是一个地址连续的存储空间,这样 PFN 也是连续的。该模型的特点是适用于 UMA 的物理内存。在该模型当中,有一个全局的
mem_map(https://elixir.bootlin.com/linux/v7.0.10/source/mm/mm_init.c#L45) 数组来映射整个物理内存。这里
ARCH_PFN_OFFSET貌似是在不同的架构下可能物理内存的起始地址并不是第0号页帧(TODO,这里存疑),这里表示页帧号的起始偏移量。1
2
3
4
5
6/* file: include/asm-generic/memory_model.h */
DISCONTIGMEM: 当内存不连续的时候,FLATMEM 就不再方便管理了,上面提到的
mem_map数组使用 PFN 作为page的索引,当空间不连续的时候,该数组就会造成大量的 hole,另外对于我们的 NUMA 架构,每个 node 都存在自己的内存区域,使用全局的变量来追踪内存块也不太合理。在这个情况下,我们实际将全局的
mem_map放在了每个 node 的结构体当中作为一个字段,每个 node 自己管理自己的内存。因此我们在索引 PFN 和 page 的关系的时候,多了一个去查看 node 的步骤。1
2
3
4
5
6/* file: include/linux/mmzone.h */
typedef struct pglist_data {
struct page *node_mem_map;SPARSEMEM: 通用的内存模型,支持若干的高级功能,该模型将内存分为多个区段,每个连续的地址区段用
mem_section(https://elixir.bootlin.com/linux/v7.0.10/source/mm/sparse.c#L27)数组表示,每个 section 又被单独地管理起来。mem_section的结构体里面有一个section_mem_map字段,该字段指向连续的page对象。该模型是对 DISCONTIGMEM 的升级。ZONE_DEVICE: TODO
这里我们重点理解一下前三种模型。

2 NODE
我们的操作系统维护了一个 node 结构体的快速索引表,这个索引表主要是为了判别 node 的相关属性的。这个索引表是一个属性为 nodemask_t 的数组(https://elixir.bootlin.com/linux/v7.0.10/source/mm/page_alloc.c#L224)。
- N_POSSIBLE: 表示这个 node 理论上可能存在,或者未来可能被带上线。不代表它现在一定可用,只是说它在系统的可能拓扑里。
- N_ONLINE: 表示这个 node 当前已经在线、可用。这是“现在能不能参与系统运行”的状态。
- N_NORMAL_MEMORY: 表示这个 node 里有 normal memory,也就是常规可用的普通内存。
- N_HIGH_MEMORY: 表示这个 node 里有 normal memory 或 high memory。这个主要和老的 32 位高端内存模型有关。如果没开 CONFIG_HIGHMEM,它基本就等同于 N_NORMAL_MEMORY。
- N_MEMORY: 表示这个 node 里有 内存,范围更泛一些,包含:
- normal memory
- high memory
- movable memory
1 | # (base) jvle@jvle-ThinkPad-X1-Carbon-Gen-8 15:08:00> <~/.../kernel_study/mm> |
以上指令可以查看 node 掩码数组,这里的 0 表示 node0 在掩码当中。
内核代码一般如何使用 node?
1 | // 或者 for_each_node(nid) |
遍历所有在线 node,取到该 node 的 pg_data_t,对每个 node 做操作。
我们也可以通过 node_states 去访问。
1 | node_states[N_POSSIBLE] |
3 ZONE
zone 的类型有:
- ZONE_DMA: 表示一小段适合老旧或受限 DMA 设备使用的内存。这类设备不能访问全部物理地址,只能访问较低地址范围,所以内核专门留出这类 zone。
- ZONE_DMA32: 和 ZONE_DMA 类似,但通常表示 32 位 DMA 可寻址范围内 的内存。常见于 64 位平台上,某些设备虽然运行在 64 位系统里,但 DMA 仍只能打到 4GB 以内。
- ZONE_NORMAL: 最常规、最重要的普通内存区域。内核可以一直直接访问这部分内存,很多核心内存管理操作都依赖它,所以它通常是最关键的 zone。
- ZONE_HIGHMEM: 高端内存。主要出现在某些 32 位架构上,这部分物理内存没有永久映射到内核地址空间,内核要访问它时,需要临时建立映射。现代 64 位系统里一般不需要它。
- ZONE_MOVABLE: 可移动内存区域。这里的大多数页内容可以在不同物理页之间迁移,因此适合做页迁移、内存热插拔、减少碎片等场景。它看起来像普通内存,但更强调“内容可搬移”。
- ZONE_DEVICE: 设备内存区域。表示不属于普通 RAM,而是来自设备的内存,比如 PMEM、GPU memory。它存在的目的不是把这类内存当普通内存完全等价使用,而是让内核能为这些地址范围提供 struct page 和相关管理能力。
4 代码观察
4.1 NODE 和 ZONE 代码实例
以下给出一段内核代码来观察本机的内存情况。
1 | obj-m += pgdat_inspect.o |
1 |
|
最后安装到内核当中去观察情况。
1 | make |
如果只去看某个 node:
1 | sudo insmod pgdat_inspect.ko target_nid=0 |
如果想把空的 zone 也打出来。
1 | sudo insmod pgdat_inspect.ko show_empty_zones=1 |
不加任何参数,观察如下:
具体解释可以查看 https://www.kernel.org/doc/html/latest/translations/zh_CN/mm/physical_memory.html#id4。
1 | [112504.823310] pgdat_inspect: loading out-of-tree module taints kernel. |
4.2 内存模型实例
1 |
|
1 | KDIR := /lib/modules/$(shell uname -r)/build |
输出。
具体解释一下,这里的 pfn 指代的是页帧号,page 指代的是 struct page 的地址,phys 才是对应的物理地址。
1 | [195192.121075] memory_model: init |