4. 内存分配策略基础:本地分配优先原则、first-touch策略、page allocator在NUMA下的工作流程、zone与node的关系
4.1 NUMA 内存分配的核心原则:本地分配优先
在 NUMA 架构下,内存访问延迟的核心矛盾在于:访问本地内存(Local Memory)的速度远快于访问远程内存(Remote Memory)。因此,Linux 内核的内存分配策略首要原则就是 本地分配优先(Local Allocation First)。
该原则的含义是:当一个 CPU 上的进程请求分配物理内存页时,内核会优先从该 CPU 所在的 NUMA 节点(Node)所关联的内存区域(Zone)中分配。只有当本地节点内存不足时,才会跨节点分配远程内存。
关键指标: 本地内存访问延迟通常为 100-200 ns,而跨节点(通过 QPI/UPI/CCIX 等互联)访问延迟可能高达 300-600 ns,甚至更高。带宽也可能下降 30%-50%。因此,违反本地分配原则会直接导致应用性能显著下降。
4.2 First-Touch 策略:决定内存页归属的关键
Linux 内核默认采用 First-Touch(首次接触) 策略来分配物理内存页。这意味着,虚拟内存地址的物理页框是在进程首次访问(读或写)该地址时才真正分配的,而不是在 malloc() 或 mmap() 调用时。
First-Touch 策略与 NUMA 本地分配原则紧密结合:
- 虚拟地址分配: 进程调用
malloc()时,内核仅在进程的虚拟地址空间中分配一段连续的虚拟地址(VMA),并不立即分配物理页。 - 缺页异常(Page Fault): 当进程的某个线程首次访问该虚拟地址时,触发缺页异常。
- 物理页分配: 内核的缺页异常处理程序会检查当前执行该访问的 CPU 所在的 NUMA 节点,并从该节点的 本地内存区域 分配一个物理页框。
- 页表映射: 将分配的物理页框映射到该虚拟地址。
实践启示: 在 NUMA 系统上,哪个 CPU 线程先访问数据,数据就分配在哪个 CPU 的本地内存上。因此,如果初始化阶段由一个线程(例如主线程)分配并填充数据,而后续由另一个 NUMA 节点上的工作线程处理数据,就会导致大量的远程内存访问。正确的做法是让工作线程自己初始化其将要处理的数据。
4.3 Zone 与 Node 的关系:内存层次结构
理解 NUMA 内存分配,必须厘清 Node 和 Zone 这两个核心概念及其关系。
| 概念 | 层级 | 含义 | NUMA 视角 |
|---|---|---|---|
| Node (节点) | 物理拓扑层 | 一个或多个 CPU 核心及其本地内存、IO 控制器的集合。每个 Node 有独立的 struct pglist_data 数据结构。 |
NUMA 架构的基本单元。跨 Node 访问即为远程访问。 |
| Zone (区域) | 内存属性层 | 根据物理内存的用途和地址范围划分的区域。常见 Zone 包括 ZONE_DMA、ZONE_DMA32、ZONE_NORMAL、ZONE_HIGHMEM、ZONE_MOVABLE。 |
每个 Node 内部都包含一组完整的 Zone。例如,Node 0 有自己的 ZONE_NORMAL,Node 1 也有自己的 ZONE_NORMAL。 |
关系总结: 一个物理系统包含多个 Node,每个 Node 包含多个 Zone。内存分配时,内核首先确定目标 Node(本地优先),然后在目标 Node 的各个 Zone 中按优先级(如 ZONE_NORMAL -> ZONE_DMA32 -> ZONE_DMA)尝试分配。
4.4 Page Allocator 在 NUMA 下的工作流程
Linux 内核的页分配器(Page Allocator)基于 伙伴系统(Buddy System),在 NUMA 环境下,其工作流程遵循严格的层级决策:
- 确定分配 Node: 根据当前进程的 CPU 亲和性(cpuset)和内存策略(mempolicy),确定首选 NUMA Node。默认策略是
MPOL_DEFAULT,即本地分配优先。 - 遍历 Zone 列表: 在选定的 Node 内,按照
ZONE_NORMAL→ZONE_DMA32→ZONE_DMA的顺序尝试分配。每个 Zone 内部使用伙伴系统查找空闲页块。 - 水位线检查(Watermark): 分配前检查 Zone 的空闲页数量是否高于
WMARK_LOW水位线。如果低于WMARK_LOW,内核会唤醒kswapd内核线程进行异步回收。 - 分配失败回退: 如果本地 Node 的所有 Zone 都无法满足分配请求(例如内存不足),则触发 跨节点回退(Fallback)。内核会查询
node_distance表,按距离从近到远依次尝试其他 Node。 - OOM Killer: 如果所有 Node 都无法分配,且分配请求不可等待,则触发 OOM Killer 终止进程以释放内存。
4.5 关键数据结构与代码路径
NUMA 内存分配的核心数据结构是 struct pglist_data,每个 Node 对应一个实例。其内部包含 struct zone 数组,管理该 Node 下的所有 Zone。
// 每个 NUMA 节点对应一个 pglist_data 结构
struct pglist_data {
struct zone node_zones[MAX_NR_ZONES]; // 该节点下的所有 Zone
struct zonelist node_zonelists[MAX_ZONELISTS]; // 备用 Zone 列表(用于 Fallback)
int nr_zones; // 该节点实际包含的 Zone 数量
unsigned long node_start_pfn; // 该节点起始物理页框号
unsigned long node_present_pages; // 该节点总物理页数
unsigned long node_spanned_pages; // 该节点总跨度页数
int node_id; // 节点 ID
// ... 其他字段
};
// 每个 Zone 管理一段连续物理内存
struct zone {
unsigned long watermark[NR_WMARK]; // 水位线: WMARK_MIN, WMARK_LOW, WMARK_HIGH
long lowmem_reserve[MAX_NR_ZONES]; // 为低内存 Zone 保留的内存
struct pglist_data *zone_pgdat; // 指向所属 Node
struct free_area free_area[MAX_ORDER]; // 伙伴系统的空闲页块链表
// ... 其他字段
};
性能陷阱: 当本地 Node 内存不足时,内核会通过
node_zonelists 中的备用列表进行跨节点分配。如果系统配置不当(例如 cpuset 限制过严),可能导致频繁的远程内存分配,造成严重的性能抖动。监控 /sys/devices/system/node/node*/numastat 中的 interleave_hit 和 other_node 指标可以诊断此类问题。
4.6 总结:策略、流程与数据结构对照
| 策略/概念 | 核心机制 | 数据结构 | 性能影响 |
|---|---|---|---|
| 本地分配优先 | 分配请求优先选择当前 CPU 所在 Node | current->mempolicy |
保证低延迟本地访问 |
| First-Touch | 物理页在首次访问时分配,分配 Node 由访问线程决定 | 缺页异常处理函数 do_anonymous_page() |
数据局部性由初始化线程决定 |
| Zone 遍历 | 按 ZONE_NORMAL → ZONE_DMA32 → ZONE_DMA 顺序尝试 |
struct zone 数组 |
确保优先使用通用内存 |
| 跨节点 Fallback | 本地失败后按距离顺序尝试其他 Node | node_zonelists |
引入远程访问延迟 |