英语

1999 年 11 月由 Kanoj Sarcar 启动 <kanoj@sgi.com>

什么是 NUMA?

可以从几个角度回答这个问题:硬件视角和 Linux 软件视角。

从硬件角度来看,NUMA 系统是一个计算机平台,它包含多个组件或组件集合,每个组件或组件集合可能包含 0 个或多个 CPU、本地内存和/或 IO 总线。 为了简洁起见,并将这些物理组件/组件集合的硬件视图与它们的软件抽象区分开来,我们在本文档中将这些组件/组件集合称为“单元”。

每个“单元”都可以被视为系统的 SMP [对称多处理器] 子集 - 尽管独立 SMP 系统所需的一些组件可能未在任何给定单元上填充。 NUMA 系统的单元通过某种系统互连连接在一起 - 例如,纵横开关或点对点链路是常见的 NUMA 系统互连类型。 这两种类型的互连都可以聚合以创建具有单元的 NUMA 平台,这些单元与其他单元之间的距离不同。

对于 Linux,感兴趣的 NUMA 平台主要是所谓的缓存一致性 NUMA 或 ccNUMA 系统。 对于 ccNUMA 系统,所有内存对连接到任何单元的任何 CPU 都是可见和可访问的,并且缓存一致性由处理器缓存和/或系统互连在硬件中处理。

内存访问时间和有效内存带宽因进行内存访问的 CPU 或 IO 总线所在的单元与包含目标内存的单元之间的距离而异。 例如,连接到同一单元的 CPU 对内存的访问将比对其他远程单元上的内存的访问体验更快的访问时间和更高的带宽。 NUMA 平台可以具有与任何给定单元相距多个远程距离的单元。

平台供应商构建 NUMA 系统不仅仅是为了让软件开发人员的生活变得有趣。 相反,这种架构是一种提供可扩展内存带宽的方法。 但是,为了实现可扩展的内存带宽,系统和应用程序软件必须安排将大部分内存引用[缓存未命中]到“本地”内存 - 如果有的话,则在同一单元上的内存 - 或到最近的具有内存的单元。

这引出了 NUMA 系统的 Linux 软件视图

Linux 将系统的硬件资源划分为多个称为“节点”的软件抽象。 Linux 将节点映射到硬件平台的物理单元上,从而为某些架构抽象出一些细节。 与物理单元一样,软件节点可能包含 0 个或多个 CPU、内存和/或 IO 总线。 并且,同样,对“更近”节点上的内存的访问 - 映射到更近单元的节点 - 通常会比对更远程单元的访问体验更快的访问时间和更高的有效带宽。

对于某些架构(例如 x86),Linux 将“隐藏”任何表示未连接内存的物理单元的节点,并将连接到该单元的任何 CPU 重新分配给表示具有内存的单元的节点。 因此,在这些架构上,不能假定 Linux 与给定节点关联的所有 CPU 都将看到相同的本地内存访问时间和带宽。

此外,对于某些架构(同样,x86 是一个例子),Linux 支持仿真其他节点。 对于 NUMA 仿真,linux 会将现有节点 - 或非 NUMA 平台的系统内存 - 划分为多个节点。 每个仿真的节点将管理底层单元物理内存的一部分。 NUMA 仿真可用于在非 NUMA 平台上测试 NUMA 内核和应用程序功能,并且在与 cpusets 一起使用时,作为一种内存资源管理机制。 [参见 CPUSETS]

对于每个具有内存的节点,Linux 构建一个独立的内存管理子系统,其中包含其自己的空闲页面列表、正在使用的页面列表、使用情况统计信息和用于协调访问的锁。 此外,Linux 为每个内存区域[DMA、DMA32、NORMAL、HIGH_MEMORY、MOVABLE 中的一个或多个]构建一个有序的“区域列表”。 区域列表指定在选定的区域/节点无法满足分配请求时要访问的区域/节点。 当区域没有可用内存来满足请求时,这种情况称为“溢出”或“回退”。

由于某些节点包含多个包含不同类型内存的区域,因此 Linux 必须决定是否对区域列表进行排序,以便分配回退到不同节点上的相同区域类型,或回退到同一节点上的不同区域类型。 这是一个重要的考虑因素,因为某些区域(例如 DMA 或 DMA32)代表相对稀缺的资源。 Linux 选择默认的节点排序区域列表。 这意味着它会尝试从同一节点回退到其他区域,然后再使用按 NUMA 距离排序的远程节点。

默认情况下,Linux 将尝试从执行请求的 CPU 分配到的节点满足内存分配请求。 具体来说,Linux 将尝试从请求来源的节点的适当区域列表中的第一个节点进行分配。 这称为“本地分配”。 如果“本地”节点无法满足请求,则内核将检查所选区域列表中其他节点的区域,以查找列表中可以满足请求的第一个区域。

本地分配将倾向于使随后对已分配内存的访问“本地”于底层物理资源并远离系统互连 - 只要内核代表其分配了一些内存的任务后来没有从该内存迁移开。 Linux 调度器了解平台的 NUMA 拓扑 - 体现在“调度域”数据结构中[参见 调度器域] - 并且调度器尝试最小化任务迁移到远距离调度域。 但是,调度器不会直接考虑任务的 NUMA 足迹。 因此,在足够的不平衡下,任务可以在节点之间迁移,远离其初始节点和内核数据结构。

系统管理员和应用程序设计人员可以使用各种 CPU 亲和性命令行界面(例如 taskset(1) 和 numactl(1))和程序接口(例如 sched_setaffinity(2))来限制任务的迁移,以提高 NUMA 局部性。 此外,可以使用 Linux NUMA 内存策略来修改内核的默认本地分配行为。 [参见 NUMA 内存策略]。

系统管理员可以使用控制组和 CPUsets 来限制非特权用户可以在调度或 NUMA 命令和函数中指定的 CPU 和节点的内存。 [参见 CPUSETS]

在不隐藏无内存节点的架构上,Linux 将仅将具有内存的区域[节点]包含在区域列表中。 这意味着对于无内存节点,“本地内存节点” - CPU 节点区域列表中的第一个节点的节点 - 将不是节点本身。 相反,它将是内核在构建区域列表时选择的作为最近的具有内存的节点。 因此,默认情况下,本地分配将成功,内核将提供最近的可用内存。 这是允许此类分配在包含内存的节点溢出时回退到其他附近节点的同一机制的结果。

某些内核分配不希望或不能容忍此分配回退行为。 相反,他们想确保他们从指定的节点获取内存,或者收到该节点没有可用内存的通知。 这通常是在子系统分配每个 CPU 内存资源时的情况,例如。

进行此类分配的典型模型是使用内核的 numa_node_id() 或 CPU_to_node() 函数之一获取连接到“当前 CPU”的节点的节点 ID,然后仅从返回的节点 ID 请求内存。 当此类分配失败时,请求子系统可能会恢复到其自己的回退路径。 slab 内核内存分配器就是一个例子。 或者,子系统可能会选择在分配失败时禁用或不启用自身。 内核性能分析子系统就是一个例子。

如果架构支持 - 不隐藏 - 无内存节点,那么连接到无内存节点的 CPU 总是会产生回退路径开销,或者如果某些子系统尝试仅从没有内存的节点分配内存,则会初始化失败。 为了透明地支持此类架构,内核子系统可以使用 numa_mem_id() 或 cpu_to_mem() 函数来定位调用或指定 CPU 的“本地内存节点”。 同样,这是将尝试默认本地页面分配的同一节点。