NUMA-02 一段内存到底在哪个 node:用户态 NUMA 编程接口 NUMA你的内存不是一整块看懂 NUMA 与机器拓扑 学会了“看”拓扑但只会看还不够。真正要解决“prefetch 回 CPU 该落哪个 node”你得会用代码去查一段内存现在在哪、再把它搬到你想要的 node。0. 这一章要解决的三件事在用户态使用 NUMA核心就三件事给一段内存指定 node查一段内存当前在哪个 node把它迁到另一个 node这三件事分别对应set_mempolicy/mbind、move_pages查询模式、move_pages/migrate_pages迁移模式。本章把它们逐个讲清最后用一个可编译运行的小实验把它们串起来。用户态的 NUMA 接口分两层底层是内核系统调用numaif.h里声明上层是 libnuma 封装numa.h更好用。关系如下你的程序有些封装也直接 syscalllibnuma 高层 APInuma.hnuma_alloc_onnode / numa_move_pages ...原始系统调用numaif.hmbind / set_mempolicy / move_pages / migrate_pages内核 mm/mempolicy.c、mm/migrate.c一句话能用 libnuma 就用 libnuma跨平台、少踩坑但要精确控制、或看懂内核在做什么得认识底层那几个 syscall。1. 系统调用层四个核心 syscall这些声明在numaif.h链接时需要-lnuma。1.1 set_mempolicy设定“本线程之后的分配”落哪个 node#includenumaif.hlongset_mempolicy(intmode,constunsignedlong*nodemask,unsignedlongmaxnode);它设置的是调用线程的默认内存策略影响的是之后发生的分配记住第 01 章的 first-touch真正分配发生在首次写入时。mode常用三种mode含义典型用途MPOL_DEFAULT恢复默认first-touch落在触碰它的 CPU 所在 node复位策略MPOL_BIND强制只在nodemask指定的 node 上分配不够就 OOM严格本地化MPOL_PREFERRED优先在指定 node 分配不够可退到别处软亲和MPOL_INTERLEAVE在nodemask的多个 node 上轮流分配带宽敏感、想摊平1.2 mbind只对“某一段地址”设定策略set_mempolicy是“线程级”的mbind是“地址段级”的——只作用于[addr, addrlen)这段虚拟地址#includenumaif.hlongmbind(void*addr,unsignedlonglen,intmode,constunsignedlong*nodemask,unsignedlongmaxnode,unsignedflags);关键在flags0只改策略不迁移已存在的页只对之后新分配生效。MPOL_MF_MOVE连同这段里已经分配的页一起迁到目标 node本进程私有页。MPOL_MF_MOVE_ALL连共享页也迁需要权限。MPOL_MF_STRICT若有页无法满足策略就报错。所以“把一段已经分配的内存搬到 node k”一种写法就是mbind(addr, len, MPOL_BIND, {k}, ..., MPOL_MF_MOVE)。1.3 move_pages本章主角——逐页“查询”或“迁移”move_pages是最贴合我们目标的一个因为它能按页粒度既查询又迁移#includenumaif.hlongmove_pages(intpid,unsignedlongcount,void**pages,// 每页的起始地址数组constint*nodes,// 目标 node 数组传 NULL 只查询不迁移int*status,// 输出每页当前所在 node或负的错误码intflags);// 通常 MPOL_MF_MOVE两种用法查询模式nodes NULL内核把每页当前所在 node 填进status[i]。这就是验证“页到底迁到哪个 node 了”的标准手段。迁移模式nodes ! NULL把pages[i]迁到nodes[i]结果/错误码写回status[i]。move_pages的查询模式是最直接的验收工具迁移完成后逐页读status就能确认页是否真落在了期望的 node 上。1.4 migrate_pages把整个进程从一组 node 挪到另一组#includenumaif.hlongmigrate_pages(intpid,unsignedlongmaxnode,constunsignedlong*old_nodes,constunsignedlong*new_nodes);它是“批发”操作把进程里所有位于old_nodes的页尽量迁到new_nodes按位置一一对应。适合“把某个进程整体从 node0 赶到 node1”这类运维场景粒度粗不适合逐页精确控制。四者对照接口粒度查询迁移适用set_mempolicy线程后续分配否否只定策略设默认落点mbind地址段否可带 MOVE flag段级绑定迁移move_pages单页是是精确查询/迁移、验收migrate_pages整进程否是粗粒度整体搬迁2. libnuma 层更顺手的封装numa.h-lnuma把上面的能力包成了更好用的函数。常用的一把#includenuma.hintnuma_available(void);// 0 表示内核不支持 NUMA必须先查intnuma_max_node(void);// 最大 node 编号node 数 该值 1intnuma_node_of_cpu(intcpu);// 某个 CPU 属于哪个 node —— GPU 邻近 node 反推常用void*numa_alloc_onnode(size_tsize,intnode);// 直接在指定 node 上分配void*numa_alloc_interleaved(size_tsize);// 跨所有 node interleave 分配voidnuma_free(void*start,size_tsize);longnuma_move_pages(...);// move_pages 的封装voidnuma_set_preferred(intnode);// set_mempolicy(MPOL_PREFERRED,...)libnuma vs 原始 syscall 怎么选只是想“在 node k 上分配一块内存做实验” →numa_alloc_onnode一行搞定。要精确验收“这页现在在哪” → 直接move_pages查询模式语义最透明。写进产品代码、要跨发行版 → libnuma 更稳。3. 内存策略详解BIND / PREFERRED / INTERLEAVE这三种 mode 是最常被混淆的用一张图区分“node0 内存不够时会怎样”在 node0 上申请内存但 node0 已满MPOL_BIND(node0)不许退让 → 触发回收/OOMMPOL_PREFERRED(node0)优先 node0满了退到 node1MPOL_INTERLEAVE(node0,1)本就轮流分不存在必须 node0MPOL_BIND硬绑定。要么给你 node0要么宁可回收/杀进程也不放到别的 node。适合“绝对不能远程”的极致本地化但有 OOM 风险。MPOL_PREFERRED软亲和。先试 node0实在不行退而求其次。大多数“希望本地但别把程序搞崩”的场景选它。MPOL_INTERLEAVE把页轮流撒在多个 node 上牺牲单点本地性换总带宽。适合一块大内存被所有 node 均匀访问的负载如某些 HPC 全局数组。把设备页迁回 CPU 时最贴切的语义其实类似PREFERRED(目标 node)—— 首选那个 node真放不下也别让迁移失败。4. GPU 与 CPU 的“距离”是怎么判断出来的前面反复出现一个前提一块 GPU 有一个“最近的 CPU node”。这个“最近”不是感觉而是内核已经量化好、放在 sysfs 里的数字。这一节我们关注这个问题——距离从哪来、用什么单位、怎么读是后面一切“选对 node”的地基。4.1 距离从哪来“GPU 离哪个 CPU node 近”这个结论内核已经帮我们算好、直接放在 sysfs 里来源位置距离以什么形式表达GPU → 最近 CPU node/sys/bus/pci/devices/BDF/numa_node直接给出 GPU 所属即最近CPU node idCPU 之间/sys/devices/system/node/nodeX/distanceNUMA distance 矩阵本地10、远端更大numa_node是 ACPI 固件_PXM方法在启动时算好的“GPU 属于哪个 proximity domain即哪个 CPU node”。值得一提的是GPU 驱动如 KFD自己维护的拓扑底层也是基于同一套 PCIe / ACPI 亲和信息构建的与这里的numa_node同源、指向同一个 CPU node——所以直接读numa_node就够了。4.2 NUMA distance 的单位为什么本地是 10Linux 用一个距离矩阵描述 node 两两之间的相对代价约定本地固定为10即 1.0 的十倍表示法远端按倍数放大——相邻常见~12–16跨 socket 常见20–32。本机是单 node矩阵退化成一个数$ cat /sys/devices/system/node/node0/distance 10一台双路 NPS1 的 EPYC 上会是10 32本 socket 10、对端 socket 32NPS4 则会出现10 12 12 12 32 ...这类同 socket 内 node 间略大、跨 socket 更大的矩阵。这个矩阵就是“把页迁到哪个 node 更近”的量化依据。4.3 判断流程值 k (0)值 -1单 node/无亲和一块 GPU已知 PCI BDF读 PCI numa_node最近 CPU node k回退默认策略得到目标 CPU node再用 node distance 矩阵比较各 node 远近做决策4.4 单 node 机器上的退化在只有一个 CPU node 的机器上PCInuma_node读出-1无可区分的亲和distance 矩阵只有一个10。两种来源一致地表示“无处可选就是 node0”——这正是理解多 node 场景的对照基准多 node 时正是这些数字开始出现差异才有了“选哪个更近”的问题。5. 动手实验分配 → 查询 → 迁移 → 验证下面这个程序把本章的接口串成一条完整链路分配一块内存并触碰它、用move_pages查询它现在在哪个 node、若有多 node迁到目标 node、再查一次确认。它在单 node 机器上也能跑——只是查询结果始终是 node0正好印证“单 node 无处可迁”。5.1 代码创建numa_demo.c// numa_demo.c —— 分配 → 查询 → 迁移 → 验证// 编译: gcc numa_demo.c -o numa_demo -lnuma#includenuma.h#includenumaif.h#includestdio.h#includestdlib.h#includestring.h#includeunistd.hintmain(intargc,char**argv){if(numa_available()0){fprintf(stderr,内核不支持 NUMA\n);return1;}intmax_nodenuma_max_node();printf(系统 node 数: %d (node 0..%d)\n,max_node1,max_node);// 1) 分配一页并 first-touch让它真正落到物理内存size_tpagesysconf(_SC_PAGESIZE);void*pnuma_alloc_onnode(page,0);// 先放到 node0if(!p){perror(numa_alloc_onnode);return1;}memset(p,1,page);// 触碰 真正分配// 2) 查询模式nodes 传 NULLstatus 回填当前所在 nodevoid*pages[1]{p};intstatus[1]{-1};if(move_pages(0,1,pages,NULL,status,0)!0)perror(move_pages(query));printf(迁移前该页在 node %d\n,status[0]);// 3) 若有第二个 node迁过去inttarget(max_node1)?1:0;if(target!0){intnodes[1]{target};status[0]-1;if(move_pages(0,1,pages,nodes,status,MPOL_MF_MOVE)!0)perror(move_pages(migrate));printf(请求迁到 node %dmove_pages 返回 status%d\n,target,status[0]);// 4) 再查一次验证status[0]-1;move_pages(0,1,pages,NULL,status,0);printf(迁移后该页在 node %d\n,status[0]);}else{printf(只有一个 node无处可迁单 node 机器的正常现象。\n);}numa_free(p,page);return0;}5.2 编译与运行gcc numa_demo.c-onuma_demo-lnuma./numa_demo在单 node 机器上预期输出类似系统 node 数: 1 (node 0..0) 迁移前该页在 node 0 只有一个 node无处可迁单 node 机器的正常现象。在一台多 node 的 EPYC 服务器上跑同样的程序你会看到“迁移前node 0 → 请求迁到 node 1 → 迁移后node 1”move_pages的查询模式把迁移落点清清楚楚地验证出来。6. 三种能力把本章接口按“做什么”归成三类能力方便按需取用能力用户态对应内核侧最终落到决定“分配/迁到哪个 node”set_mempolicy/mbind/move_pages(nodes[k])mm/mempolicy.c策略执行迁移move_pages(nodes[k])/migrate_pagesmigrate_pages()内核路径验证是否落对move_pages(nodesNULL)查询 status读页的page_to_nid()一句话move_pages既是执行迁移的手也是验收结果的尺而“node id 从哪来、迁移在内核里怎么完成”分别是第 03 章内核页迁移和第 04 章HMM/ZONE_DEVICE 设备内存迁移的内容。7. 本章小结用户态 NUMA 接口分两层底层 syscallnumaif.h libnuma 封装numa.h。四个核心 syscallset_mempolicy线程默认、mbind地址段、move_pages单页查询/迁移本章主角、migrate_pages整进程。三种策略BIND硬绑可能 OOM、PREFERRED软亲和推荐、INTERLEAVE摊带宽。move_pages查询模式nodesNULL是验证“页迁到哪个 node”的标准手段。关联阅读NUMA你的内存不是一整块看懂 NUMA 与机器拓扑