K10 —— NUMA 系统:内存节点和本地分配
金句:在多路服务器上,”本地内存”和”远程内存”的访问延迟可以相差 3 倍——NUMA 就是这个差异的根源。
NUMA 的概念
NUMA(Non-Uniform Memory Access,非均匀内存访问)是指多路服务器中,每个 CPU 核心有自己”本地”的内存,访问本地内存比访问其他 CPU 的内存快。
NUMA 系统示例(2 路服务器):
CPU 0 ── 本地内存 64GB(节点 0) CPU 1 ── 本地内存 64GB(节点 1)
│ │
└── 访问延迟 60ns(本地) └── 访问延迟 60ns(本地)
└── 访问延迟 100ns(跨节点) └── 访问延迟 100ns(跨节点)
NUMA 的核心问题:
- 本地内存访问快(60ns)
- 远程内存访问慢(100ns)
- 如果分配策略不对,程序会变慢
UMA(Uniform Memory Access):
- 所有内存统一访问
- 多核通过总线访问同一个内存
- 访问延迟相同(但总线是瓶颈)NUMA 节点和内存布局
NUMA 节点(Node)的定义:
每个节点包含:
- 1~N 个 CPU 核心
- 本地内存(DRAM)
- 本地 PCI/IO 设备
Linux 用 struct pglist_data 表示 NUMA 节点:
typedef struct pglist_data {
int node_id; // 节点 ID(0, 1, ...)
unsigned long node_start_pfn; // 起始物理页号
unsigned long node_present_pages; // 节点总页数
pg_data_t *node_zones[MAX_NR_ZONES]; // zone(DMA/Normal/HighMem)
struct zoneref *node_zonelists; // 备用节点列表
// ...
} pg_data_t;
查看 NUMA 布局:
$ numactl --hardware
available: 2 nodes (0-1)
node 0 size: 65536 MB
node 1 size: 65536 MB
node 0 free: 12345 MB
node 1 free: 23456 MB
$ cat /proc/buddyinfo
Node 0, zone Normal free pages 12345/67890
Node 1, zone Normal free pages 23456/78901NUMA 感知分配
Linux 默认的 NUMA 分配策略:
1. First Touch(首次触碰):
- 页面在哪第一次访问,就在哪个节点分配
- 线程在 Node 0 访问,内存就分配在 Node 0
- 这是默认策略
2. interleaved(交错分配):
- 轮询在所有节点分配
- 用于避免单节点内存耗尽
3. membind(绑定到节点):
- 绑定到特定节点
- 强制在该节点分配
numactl 控制分配策略:
$ numactl --localalloc ./program
→ 本地分配(默认)
$ numactl --interleave=all ./program
→ 交错分配(所有节点均匀)
$ numactl --membind=0 ./program
→ 强制在 Node 0 分配
$ numactl --cpunodebind=0 --membind=0 ./program
→ 在 Node 0 的 CPU 上运行,内存也在 Node 0NUMA API:mbind 和 set_mempolicy
C 程序控制 NUMA 分配:
#include <numaif.h>
// 方法 1:mbind(单个内存范围)
struct bitmask *nodemask = numa_allocate_nodemask();
numa_bitmask_setbit(nodemask, 0); // 节点 0
mbind(addr, size, MPOL_BIND, nodemask->maskp, nodemask->size, 0);
numa_free_nodemask(nodemask);
// 方法 2:set_mempolicy(全局策略)
struct bitmask *nodemask = numa_allocate_nodemask();
numa_bitmask_setbit(nodemask, 0);
set_mempolicy(MPOL_BIND, nodemask->maskp, nodemask->size);
numa_free_nodemask(nodemask);
// 方法 3:alloc_pages(直接分配)
struct page *page = alloc_pages_node(node_id, GFP_KERNEL, order);
// 或者
alloc_pages(GFP_KERNEL, order); // 自动 first-touch
MPOL_* 策略:
MPOL_DEFAULT:系统默认(first-touch)
MPOL_BIND:严格绑定到指定节点
MPOL_INTERLEAVE:交错分配
MPOL_PREFERRED:优先节点,回退到其他NUMA 的内存回收和迁移
NUMA 内存回收的特点:
1. 每个节点独立的水位线:
- kswapd 运行在每个节点上
- 每个节点独立回收
2. 跨节点回收:
- 当本地节点内存紧张时
- 可以从远程节点回收(如果页面可迁移)
3. 内存迁移(migration):
- 把页面从节点 A 移动到节点 B
- move_pages(2) 系统调用
- 成本高(复制 + 更新页表)
迁移场景:
- 进程绑定了 CPU(cpuset)
- 但内存不够,需要迁移到另一个节点
- 或者 NUMA 平衡器(numa_balancing)自动迁移
numa_balancing(自动 NUMA 优化):
- 内核线程 numa_balancing
- 监控进程的 NUMA 访问模式
- 如果发现访问和分配不在同一节点,迁移页面
- 透明,不需要应用修改总结
- NUMA:Non-Uniform Memory Access,多路服务器中本地内存快,远程内存慢(60ns vs 100ns)
- 节点:每个 CPU + 本地内存 = 一个 NUMA 节点,用 struct pglist_data 表示
- First Touch:默认策略,页面在首次访问的节点分配
- numactl:–localalloc/–interleave=all/–membind=0 控制分配策略
- NUMA API:mbind / set_mempolicy / alloc_pages_node 控制分配
- NUMA 平衡器:numa_balancing 自动迁移页面到访问节点
K 层总结:K01~K10 覆盖了 SLAB/SLUB 分配器 → VMA/地址空间 → COW 复制 → OOM Killer → Page Cache → mmap/demand paging → malloc/brk/ptmalloc → shm共享内存 → 内存泄漏 → NUMA 的完整链路。
下篇预告(K11):buddy system 是怎么分配物理页的?alloc_pages 的完整路径,以及物理内存管理的边界情况。
关注公众号「AI不着急」,回复”资料”获取内存学习路线图。
评论