2. 物理内存管理基础:页框、内存节点与内存域

物理内存是 Linux 内核最核心的资源之一。内核必须高效地管理物理内存的分配与回收,同时要兼顾不同硬件架构(如 NUMA、UMA)和不同用途(如 DMA、普通映射、内核映射)的差异。本章深入剖析物理内存管理的三个基本抽象:页框(Page Frame)、内存节点(Node) 与 内存域(Zone),并介绍它们在内核中的数据结构与组织关系。

2.1 页框(Page Frame)——物理内存的最小单位

物理内存被划分为固定大小的连续块,称为页框(Page Frame)。每个页框对应一个物理页,其大小通常为 4KB(在 ARM64 或 x86_64 上可配置为 16KB 或 64KB)。内核使用 struct page 结构体来描述每一个页框。

关键概念: 页框是物理内存分配的最小粒度。Buddy 分配器以页框为单位进行分配和回收,Slab 分配器则从页框中进一步切分出更小的对象。

struct page 核心字段

字段 类型 说明
flags unsigned long 页框状态标志(如 PG_locked、PG_dirty、PG_swapbacked)
_refcount atomic_t 引用计数,决定页框是否可被回收
mapping struct address_space * 指向该页框所属的地址空间(文件页或匿名页)
lru struct list_head 用于 LRU 链表(活跃/不活跃页链表)
private unsigned long 私有数据,用于 slab 或文件系统元数据
slab_cache struct kmem_cache * 若页框被 slab 分配器管理,指向所属的 slab 缓存
提示: 每个物理页框都有一个 struct page 实例,所有实例存储在全局数组 mem_map 中。通过 virt_to_page() 或 pfn_to_page() 宏可以从虚拟地址或页框号(PFN)快速获取对应的 struct page。

2.2 内存节点(Node)——NUMA 架构下的顶层划分

在 NUMA(非统一内存访问)系统中,CPU 访问本地内存的速度远快于访问远程内存。内核将物理内存划分为多个内存节点(Node),每个节点对应一个 CPU 或一组 CPU 的本地内存。在 UMA(统一内存访问)系统中,只有一个内存节点。

每个内存节点由 struct pglist_data(简称 pg_data_t)描述,节点之间通过链表串联。

struct pglist_data 核心字段

字段 说明
node_zones[] 该节点包含的内存域数组(ZONE_DMA、ZONE_NORMAL、ZONE_HIGHMEM 等)
node_zonelists[] 备用内存域列表,用于跨域/跨节点分配时的回退顺序
nr_zones 该节点实际使用的内存域数量
node_start_pfn 该节点起始物理页框号
node_spanned_pages 该节点覆盖的物理页框总数(含空洞)
node_present_pages 该节点实际可用的物理页框数
pgdat_list 链表指针,将所有节点串联起来

2.3 内存域(Zone)——按用途划分的内存区域

每个内存节点内部,物理内存根据用途和地址范围进一步划分为多个内存域(Zone)。常见的内存域包括:

  • ZONE_DMA / ZONE_DMA32:用于 DMA 操作的物理内存,地址范围受限(如 x86 上 ZONE_DMA 为 0~16MB)。
  • ZONE_NORMAL:内核直接映射的物理内存,线性映射到内核虚拟地址空间。
  • ZONE_HIGHMEM:仅存在于 32 位系统,物理地址高于内核线性映射范围的区域,需要临时映射才能访问。
  • ZONE_MOVABLE:可迁移页的集合,用于内存热插拔和防碎片。
  • ZONE_DEVICE:用于持久内存或设备内存。

每个内存域由 struct zone 描述,其中包含 Buddy 分配器的核心数据结构——空闲链表数组(free_area[])。

struct zone 核心字段

