由 Kanoj Sarcar 于 1999 年 11 月启动 <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 系统不仅仅是为了让软件开发人员的生活变得有趣。相反,这种架构是一种提供可扩展内存带宽的方法。但是,为了实现可扩展的内存带宽,系统和应用程序软件必须安排大部分内存引用 [缓存未命中] 到“本地”内存——如果存在,则与 CPU 在同一个单元上的内存——或到最近的有内存的单元。
这引出了 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 内存策略]。
系统管理员可以使用控制组和 CPU 集来限制非特权用户可以在调度或 NUMA 命令和函数中指定的 CPU 和节点的内存。[参见 CPUSETS]
在不隐藏无内存节点的架构上,Linux 将仅在区域列表中包含具有内存的区域 [节点]。这意味着对于无内存节点,“本地内存节点”(CPU 节点的区域列表中第一个区域的节点)将不是节点本身。相反,它将是内核在构建区域列表时选择的最近的具有内存的节点。因此,默认情况下,本地分配将成功,内核将提供最近的可用内存。这是相同机制的结果,该机制允许此类分配在包含内存的节点溢出时回退到附近的其他节点。
某些内核分配不希望或不能容忍这种分配回退行为。相反,他们希望确保他们从指定的节点获得内存,或者被通知该节点没有空闲内存。例如,当子系统分配每个 CPU 的内存资源时,通常会出现这种情况。
进行此类分配的典型模型是使用内核的 numa_node_id() 或 CPU_to_node() 函数之一获取“当前 CPU”所连接到的节点的节点 ID,然后仅从返回的节点 ID 请求内存。当此类分配失败时,请求子系统可能会恢复到其自己的回退路径。slab 内核内存分配器就是一个例子。或者,子系统可以选择在分配失败时禁用或不启用自身。内核分析子系统就是一个例子。
如果架构支持(不隐藏)无内存节点,那么连接到无内存节点的 CPU 将始终产生回退路径开销,或者某些子系统如果尝试仅从没有内存的节点分配内存,则会初始化失败。为了透明地支持此类架构,内核子系统可以使用 numa_mem_id() 或 cpu_to_mem() 函数来查找调用或指定的 CPU 的“本地内存节点”。同样,这是尝试默认的本地页面分配的同一节点。