什么是linux内存策略?
在linux内核的NUMA系统或者类NUMA系统,内核策略决定了内核将从那个node分配内存。linux自从2.4.?就已经支持了非统一内存访问(NUMA Non-Uniform Memory Access)平台。现在的内存策略支持是在大约2004年五月被加入到linux2.6的。这篇文档试图描述2.6内存策略支持的概念和API。
不要把cpu集(cpuset)的概念和内存策略搞混(Documentation/cgroup-v1/cpusets.txt),cpuset是一种管理机制,用于限制一组进程可以从中分配内存的nodes。内存策略是一种编程接口,对NUMA敏感的程序可以从中受益。当cpuset 和policy 都应用于任务时,cpuset 的限制优先。 有关更多详细信息,请参阅下面的“内存策略和 CPUSET”。
MEMORY POLICY CONCEPTS
内存策略概念
内存策略的范围
linux内核支持内存策略的范围(scopes),下面从一般到具体来描述。
系统默认策略(System Default Policy):这个策略是被硬编码在内核中的。它是管理所有页分配的策略,而这不能被下面讨论的更具体的策略所控制。当系统正在运行(up and running),系统默认策略将使用下面描述的本地分配(local allocation)策略。但是,在启动期间,系统默认策略将设置为在具有“足够”内存的所有节点之间交错分配(interleave allocations),以免使初始启动节点过载。
任务/进程策略(Task/Process Policy):这是一个可选的每任务策略。当被一个特定的任务选择时,此策略控制了一些任务所有页的分配,这些任务代表不受更具体范围的控制。如果一个任务没有定义任务策略,那么所有由任务策略控制的页面分配“回退”到系统默认策略。
任务策略适用于任务的整个地址空间。 因此,它是可继承的,而且 也确实是跨fork和exec之间被继承的。允许父任务为一个子进程建 立任务策略,而这子进程是由一个不了解内存策略的可执行镜像 所exec的。有关那些任务可用于设置/更改其任务/进程策略的系统 调用的概述,请参阅下面的 MEMORY POLICY APIS 部分。
在多线程任务中,任务策略仅适用于安装l了策略的线程 [Linux 内 核任务] 以及该线程随后创建的任何线程。 安装新任务策略时存在 的任何同级线程都保留其当前策略。
任务策略仅适用于安装策略后分配的页面。 当任务更改其任务策 略时,任何已被任务故障(faulted)的页面将保留其在分配时的基于 的内存策略。
VMA策略:一个"VMA"或者"虚拟内存区(Virtual Memory Area)" 与一个任务的虚拟地址空间范围所关联。一个任务可能会为它虚拟地址空间的一个范围定义特定的策略。有关用于设置 VMA 策略的 mbind() 系统调用的概述,请参阅下面的 MEMORY POLICIES APIS 部分。
VMA 策略将管理该地址空间区域的页面的分配。 没有明确 VMA 策略的任务地址空间的任何区域都将回退到任务策略,而任务策略本身可能回退到系统默认策略。
VMA 策略有一些复杂的细节:
VMA 策略仅适用于匿名页面。这些包括为匿名段(例如任务堆栈)分配的页面,以及使用 MAP_ANONYMOUS 标志进行 mmap() 的地址空间的任何区域。
如果 VMA 策略应用于文件映射,并且映射使用了 MAP_SHARED 标志,则该策略将被忽略。如果文件映射使用了 MAP_PRIVATE 标志,VMA 策略将仅在尝试写入映射时分配匿名页面时应用 - 即在 Copy-On-Write 时。
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 策略不同的是,VMA 策略可以被视为任务地址空间(address space)范围的属性,而共享策略直接应用于共享对象。 因此,附加到该对象的所有任务都共享该策略,并且任何任务为该共享对象分配的所有页面都将遵守该共享策略。
从 2.6.22 开始,只有由 shmget() 或 mmap(MAP_ANONYMOUS|MAP_SHARED) 创建的共享内存段支持共享策略。 当共享策略支持被添加到 Linux 时,相关的数据结构被添加到 Hugetlbfs 共享内存段。 当时,hugetlbfs 不支持故障时分配——也就是延迟分配——所以hugetlbfs 共享内存段从未“连接”到共享策略支持。 虽然hugetlbfs段现在支持延迟分配,但他们对共享策略的支持还没有完成。
如上所述 [VMA 策略],使用 MAP_SHARED 调用mmap() 的常规文件的page cache页面的分配,会忽略安装在由共享文件映射支持的虚拟地址范围上的任何 VMA 策略。相反,共享页面的缓存页面,包括尚未由任务写入的私有映射的页面,会遵循任务策略,如果有的话,否则为系统默认策略。
共享策略支持共享对象的子集范围上的不同策略。 但是,Linux 仍然为每个不同的策略范围拆分安装策略的任务的 VMA。因此,附加到共享内存段的不同任务可以具有映射该共享对象的不同 VMA 配置。当一个任务在该区域的一个或多个范围上安装了共享策略时, 可以通过检查共享共享内存区域的任务的 /proc//numa_maps 看出。
内存策略的组成部分
一个Linux内存策略包括一个模式(mode),可选的模式标志,和一个可选的节点(node)集合。模式决定了策略的行为,可选的模式标志决定了模式的行为,可选的节点集可以被视为策略行为的参数。
在内部,内存策略由引用计数结构 struct mempolicy 实现。 当需要解释行为时,该结构的细节将在下文中讨论。
linux内存策略支持下面四种行为模式:
- 默认模式–MPOL_DEFAULT:此模式只在内存策略api中被用到。在内部,MPOL_DEFAULT在所有策略范围中被转化为NULL内存策略。当指定 MPOL_DEFAULT 时,将简单地删除任何现有的非默认策略。 因此,MPOL_DEFAULT 意味着“回退到下一个最具体的策略范围”。
例如,一个NULL或者默认任务策略将回退到系统默认策略。NULL或者默认vma策略将回退到任务策略。当它被内存策略api中指定时,默认的模式并不适用可选的节点集合。为该策略指定的节点集不为空的话,是错误的。 - MPOL_BIND:此模式指定了内存只能来自该策略指定的节点集合。内存将会在集合中还由足够空闲内存的节点中分配,并且此节点离分配发生的节点最近。
MPOL_PREFERRED: This mode specifies that the allocation should be - MPOL_PREFERRED:此模式指定应从策略中指定的单个节点尝试分配。如果该分配失败,那内核将根据平台固件提供的信息,按照与首选节点的距离增加的顺序搜索其他节点。
在内部,首选(Preferred)策略使用一个单节点–struct mempolicy
的preferred_node
成员。当内部模式标志MPOL_F_LOCAL 被设置时,preferred_node
将会被忽略并且该策略会被解释为本地分配。"本地"分配策略可以被看作是一种首选策略,只不过是从包含了分配发生的cpu的节点开始。
用户可以通过在此模式下传递空节点掩码来指定始终首选本地分配。如果传递了空的节点掩码,那么策略将不能使用下方描述的MPOL_F_STATIC_NODES和MPOL_F_RELATIVE_NODES 标志。 - MPOL_INTERLEAVED:此模式制定了页的分配必须是交错的(在页面粒度上),必须跨策略中指定的节点。此模式根据它所使用的上下文,行为也稍有不同:
对于匿名页面和共享内存页面的分配,交错模式使用错误地址的页面偏移量将策略指定的节点集索引到段 [VMA],这个段包含了以策略指定的节点数量为模的地址。然后它尝试分配一个页面,从选定的节点开始,就好像该节点已经由首选策略指定或像被本地分配选定一样。也就是说,分配将遵循每个节点的区域列表(zone list)。
对于缓存页面的页的分配,交错模式使用每个任务维护的节点计数器来索引策略指定的节点集。此计数器在到达指定的最高节点后,会绕回最低的指定节点。这将倾向于根据页面的分配顺序将页面分散到策略指定的节点上,而不是根据地址范围或文件中的任何页面偏移量。 在系统启动期间,临时的交错系统默认策略将以这种模式工作。
linux内存策略支持下面几种可选的模式标志:
- MPOL_F_STATIC_NODES:如果在定义内存策略后任务或 VMA 的节点集发生更改,此标志指定了用户传递的节点掩码不能被重映射。
如果没有此标志,则任何时候可能会由于节点集的更改而导致内存策略反弹,节点(首选模式)或节点掩码(绑定、交错模式)将重新映射到新的节点集。 这可能会导致使用以前所不预期的节点。
若使用此标志,如果用户指定的节点与任务的 cpuset 允许的节点重叠,则内存策略将应用于它们的交集。 如果两组节点不重叠,则使用默认策略。
例如,考虑附加到具有 mems 1-3 的 cpuset 的任务,该任务在同一组上设置 Interleave 策略。 如果 cpuset 的 mems 更改为 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 nodemask 中设置节点。
例如,考虑附加到具有 mems 2-5 的 cpuset 的任务,该任务在具有 MPOL_F_RELATIVE_NODES 的同一组上设置交错策略。如果 cpuset 的 mems 更改为 3-7,则交错现在发生在节点 3,5-7 上。如果 cpuset 的 mems 然后更改为 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分别增加和减少此引用计数。当引用计数变为0, mpol_put将释放该结构到内存策略的缓存中。
当分配一个新的内存策略时,它的引用计数被初始化为1,表示正在安装新策略的任务所持有的引用。当指向内存策略结构的指针存储在另一个结构中时,会添加另一个引用,因为在策略安装完成时将删除任务的引用。
在策略的运行时“使用”期间,我们尝试最小化对引用计数的原子操作,因为这会导致缓存行在 cpu和 NUMA 节点之间反弹。 这里的“使用”是指以下情况之一:
-
通过任务本身[使用下面讨论的 get_mempolicy() API] 或使用 /proc//numa_maps 接口的另一个任务查询策略。
-
检查策略以确定用于页面分配的策略模式和关联节点或节点列表(如果有)。 这被认为是“热路径”。 请注意,对于 MPOL_BIND,“使用”扩展到整个分配过程,这可能会在页面回收期间休眠,因为 BIND 策略节点掩码是通过引用使用来过滤不合格节点的。
我们可以像下面这样避免额外引用:
-
我们永远不需要获取/释放系统默认策略,因为一旦系统启动并运行,它就不会改变或释放。
-
查询策略时,我们不需要对目标任务的任务策略或vma策略进行额外的引用,因为我们总是在查询过程中获取任务的mm的mmap_sem以供读取。 set_mempolicy() 和 mbind() API [见下文] 在安装或替换任务或 vma 策略时总是获取 mmap_sem 以进行写入。因此,当另一个任务或线程正在查询它时,任务或线程不可能释放策略。
-
任务或 vma 策略的页面分配使用发生在我们持有它们 mmap_sem 以供读取的故障路径中。同样,因为替换任务或 vma 策略需要持有 mmap_sem 以进行写入,所以当我们将其用于页面分配时,该策略无法释放。
-
共享策略需要特殊考虑。一个任务可以替换共享内存策略,而另一个具有不同 mmap_sem 的任务根据策略查询或分配页面。为了解决这种潜在的竞争,共享策略在查找期间添加了对共享策略的额外引用,同时在共享策略管理结构上持有自旋锁。这要求我们在完成“使用”策略后删除这个额外的引用。我们必须在用于非共享策略的相同查询/分配路径中删除对共享策略的额外引用。出于这个原因,共享策略被标记为这样,额外的引用被“有条件地”删除——即,仅用于共享策略。
由于这种额外的引用计数,并且因为我们必须在自旋锁下的树结构中查找共享策略,因此在页面分配路径中使用共享策略的成本更高。对于在不同 NUMA 节点上运行的任务共享的共享内存区域的共享策略尤其如此。通过总是回退到共享内存区域的任务或系统默认策略,或者将整个共享内存区域预置入内存并将其锁定,可以避免这种额外的开销。但是,这可能不适用于所有应用程序。
内存策略的 API
linux支持三个系统调用来控制内存策略。这些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’ 指向至少包含 ‘maxnode’ id 的节点 id 的位掩码。 可以通过将 ‘mode’ 参数与标志组合来传递可选模式标志(例如:MPOL_INTERLEAVE | MPOL_F_STATIC_NODES)。
有关更多详细信息,请参阅 set_mempolicy(2) 手册页
获取 [Task] 内存策略或相关信息
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) 指定的策略安装为 VMA 策略,用于由 ‘start’ 和 ‘len’ 参数指定的调用任务的地址空间范围。 可以通过“flags”参数请求其他操作。
有关更多详细信息,请参阅 mbind(2) 手册页。
内存策略命令行接口
尽管严格来说不是 Linux 内存策略实现的一部分,但存在一个命令行工具 numactl(8),它允许人们:
-
通过 set_mempolicy(2)、fork(2) 和 exec(2) 为指定程序设置任务策略
-
通过 mbind(2) 为共享内存段设置共享策略
numactl(8) 工具与包含内存策略系统调用包装器的库的运行时版本一起打包。 一些发行版将头文件和编译时库打包在一个单独的开发包中。
内存策略和cpu集
如上所述,内存策略在 cpuset 中工作。对于需要一个节点或一组节点的内存策略,节点被限制为 cpuset 约束允许其内存的节点集。如果为策略指定的节点掩码包含 cpuset 不允许的节点并且未使用 MPOL_F_RELATIVE_NODES,则使用为策略指定的节点集与具有内存的节点集的交集。如果结果为空集,则认为该策略无效,无法安装。如果使用 MPOL_F_RELATIVE_NODES,则策略的节点将映射到任务的允许节点集并将其折叠到任务的允许节点集中,如前所述。
当两个 cpuset 中的任务共享对内存区域的访问时,内存策略和 cpuset 的交互可能会出现问题,例如由 mmap() 的 shmget() 创建的具有 MAP_ANONYMOUS 和 MAP_SHARED 标志的共享内存段,并且任何任务安装共享区域上的策略,只有在两个 cpuset 中都允许内存的节点才能在策略中使用。获取此信息需要“跳出”内存策略 API 以使用 cpuset 信息,并且需要知道其他任务可能在哪些 cpuset 中附加到共享区域。此外,如果 cpuset 允许的内存集不相交,则“本地”分配是唯一有效的策略。