由Kanoj Sarcar < kanoj @ sgi于 1999年11月开始。com >
什么是NUMA?
这个问题可以从两个角度回答:硬件视图和Linux软件视图。
从硬件角度来看,NUMA系统是一个计算机平台,它包含多个组件或组件,每个组件或组件可能包含0个或多个CPU,本地内存和/或IO总线。为了简洁起见,并为了从软件抽象中消除这些物理组件/组件的硬件视图的歧义,在本文档中,我们将这些组件/组件称为“单元”。
每个“单元”都可以视为系统的SMP(对称多处理器)子集,尽管独立SMP系统所需的某些组件可能未填充在任何给定的单元上。NUMA系统的单元通过某种类型的系统互连连接在一起,例如,交叉开关或点对点链接是NUMA系统互连的常见类型。可以将这两种类型的互连聚合在一起,以创建NUMA平台,其中的单元与其他单元之间的距离要多。
对于Linux,感兴趣的NUMA平台主要是所谓的Cache Coherent 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一起使用时,它可作为一种内存资源管理机制。[请参阅文档/admin-guide/cgroup-v1/cpusets.rst]
对于每个具有内存的节点,Linux都会构建一个独立的内存管理子系统,并具有自己的空闲页面列表,使用中页面列表,使用情况统计信息和用于中介访问的锁。此外,Linux为每个内存区域(DMA,DMA32,NORMAL,HIGH_MEMORY,MOVABLE中的一个或多个)构造一个有序的“区域列表”。区域列表指定当选定的区域/节点不能满足分配请求时要访问的区域/节点。当区域没有可用内存来满足请求时,这种情况称为“溢出”或“后备”。
因为某些节点包含多个包含不同类型内存的区域,所以Linux必须决定是对区域列表进行排序,以使分配回落到不同节点上的相同区域类型,还是落到同一节点上的不同区域类型。这是一个重要的考虑因素,因为某些区域(例如DMA或DMA32)代表相对稀缺的资源。Linux选择默认的节点排序区域列表。这意味着在使用按NUMA距离排序的远程节点之前,它会尝试从同一节点回退到其他区域。
默认情况下,Linux将尝试满足来自执行请求的CPU所分配到的节点的内存分配请求。具体来说,Linux将尝试从相应区域列表中的第一个节点为请求发起的节点分配资源。这称为“本地分配”。如果“本地”节点不能满足请求,内核将检查所选区域列表中其他节点的区域,以寻找列表中可以满足请求的第一个区域。
本地分配将倾向于保持对底层物理资源“本地”访问分配的内存的后续访问,并且不与系统互连—只要代表内核分配了一些内存的任务以后不会从该内存中迁移出去。Linux调度程序知道平台的NUMA拓扑-体现在“调度域”数据结构中[请参见Documentation / scheduler / sched-domains.rst] –调度器尝试将任务迁移到远程调度域的可能性降到最低。但是,调度程序不会直接考虑任务的NUMA占用空间。因此,在足够的不平衡状态下,任务可以在远离其初始节点和内核数据结构的节点之间迁移。
系统管理员和应用程序设计人员可以使用各种CPU相似性命令行界面(例如tasket(1)和numactl(1))和程序界面(例如sched_setaffinity(2))来限制任务的迁移,以改善NUMA局部性。此外,可以使用Linux NUMA内存策略修改内核的默认本地分配行为。[请参阅 文档/管理指南/mm/numa_memory_policy.rst ]。
系统管理员可以使用控制组和CPUset限制非特权用户可以在调度或NUMA命令和功能中指定的CPU和节点的内存。[请参阅文档/admin-guide/cgroup-v1/cpusets.rst]
在不会隐藏无内存节点的体系结构上,Linux将仅包含区域列表中具有内存的区域[节点]。这意味着对于无内存节点,“本地内存节点”(CPU节点的区域列表中第一个区域的节点)将不是节点本身。相反,它将是内核在构建区域列表时选择为与内存最近的节点的节点。因此,默认情况下,本地分配将随着内核提供最接近的可用内存而成功。这是相同机制的结果,该机制允许在确实包含内存溢出的节点时将此类分配回退到其他附近的节点。
一些内核分配不希望或不能容忍此分配回退行为。而是他们希望确保它们从指定的节点获取内存,或者得到通知该节点没有可用内存。例如,当子系统为每个CPU分配内存资源时,通常就是这种情况。
进行这种分配的典型模型是使用内核的numa_node_id()或CPU_to_node()函数之一获取“当前CPU”所连接的节点的节点ID,然后仅从返回的节点ID请求内存。当这样的分配失败时,请求子系统可以恢复到其自己的后备路径。平板内核内存分配器就是一个例子。或者,子系统可以选择在分配失败时禁用或不启用自身。内核分析子系统就是一个例子。
如果该体系结构支持(而不是隐藏)无内存节点,那么连接到无内存节点的CPU总是会产生后备路径开销,或者如果某些子系统尝试从没有内存的节点中专门分配内存,则某些子系统将无法初始化。为了透明地支持这种架构,内核子系统可以使用numa_mem_id()或cpu_to_mem()函数来为调用或指定的CPU定位“本地内存节点”。同样,这是将尝试从其尝试默认本地页面分配的同一节点。