字段 说明
free_area[MAX_ORDER] Buddy 分配器的空闲链表数组,每个元素管理 2^order 个连续页框
pageset[] 每 CPU 页框缓存,用于减少锁竞争
zone_start_pfn 该内存域起始页框号
spanned_pages 该内存域覆盖的页框总数
present_pages 该内存域实际可用的页框数
watermark[] 水位线(WMARK_MIN、WMARK_LOW、WMARK_HIGH),用于控制内存回收触发时机
zone_pgdat 指向所属的内存节点

2.4 三层结构关系图

下图展示了物理内存管理的三层抽象:Node → Zone → Page Frame,以及 Buddy 分配器在 Zone 层如何管理空闲页框。

物理内存三层结构:Node → Zone → Page Frame Node 0 (pglist_data) ZONE_DMA (0~16MB) free_area[0] → 4KB 空闲链表 free_area[1] → 8KB 空闲链表 ... ZONE_NORMAL (16MB~896MB) free_area[0] → 4KB 空闲链表 free_area[1] → 8KB 空闲链表 ... ZONE_HIGHMEM (>896MB) free_area[0] → 4KB 空闲链表 ... Node 1 (pglist_data) ZONE_DMA32 (0~4GB) free_area[0] → 4KB 空闲链表 free_area[1] → 8KB 空闲链表 ... ZONE_NORMAL free_area[0] → 4KB 空闲链表 free_area[1] → 8KB 空闲链表 ... ZONE_MOVABLE free_area[0] → 4KB 空闲链表 ... pgdat_list 每个 Zone 内部维护 free_area[MAX_ORDER] 数组,Buddy 分配器据此管理 2^order 连续页框

2.5 内存域水位线与分配回退路径

每个内存域维护三个水位线(Watermark),用于控制内存分配与回收的触发时机:

  • WMARK_MIN:最低水位。空闲内存低于此值时,分配器必须阻塞等待内存回收。
  • WMARK_LOW:低水位。空闲内存低于此值时,内核唤醒 kswapd 进行异步回收。
  • WMARK_HIGH:高水位。空闲内存高于此值时,kswapd 可以休眠。

当在指定内存域分配失败时,内核会按照 node_zonelists 中定义的备用顺序依次尝试其他内存域,甚至跨节点回退。回退策略分为两种:

策略 说明
ZONELIST_FALLBACK 优先在同一节点内回退到其他 Zone(如 ZONE_NORMAL → ZONE_DMA),再跨节点
NODE_FALLBACK 优先跨节点回退到相同类型的 Zone,再考虑同一节点内的其他 Zone
注意: 在 NUMA 系统中,跨节点分配会带来额外的内存访问延迟。内核通过 __GFP_THISNODE 标志强制分配器只在当前节点分配,避免性能下降。

2.6 页框分配与释放的完整路径

下图展示了从分配请求到最终获取物理页框的完整路径,涉及 Node、Zone、Buddy 三层协作。

页框分配路径:从 Node 到 Buddy 1. 分配请求 gfp_t 掩码 + order 2. 选择 Node 根据 CPU 所在节点 3. 选择 Zone 根据 gfp_zone() 4. 检查水位线 是否满足 WMARK_LOW? 5. Buddy 分配 __rmqueue() 6. 返回 struct page pfn_to_page() 回退路径 zonelist 回退 分配失败时,内核按 zonelist 顺序回退到其他 Zone 或 Node,直到成功或返回 NULL

2.7 关键数据结构关系总结

抽象层 内核数据结构 核心职责 关联分配器
页框 struct page 描述单个物理页框的状态、引用计数、映射关系 Buddy、Slab
内存域 struct zone 管理特定用途的内存区域,维护空闲链表和水位线 Buddy
内存节点 struct pglist_data 管理 NUMA 节点内的所有内存域,提供备用分配路径 Buddy
实践要点: 在调试内存分配问题时,可以通过 /proc/zoneinfo 查看每个 Zone 的空闲页框数和水位线;通过 /proc/buddyinfo 查看 Buddy 分配器的空闲链表分布;通过 numactl --hardware 查看 NUMA 节点拓扑。