NUMA 内存策略¶
什么是 NUMA 内存策略?¶
在 Linux 内核中,“内存策略”决定了内核将从哪个节点在 NUMA 系统或模拟 NUMA 系统中分配内存。自 2.4.? 以来,Linux 已经支持具有非统一内存访问架构的平台。当前的内存策略支持在 2004 年 5 月左右添加到 Linux 2.6。本文档尝试描述 2.6 内存策略支持的概念和 API。
内存策略不应与 cpusets (Documentation/admin-guide/cgroup-v1/cpusets.rst
) 混淆,后者是一种管理机制,用于限制一组进程可以分配内存的节点。内存策略是一种编程接口,NUMA 感知应用程序可以利用它。当 cpusets 和策略都应用于任务时,cpuset 的限制优先。有关更多详细信息,请参见下面的 内存策略和 cpusets。
内存策略概念¶
内存策略的范围¶
Linux 内核支持内存策略的 _范围_,此处从最通用到最具体进行描述
- 系统默认策略
此策略被“硬编码”到内核中。它是控制所有不受下面讨论的更具体策略范围控制的页面分配的策略。当系统“启动并运行”时,系统默认策略将使用下面描述的“本地分配”。但是,在启动期间,系统默认策略将设置为在所有具有“足够”内存的节点上交叉分配,以免在启动时使初始启动节点过载。
- 任务/进程策略
这是一个可选的、每个任务的策略。当为特定任务定义时,此策略控制由该任务或代表该任务进行的所有页面分配,这些分配不受更具体范围的控制。如果任务未定义任务策略,则所有应由任务策略控制的页面分配将“回退”到系统默认策略。
任务策略适用于任务的整个地址空间。因此,它是可继承的,并且在 fork() [不带 CLONE_VM 标志的 clone()] 和 exec*() 中确实是继承的。这允许父任务为从不知道内存策略的可执行映像 exec() 的子任务建立任务策略。有关任务可以用来设置/更改其任务/进程策略的系统调用的概述,请参阅下面的 内存策略 API 部分。
在多线程任务中,任务策略仅适用于安装该策略的线程 [Linux 内核任务] 以及该线程随后创建的任何线程。安装新任务策略时存在的任何同级线程都会保留其当前策略。
任务策略仅适用于安装策略后分配的页面。当任务更改其任务策略时,任务已错误捕获的任何页面将保留在分配时的策略所分配的位置。
- VMA 策略
“VMA”或“虚拟内存区域”指的是任务虚拟地址空间的一个范围。任务可以为其虚拟地址空间的范围定义特定的策略。有关用于设置 VMA 策略的 mbind() 系统调用的概述,请参阅下面的 内存策略 API 部分。
VMA 策略将控制支持地址空间此区域的页面的分配。任务地址空间中没有任何显式 VMA 策略的任何区域将回退到任务策略,而任务策略本身可能会回退到系统默认策略。
VMA 策略有一些复杂的细节
VMA 策略仅适用于匿名页面。这些页面包括为匿名段(例如任务堆栈和堆)分配的页面,以及使用 MAP_ANONYMOUS 标志 mmap() 的地址空间的任何区域。如果将 VMA 策略应用于文件映射,则如果映射使用了 MAP_SHARED 标志,则该策略将被忽略。如果文件映射使用了 MAP_PRIVATE 标志,则只有在尝试写入映射时分配匿名页面时(即,在写时复制时),才会应用 VMA 策略。
VMA 策略在共享虚拟地址空间的所有任务(又名线程)之间共享,而与策略的安装时间无关;并且它们在 fork() 中继承。但是,由于 VMA 策略引用任务地址空间的特定区域,并且由于地址空间在 exec*() 时被丢弃并重新创建,因此 VMA 策略在 exec() 中不可继承。因此,只有 NUMA 感知应用程序可以使用 VMA 策略。
任务可以在先前 mmap() 区域的子范围内安装新的 VMA 策略。发生这种情况时,Linux 会将现有虚拟内存区域拆分为 2 或 3 个 VMA,每个 VMA 都有自己的策略。
默认情况下,VMA 策略仅适用于安装策略后分配的页面。VMA 范围内已错误捕获的任何页面仍保留在分配时的策略所分配的位置。但是,自 2.6.16 起,Linux 通过 mbind() 系统调用支持页面迁移,以便可以将页面内容移动以匹配新安装的策略。
- 共享策略
从概念上讲,共享策略适用于映射到共享到一个或多个任务的不同地址空间中的“内存对象”。应用程序以与 VMA 策略相同的方式安装共享策略 - 使用 mbind() 系统调用指定映射共享对象的虚拟地址范围。但是,与 VMA 策略(可以认为是任务地址空间范围的属性)不同,共享策略直接应用于共享对象。因此,所有附加到该对象的任务都共享该策略,并且由任何任务为共享对象分配的所有页面都将遵守共享策略。
从 2.6.22 版本开始,只有通过 shmget() 或 mmap(MAP_ANONYMOUS|MAP_SHARED) 创建的共享内存段才支持共享策略。当 Linux 添加共享策略支持时,相关的数据结构被添加到 hugetlbfs 共享内存段。当时,hugetlbfs 不支持在缺页时分配(即延迟分配),因此 hugetlbfs 共享内存段从未与共享策略支持“挂钩”。尽管 hugetlbfs 段现在支持延迟分配,但它们对共享策略的支持尚未完成。
如上面 VMA 策略 部分所述,对于通过 MAP_SHARED 进行 mmap() 映射的常规文件,其页缓存页面的分配会忽略安装在共享文件映射所支持的虚拟地址范围上的任何 VMA 策略。相反,共享页缓存页面(包括尚未被任务写入的私有映射所支持的页面)会遵循任务策略(如果有),否则遵循系统默认策略。
共享策略基础设施支持在共享对象的子集范围上使用不同的策略。但是,Linux 仍然会为每个不同的策略范围拆分安装该策略的任务的 VMA。因此,附加到共享内存段的不同任务可以拥有不同的 VMA 配置,从而映射同一个共享对象。当一个任务在该区域的一个或多个范围上安装了共享策略时,可以通过检查共享共享内存区域的任务的 /proc/<pid>/numa_maps 来看到这一点。
内存策略的组成部分¶
NUMA 内存策略由“模式”、可选模式标志和可选节点集组成。“模式”决定策略的行为,可选模式标志决定模式的行为,可选节点集可以看作是策略行为的参数。
在内部,内存策略通过引用计数结构 struct mempolicy 实现。此结构的详细信息将在下面根据需要进行讨论,以解释其行为。
NUMA 内存策略支持以下 4 种行为模式:
- 默认模式--MPOL_DEFAULT
此模式仅在内存策略 API 中使用。在内部,MPOL_DEFAULT 在所有策略范围中都被转换为 NULL 内存策略。当指定 MPOL_DEFAULT 时,任何现有的非默认策略都将被简单地删除。因此,MPOL_DEFAULT 的意思是“回退到下一个最具体的策略范围”。
例如,NULL 或默认的任务策略将回退到系统默认策略。NULL 或默认的 VMA 策略将回退到任务策略。
在其中一个内存策略 API 中指定时,默认模式不使用可选节点集。
为此策略指定的节点集非空是一种错误。
- MPOL_BIND
此模式指定内存必须来自策略指定的节点集。内存将从该集合中具有足够可用内存且最接近分配发生节点的节点进行分配。
- MPOL_PREFERRED
此模式指定应尝试从策略中指定的单个节点分配。如果该分配失败,内核将根据平台固件提供的信息,按照与首选节点距离增加的顺序搜索其他节点。
在内部,首选策略使用单个节点,即 struct mempolicy 的 preferred_node 成员。当内部模式标志 MPOL_F_LOCAL 设置时,preferred_node 将被忽略,并且该策略被解释为本地分配。“本地”分配策略可以看作是一个首选策略,从包含分配发生的 CPU 的节点开始。
用户可以通过使用此模式传递一个空的节点掩码来指定始终首选本地分配。如果传递了空的节点掩码,则该策略不能使用下面描述的 MPOL_F_STATIC_NODES 或 MPOL_F_RELATIVE_NODES 标志。
- MPOL_INTERLEAVED
此模式指定页面分配应在策略指定的节点上以页面粒度进行交错。此模式的行为也会根据其使用的上下文略有不同。
对于匿名页面和共享内存页面的分配,交错模式使用错误地址在包含地址的段 [VMA] 中的页面偏移量,模策略指定的节点数来索引策略指定的节点集。然后,它会尝试分配一个页面,从选定的节点开始,就像该节点已被首选策略指定或已被本地分配选择一样。也就是说,分配将遵循每个节点的 zonelist。
对于页缓存页面的分配,交错模式使用每个任务维护的节点计数器来索引策略指定的节点集。此计数器在达到最高指定节点后会回绕到最低指定节点。这将倾向于根据页面分配的顺序而不是基于地址范围或文件中的任何页面偏移量将页面分散到策略指定的节点上。在系统启动期间,临时的交错系统默认策略会在此模式下工作。
- MPOL_PREFERRED_MANY
此模式指定分配应优先从策略中指定的节点掩码中满足。如果节点掩码中的所有节点都有内存压力,则分配可以回退到所有现有的 NUMA 节点。这实际上是允许将 MPOL_PREFERRED 用于掩码而不是单个节点。
- MPOL_WEIGHTED_INTERLEAVE
此模式的操作与 MPOL_INTERLEAVE 相同,不同之处在于交错行为是根据 /sys/kernel/mm/mempolicy/weighted_interleave/ 中设置的权重执行的。
加权交错根据权重在节点上分配页面。例如,如果节点 [0,1] 的权重为 [5,2],则在节点 0 上分配 5 个页面,在节点 1 上分配 2 个页面。
NUMA 内存策略支持以下可选模式标志:
- MPOL_F_STATIC_NODES
此标志指定如果任务或 VMA 的允许节点集在内存策略定义后发生更改,则不应重新映射用户传递的节点掩码。
如果没有此标志,则每当由于允许节点集发生更改而重新绑定 mempolicy 时,首选节点掩码(首选多个)、首选节点(首选)或节点掩码(绑定、交错)都会被重新映射到新的允许节点集。这可能会导致使用先前不需要的节点。
有了此标志,如果用户指定的节点与任务的 cpuset 允许的节点重叠,则内存策略将应用于它们的交集。如果两组节点不重叠,则使用默认策略。
例如,考虑一个附加到 cpuset 且内存为 1-3 的任务,该任务在同一集合上设置交错策略。如果 cpuset 的内存更改为 3-5,则交错现在将在节点 3、4 和 5 上发生。但是,有了此标志,由于用户节点掩码中只允许节点 3,因此“交错”仅在该节点上发生。如果用户节点掩码中的任何节点现在都不允许,则使用默认行为。
MPOL_F_STATIC_NODES 不能与 MPOL_F_RELATIVE_NODES 标志组合使用。它也不能用于使用空节点掩码(本地分配)创建的 MPOL_PREFERRED 策略。
- MPOL_F_RELATIVE_NODES
此标志指定用户传递的节点掩码将相对于任务或 VMA 的允许节点集进行映射。内核会存储用户传递的节点掩码,如果允许的节点发生更改,则原始节点掩码将相对于新的允许节点集重新映射。
如果没有此标志(并且没有 MPOL_F_STATIC_NODES),则每当由于允许节点集发生更改而重新绑定 mempolicy 时,节点(首选)或节点掩码(绑定、交错)都会被重新映射到新的允许节点集。该重新映射可能不会保留用户传递的节点掩码相对于其允许节点集在连续重新绑定时的相对性质:如果允许的节点集恢复到其原始状态,则节点掩码 1,3,5 可能会被重新映射到 7-9,然后再重新映射到 1-3。
有了此标志,重新映射会完成,以便用户传递的节点掩码中的节点编号相对于允许的节点集。换句话说,如果在用户的节点掩码中设置了节点 0、2 和 4,则该策略将在允许的节点集中的第一个节点(在绑定或交错情况下,为第三个和第五个节点)上生效。用户传递的节点掩码表示相对于任务或 VMA 的允许节点集的节点。
如果用户的节点掩码包括超出新的允许节点集范围的节点(例如,当允许的节点集仅为 0-3 时,在用户的节点掩码中设置了节点 5),则重新映射会回绕到节点掩码的开头,并且如果尚未设置,则会在 mempolicy 节点掩码中设置该节点。
例如,考虑一个附加到 cpuset 且内存为 2-5 的任务,该任务使用 MPOL_F_RELATIVE_NODES 在同一集合上设置交错策略。如果 cpuset 的内存更改为 3-7,则交错现在将在节点 3、5-7 上发生。如果 cpuset 的内存随后更改为 0,2-3,5,则交错将在节点 0,2-3,5 上发生。
由于一致的重新映射,使用此标志准备节点掩码以指定内存策略的应用程序应忽略其当前实际的 cpuset 施加的内存放置,并准备节点掩码,就好像它们始终位于内存节点 0 到 N-1 上一样,其中 N 是该策略旨在管理的内存节点数。然后,让内核重新映射到任务的 cpuset 允许的内存节点集,因为该集可能会随着时间而变化。
MPOL_F_RELATIVE_NODES 不能与 MPOL_F_STATIC_NODES 标志组合使用。它也不能用于使用空节点掩码(本地分配)创建的 MPOL_PREFERRED 策略。
内存策略引用计数¶
为了解决使用/释放竞争,struct mempolicy 包含一个原子引用计数字段。内部接口 mpol_get()/mpol_put() 分别递增和递减此引用计数。仅当引用计数变为零时,mpol_put() 才会将结构释放回 mempolicy kmem 缓存。
当分配新的内存策略时,其引用计数初始化为“1”,表示安装新策略的任务所持有的引用。当指向内存策略结构的指针存储在另一个结构中时,将添加另一个引用,因为任务的引用将在策略安装完成后被删除。
在策略的运行时“使用”期间,我们尝试最大程度地减少对引用计数的原子操作,因为这会导致缓存行在 CPU 和 NUMA 节点之间跳动。“使用”在这里指的是以下情况之一:
查询策略,无论是任务本身(使用下面讨论的 get_mempolicy() API)还是另一个任务使用 /proc/<pid>/numa_maps 接口。
检查策略以确定页面分配的策略模式以及关联的节点或节点列表(如果有)。这被认为是“热路径”。请注意,对于 MPOL_BIND,其“使用”会扩展到整个分配过程,在此过程中可能会因为页面回收而休眠,因为 BIND 策略节点掩码被引用来过滤不合格的节点。
我们可以避免在上述使用过程中进行额外的引用,方法如下:
我们永远不需要获取/释放系统默认策略,因为一旦系统启动并运行,它就不会被更改或释放。
对于查询策略,我们不需要对目标任务的任务策略或 VMA 策略进行额外的引用,因为我们在查询期间总是获取任务的 mm 的 mmap_lock 进行读取。 set_mempolicy() 和 mbind() API [见下文] 在安装或替换任务或 VMA 策略时,总是获取 mmap_lock 进行写入。因此,在另一个任务或线程正在查询策略时,一个任务或线程不可能释放策略。
页面分配对任务或 VMA 策略的使用发生在缺页路径中,我们在此路径中持有它们的 mmap_lock 进行读取。同样,因为替换任务或 VMA 策略需要持有 mmap_lock 进行写入,所以当我们使用它进行页面分配时,策略不可能被释放掉。
共享策略需要特别考虑。一个任务可以在另一个任务(具有不同的 mmap_lock)正在基于该策略查询或分配页面时,替换共享内存策略。为了解决这种潜在的竞争,共享策略基础设施在查找期间对共享策略添加一个额外的引用,同时对共享策略管理结构保持自旋锁。这要求我们在完成“使用”策略后,删除这个额外的引用。我们必须在用于非共享策略的相同查询/分配路径中删除共享策略上的额外引用。因此,共享策略被标记为这样,并且额外引用被“有条件地”删除——即,仅对共享策略。
由于这种额外的引用计数,并且由于我们必须在自旋锁下在树结构中查找共享策略,因此在页面分配路径中使用共享策略的成本更高。对于由在不同 NUMA 节点上运行的任务共享的共享内存区域上的共享策略来说,尤其如此。可以通过始终回退到共享内存区域的任务或系统默认策略,或者通过将整个共享内存区域预先加载到内存并锁定它来避免这种额外的开销。但是,这可能不适用于所有应用程序。
内存策略 API¶
Linux 支持 4 个用于控制内存策略的系统调用。这些 API 始终只影响调用任务、调用任务的地址空间,或映射到调用任务地址空间中的某个共享对象。
注意
定义这些 API 和用户空间应用程序的参数数据类型的头文件位于不属于 Linux 内核的软件包中。内核系统调用接口(带有“sys_”前缀)在 <linux/syscalls.h> 中定义;模式和标志定义在 <linux/mempolicy.h> 中。
设置 [任务] 内存策略
long set_mempolicy(int mode, const unsigned long *nmask,
unsigned long maxnode);
将调用任务的“任务/进程内存策略”设置为由 ‘mode’ 参数指定的模式,以及由 ‘nmask’ 定义的节点集。 ‘nmask’ 指向一个节点 ID 的位掩码,其中包含至少 ‘maxnode’ 个 ID。可以通过将 ‘mode’ 参数与标志组合来传递可选的模式标志(例如:MPOL_INTERLEAVE | MPOL_F_STATIC_NODES)。
有关更多详细信息,请参阅 set_mempolicy(2) 手册页
获取 [任务] 内存策略或相关信息
long get_mempolicy(int *mode,
const unsigned long *nmask, unsigned long maxnode,
void *addr, int flags);
根据 ‘flags’ 参数,查询调用任务的“任务/进程内存策略”,或指定虚拟地址的策略或位置。
有关更多详细信息,请参阅 get_mempolicy(2) 手册页
为任务地址空间的范围安装 VMA/共享策略
long mbind(void *start, unsigned long len, int mode,
const unsigned long *nmask, unsigned long maxnode,
unsigned flags);
mbind() 将 (mode, nmask, maxnodes) 指定的策略安装为调用任务的地址空间的 ‘start’ 和 ‘len’ 参数指定的范围的 VMA 策略。可以通过 ‘flags’ 参数请求其他操作。
有关更多详细信息,请参阅 mbind(2) 手册页。
为任务地址空间的范围设置主节点
long sys_set_mempolicy_home_node(unsigned long start, unsigned long len,
unsigned long home_node,
unsigned long flags);
sys_set_mempolicy_home_node 为任务地址范围中存在的 VMA 策略设置主节点。该系统调用仅更新现有内存策略范围的主节点。其他地址范围将被忽略。主节点是页面分配将来自的最接近的 NUMA 节点。指定主节点将覆盖默认分配策略,以便为执行 CPU 分配靠近本地节点的内存。
内存策略命令行界面¶
虽然不是内存策略的 Linux 实现的严格组成部分,但存在一个命令行工具 numactl(8),它允许人们
通过 set_mempolicy(2)、fork(2) 和 exec(2) 为指定的程序设置任务策略
通过 mbind(2) 为共享内存段设置共享策略
numactl(8) 工具与包含内存策略系统调用包装器的库的运行时版本一起打包。一些发行版将头文件和编译时库打包在单独的开发包中。
内存策略和 cpusets¶
内存策略在 cpusets 中工作,如上所述。对于需要一个或一组节点的内存策略,节点被限制为 cpuset 约束允许其内存的节点集。如果为策略指定的节点掩码包含 cpuset 不允许的节点,并且没有使用 MPOL_F_RELATIVE_NODES,则使用为策略指定的节点集与具有内存的节点集的交集。如果结果为空集,则认为该策略无效,无法安装。如果使用了 MPOL_F_RELATIVE_NODES,则该策略的节点会映射到任务允许的节点集中,并像之前描述的那样折叠到其中。
当两个 cpusets 中的任务共享对内存区域的访问时(例如,由 shmget() 或使用 MAP_ANONYMOUS 和 MAP_SHARED 标志的 mmap() 创建的共享内存段),内存策略与 cpusets 的交互可能会出现问题,并且如果任何任务在该区域上安装共享策略,则只有其内存在两个 cpusets 中都允许的节点才能在策略中使用。获取此信息需要“跳出”内存策略 API 以使用 cpuset 信息,并且需要知道其他任务可能附加到共享区域的 cpusets。此外,如果 cpusets 的允许内存集不相交,则“本地”分配是唯一有效的策略。