local_memory_node(int node)是内核中用于解决memoryless node(无内存节点)问题的关键函数。它的核心作用是:给定一个节点 ID,返回该节点或其最近的“有内存”节点的 ID,确保内核分配内存时不会落到一个物理上没有内存的节点上。
函数实现
根据源码中的注释,它的实现逻辑如下:
/* * Return node id of node used for "local" allocations. * I.e., first node id of first zone in arg node's generic zonelist. * Used for initializing percpu 'numa_mem', which is used primarily * for kernel allocations, so use GFP_KERNEL flags to locate zonelist. */ int local_memory_node(int node) { struct zoneref *z; struct zone *zone; z = first_zones_zonelist(node_zonelist(node, GFP_KERNEL), gfp_zone(GFP_KERNEL), NULL); zone = zonelist_zone(z); if (!zone) return node; return zone_to_nid(zone); }核心逻辑
利用 zonelist 回退机制:函数通过
node_zonelist(node, GFP_KERNEL)获取该节点的 zonelist。zonelist 在构建时已经考虑了“如果本节点没有内存,应该回退到哪个最近的节点”的问题。找到第一个有内存的节点:
first_zones_zonelist()会沿着 zonelist 找到第一个满足GFP_KERNEL分配条件的 zone。如果传入的node本身没有内存,zonelist 的第一个有效条目会指向最近的有内存节点。返回节点 ID:找到 zone 后,通过
zone_to_nid(zone)返回它所属的节点 ID。如果没有任何可用 zone(极端情况),则回退返回原始node。
为什么需要它?
在 PowerPC 等架构上,CPU 可以连接到物理上没有挂载内存的 NUMA 节点(memoryless node)。如果代码直接使用cpu_to_node(cpu)获取节点 ID,然后从这个节点分配内存,而该节点实际上没有内存,分配就会失败或产生性能问题。
local_memory_node()保证了:无论 CPU 连接的是不是无内存节点,都能拿到一个确定有内存的、距离最近的节点 ID。
典型使用场景
1. 初始化 per-CPU 的numa_mem变量
在build_all_zonelists()中,内核会为每个在线 CPU 初始化其“本地内存节点”:
for_each_online_cpu(cpu) set_cpu_numa_mem(cpu, local_memory_node(cpu_to_node(cpu)));
这样,后续的numa_mem_id()或cpu_to_mem()就能返回一个有内存的节点 ID。
2. 子系统避免在无内存节点上分配
多个子系统曾修复过相关问题,改用local_memory_node()替代直接的cpu_to_node():
blk-mq:避免将无内存节点的 ID 编码到硬件队列的
numa_node中IB/hfi1:避免在无内存节点上分配内存
BPF:避免在无内存节点上分配
关于cpu_to_node()的争论
值得注意的是,社区曾讨论过是否应该直接在cpu_to_node()内部处理无内存节点的问题。有开发者建议:与其逐个子系统修改cpu_to_node()的调用点,不如在CONFIG_HAVE_MEMORYLESS_NODES下让cpu_to_node()本身返回有内存的节点。不过这个建议最终没有被采纳,local_memory_node()作为显式的转换函数保留了下来。
总结
local_memory_node()是memoryless node 架构的适配层:它利用 zonelist 的回退机制,将“CPU 所在的拓扑节点”映射为“最近的有内存节点”,确保内核在初始化numa_mem和子系统分配内存时不会落到物理上空无一物的节点上。它的存在让内核在 PowerPC 等支持无内存节点的平台上能够正确地进行本地内存分配。