CPUSETS¶
版权 (C) 2004 BULL SA。
部分版权 (c) 2004-2006 Silicon Graphics, Inc.
修改者:Paul Jackson <pj@sgi.com>
修改者:Christoph Lameter <cl@gentwo.org>
修改者:Paul Menage <menage@google.com>
修改者:Hidetoshi Seto <seto.hidetoshi@jp.fujitsu.com>
1. Cpusets¶
1.1 什么是 cpusets?¶
Cpusets 提供了一种机制,用于将一组 CPU 和内存节点分配给一组任务。在本文档中,“内存节点”指的是包含内存的在线节点。
Cpusets 将任务的 CPU 和内存放置限制在任务当前 cpuset 中的资源内。 它们形成一个嵌套层次结构,可在虚拟文件系统中看到。 除了已经存在的之外,这些是管理大型系统上动态作业放置所需的基本钩子。
Cpusets 使用 控制组 中描述的通用 cgroup 子系统。
任务通过 sched_setaffinity(2) 系统调用请求将其 CPU 亲和性掩码中包含 CPU,以及通过 mbind(2) 和 set_mempolicy(2) 系统调用请求在其内存策略中包含内存节点,这些请求都会通过该任务的 cpuset 进行过滤,滤除该 cpuset 中不存在的任何 CPU 或内存节点。 调度程序不会在 cpus_allowed 向量中不允许的 CPU 上调度任务,并且内核页面分配器不会在请求任务的 mems_allowed 向量中不允许的节点上分配页面。
用户级代码可以通过 cgroup 虚拟文件系统按名称创建和销毁 cpusets,管理这些 cpusets 的属性和权限,以及分配给每个 cpuset 的 CPU 和内存节点,指定和查询任务分配给哪个 cpuset,并列出分配给 cpuset 的任务 pid。
1.2 为什么需要 cpusets?¶
大型计算机系统的管理,这些系统具有许多处理器(CPU)、复杂的内存缓存层次结构和具有非均匀访问时间 (NUMA) 的多个内存节点,对进程的有效调度和内存放置提出了额外的挑战。
通常,规模较小的系统可以通过简单地让操作系统在请求的任务之间自动共享可用的 CPU 和内存资源来以足够的效率运行。
但是,较大的系统可以通过仔细的处理器和内存放置来减少内存访问时间和争用,从而更多地受益,并且通常代表客户的更大投资,可以从将作业显式放置在系统适当大小的子集上受益。
这在以下情况下尤其有价值:
运行同一 Web 应用程序的多个实例的 Web 服务器,
运行不同应用程序的服务器(例如,Web 服务器和数据库),或者
运行具有严格性能要求的大型 HPC 应用程序的 NUMA 系统。
这些子集或“软分区”必须能够动态调整,随着作业组合的变化,而不会影响其他并发执行的作业。 当内存位置发生更改时,运行的作业页面的位置也可能会移动。
内核 cpuset 补丁提供了有效实现此类子集所需的最基本内核机制。 它利用 Linux 内核中现有的 CPU 和内存放置工具,避免对关键调度程序或内存分配器代码产生任何额外影响。
1.3 cpusets 如何实现?¶
Cpusets 提供了一种 Linux 内核机制来限制进程或进程集使用的 CPU 和内存节点。
Linux 内核已经有一对机制来指定任务可以调度在哪些 CPU 上 (sched_setaffinity) 以及它可以从哪些内存节点获取内存 (mbind, set_mempolicy)。
Cpusets 按如下方式扩展了这两种机制
Cpusets 是内核已知的一组允许的 CPU 和内存节点。
系统中的每个任务都通过任务结构中指向引用计数的 cgroup 结构的指针附加到 cpuset。
对 sched_setaffinity 的调用仅过滤为该任务的 cpuset 中允许的那些 CPU。
对 mbind 和 set_mempolicy 的调用仅过滤为该任务的 cpuset 中允许的那些内存节点。
根 cpuset 包含所有系统的 CPU 和内存节点。
对于任何 cpuset,都可以定义包含父 CPU 和内存节点资源子集的子 cpuset。
cpuset 的层次结构可以挂载在 /dev/cpuset 上,以便从用户空间浏览和操作。
可以将 cpuset 标记为独占,这可确保没有其他 cpuset(直接祖先和后代除外)可以包含任何重叠的 CPU 或内存节点。
您可以列出附加到任何 cpuset 的所有任务(按 pid)。
cpuset 的实现需要一些简单的钩子进入内核的其余部分,没有一个位于性能关键路径中
在 init/main.c 中,在系统启动时初始化根 cpuset。
在 fork 和 exit 中,将任务附加到其 cpuset 并从中分离。
在 sched_setaffinity 中,通过该任务的 cpuset 中允许的内容来屏蔽请求的 CPU。
在 sched.c migrate_live_tasks() 中,尽可能将迁移任务保留在其 cpuset 允许的 CPU 内。
在 mbind 和 set_mempolicy 系统调用中,通过该任务的 cpuset 中允许的内容来屏蔽请求的内存节点。
在 page_alloc.c 中,将内存限制为允许的节点。
在 vmscan.c 中,将页面回收限制为当前 cpuset。
您应该挂载“cgroup”文件系统类型,以便能够浏览和修改内核当前已知的 cpusets。 没有为 cpusets 添加新的系统调用 - 对查询和修改 cpusets 的所有支持都通过此 cpuset 文件系统进行。
每个任务的 /proc/<pid>/status 文件都有四个添加的行,显示任务的 cpus_allowed(可以调度在哪些 CPU 上)和 mems_allowed(可以从中获取内存的内存节点),采用以下示例中看到的两种格式
Cpus_allowed: ffffffff,ffffffff,ffffffff,ffffffff
Cpus_allowed_list: 0-127
Mems_allowed: ffffffff,ffffffff
Mems_allowed_list: 0-63
每个 cpuset 由 cgroup 文件系统中的一个目录表示,该目录包含(在标准 cgroup 文件之上)以下描述该 cpuset 的文件
cpuset.cpus:该 cpuset 中的 CPU 列表
cpuset.mems:该 cpuset 中的内存节点列表
cpuset.memory_migrate 标志:如果设置,则将页面移动到 cpuset 节点
cpuset.cpu_exclusive 标志:CPU 放置是否独占?
cpuset.mem_exclusive 标志:内存放置是否独占?
cpuset.mem_hardwall 标志:内存分配是否硬隔离
cpuset.memory_pressure:衡量 cpuset 中分页压力的程度
cpuset.memory_spread_page 标志:如果设置,则在允许的节点上均匀分布页面缓存
cpuset.memory_spread_slab 标志:已过时。 没有任何功能。
cpuset.sched_load_balance 标志:如果设置,则在该 cpuset 上的 CPU 内进行负载平衡
cpuset.sched_relax_domain_level:迁移任务时的搜索范围
此外,只有根 cpuset 具有以下文件
cpuset.memory_pressure_enabled 标志:计算 memory_pressure 吗?
使用 mkdir 系统调用或 shell 命令创建新的 cpusets。 通过写入该 cpusets 目录中的适当文件来修改 cpuset 的属性,例如其标志、允许的 CPU 和内存节点以及附加的任务,如上所列。
嵌套 cpusets 的命名分层结构允许将大型系统划分为嵌套的、可动态更改的“软分区”。
每个任务的附加(由该任务的任何子任务在 fork 时自动继承)到 cpuset 允许将系统上的工作负载组织成相关的任务集,以便每个集都被限制为使用特定 cpuset 的 CPU 和内存节点。 如果必要的 cpuset 文件系统目录上的权限允许,可以将任务重新附加到任何其他 cpuset。
这种对系统“大局”的管理与使用 sched_setaffinity、mbind 和 set_mempolicy 系统调用对单个任务和内存区域进行的详细放置无缝集成。
以下规则适用于每个 cpuset
其 CPU 和内存节点必须是其父级的子集。
除非其父级是独占的,否则不能将其标记为独占。
如果其 CPU 或内存是独占的,则它们可能不会与任何同级重叠。
这些规则以及 cpusets 的自然层次结构,可以有效地强制执行独占保证,而无需每次更改其中任何一个时都扫描所有 cpusets,以确保没有任何内容与独占 cpuset 重叠。 此外,使用 Linux 虚拟文件系统 (vfs) 来表示 cpuset 层次结构为 cpusets 提供了熟悉的权限和名称空间,并具有最少的额外内核代码。
根 (top_cpuset) cpuset 中的 cpus 和 mems 文件是只读的。 cpus 文件使用 CPU 热插拔通知器自动跟踪 cpu_online_mask 的值,mems 文件使用 cpuset_track_online_nodes() 钩子自动跟踪 node_states[N_MEMORY] 的值,即具有内存的节点。
cpuset.effective_cpus 和 cpuset.effective_mems 文件通常分别是 cpuset.cpus 和 cpuset.mems 文件的只读副本。 如果 cpuset cgroup 文件系统以特殊的“cpuset_v2_mode”选项挂载,这些文件的行为将变得类似于 cpuset v2 中的相应文件。 换句话说,热插拔事件不会更改 cpuset.cpus 和 cpuset.mems。 这些事件只会影响 cpuset.effective_cpus 和 cpuset.effective_mems,它们显示此 cpuset 当前正在使用的实际 CPU 和内存节点。 有关 cpuset v2 行为的更多信息,请参阅 控制组 v2。
1.4 什么是独占 cpusets?¶
如果 cpuset 是 CPU 或内存独占的,则除了直接祖先或后代之外,没有其他 cpuset 可以共享任何相同的 CPU 或内存节点。
cpuset.mem_exclusive *或* cpuset.mem_hardwall 的 cpuset 是“硬隔离的”,即它限制了内核对页面、缓冲区和其他内核通常在多个用户之间共享的数据的分配。 所有 cpusets,无论是否硬隔离,都限制了用户空间的内存分配。 这使得可以配置一个系统,以便多个独立的作业可以共享常见的内核数据(例如,文件系统页面),同时将每个作业的用户分配隔离在其自己的 cpuset 中。 为此,构造一个大型 mem_exclusive cpuset 来容纳所有作业,并为每个单独的作业构造子级、非 mem_exclusive cpusets。 即使是 mem_exclusive cpuset,也只允许在外部获取少量的典型内核内存,例如来自中断处理程序的请求。
1.5 什么是 memory_pressure?¶
cpuset 的 memory_pressure 提供了一个简单的每 cpuset 指标,用于衡量 cpuset 中的任务尝试释放 cpuset 节点上的使用中内存以满足其他内存请求的速率。
这使批处理管理器可以监控在专用 cpusets 中运行的作业,以有效地检测该作业导致的内存压力级别。
这在严格管理的系统上运行各种提交的作业混合时很有用,这些系统可以选择终止或重新确定优先级,以便作业尝试使用的内存超过分配给它们的节点上的允许内存,并且在紧密耦合的、长时间运行的大规模并行科学计算作业中,如果它们开始使用的内存超过分配给它们的内存,将无法达到所需的性能目标。
这种机制为批处理管理器提供了一种非常经济的方式来监控 cpuset 的内存压力迹象。 由批处理管理器或其他用户代码决定如何处理它并采取行动。
- ==>
除非通过将“1”写入特殊文件 /dev/cpuset/memory_pressure_enabled 来启用此功能,否则 __alloc_pages() 的重新平衡代码中的此指标的钩子会简化为仅注意到 cpuset_memory_pressure_enabled 标志为零。 因此,只有启用此功能的系统才会计算该指标。
为什么是每 cpuset 的运行平均值
因为此仪表是每 cpuset 的,而不是每任务或 mm 的,因此批量调度程序监控此指标所施加的系统负载在大型系统上会急剧减少,因为可以避免每次查询时扫描任务列表。
因为此仪表是运行平均值,而不是累积计数器,所以批量调度程序可以通过一次读取来检测内存压力,而不是必须读取和累积一段时间的结果。
因为此仪表是每 cpuset 的,而不是每任务或 mm 的,所以批量调度程序可以通过一次读取获得关键信息(cpuset 中的内存压力),而不是必须查询和累积 cpuset 中所有(动态变化的)任务集的结果。
如果附加到该 cpuset 的任何任务进入同步(直接)页面回收代码,则会保留一个每 cpuset 的简单数字过滤器(每个 cpuset 需要一个自旋锁和 3 个数据字),并由该 cpuset 更新。
每 cpuset 文件提供一个整数,表示由 cpuset 中的任务引起的最近(半衰期为 10 秒)的直接页面回收速率,以每秒尝试的回收次数(乘以 1000)为单位。
1.6 什么是内存扩散?¶
每个 cpuset 有两个布尔标志文件,用于控制内核在何处为文件系统缓冲区和相关的内核数据结构分配页面。 它们被称为“cpuset.memory_spread_page”和“cpuset.memory_spread_slab”。
如果每 cpuset 的布尔标志文件“cpuset.memory_spread_page”已设置,则内核会将文件系统缓冲区(页面缓存)均匀地分布在错误任务允许使用的所有节点上,而不是倾向于将这些页面放在任务正在运行的节点上。
如果每 cpuset 的布尔标志文件“cpuset.memory_spread_slab”已设置,则内核会将一些文件系统相关的 slab 缓存(例如 inodes 和 dentries)均匀地分布在错误任务允许使用的所有节点上,而不是倾向于将这些页面放在任务正在运行的节点上。
这些标志的设置不会影响任务的匿名数据段或堆栈段页面。
默认情况下,两种内存扩散都已关闭,并且内存页面分配在任务正在运行的本地节点上,除非受到任务的 NUMA 内存策略或 cpuset 配置的修改,只要有足够的可用空闲内存页面即可。
创建新的 cpusets 时,它们会继承其父级的内存扩散设置。
设置内存扩散会导致受影响的页面或 slab 缓存的分配忽略任务的 NUMA 内存策略并改为扩散。 使用 mbind() 或 set_mempolicy() 调用来设置 NUMA 内存策略的任务不会注意到这些调用由于其包含任务的内存扩散设置而发生的任何更改。 如果内存扩散已关闭,则当前指定的 NUMA 内存策略再次适用于内存页面分配。
“cpuset.memory_spread_page”和“cpuset.memory_spread_slab”都是布尔标志文件。 默认情况下,它们包含“0”,这意味着该功能对于该 cpuset 已关闭。 如果将“1”写入该文件,则会打开命名的功能。
实现很简单。
设置标志“cpuset.memory_spread_page”会为位于该 cpuset 中或随后加入该 cpuset 的每个任务打开每进程标志 PFA_SPREAD_PAGE。 修改页面缓存的页面分配调用以执行此 PFA_SPREAD_PAGE 任务标志的内联检查,如果设置,则对新例程 cpuset_mem_spread_node() 的调用会返回首选的分配节点。
类似地,设置“cpuset.memory_spread_slab”会打开标志 PFA_SPREAD_SLAB,并且适当标记的 slab 缓存将从 cpuset_mem_spread_node() 返回的节点分配页面。
cpuset_mem_spread_node() 例程也很简单。 它使用每任务转子 cpuset_mem_spread_rotor 的值来选择当前任务的 mems_allowed 中的下一个首选分配节点。
这种内存放置策略也称为(在其他上下文中)轮询或交错。
此策略可以为需要在相应节点上放置线程本地数据的作业提供显着改进,但需要访问需要在作业 cpuset 中的几个节点上分布的大型文件系统数据集才能适应。 如果没有此策略,特别是对于可能有一个线程读入数据集的作业,则作业 cpuset 中跨节点的内存分配可能会变得非常不均匀。
1.7 什么是 sched_load_balance?¶
内核调度程序 (kernel/sched/core.c) 会自动负载平衡任务。 如果一个 CPU 未充分利用,则在该 CPU 上运行的内核代码将寻找其他过载的 CPU 上的任务,并将这些任务移动到自身,但这受 cpuset 和 sched_setaffinity 等放置机制的约束。
负载平衡的算法成本及其对关键共享内核数据结构(例如任务列表)的影响会随着要平衡的 CPU 数量呈非线性增长。 因此,调度程序支持将系统 CPU 分区为多个调度域,以便它仅在每个调度域内进行负载平衡。 每个调度域覆盖系统 CPU 的某个子集; 没有两个调度域重叠; 有些 CPU 可能不在任何调度域中,因此不会进行负载平衡。
简而言之,在两个较小的调度域之间进行平衡比在一个大的调度域中进行平衡成本更低,但这样做意味着两个域之一中的过载不会负载平衡到另一个域。
默认情况下,有一个调度域覆盖所有 CPU,包括使用内核启动时“isolcpus=”参数标记为隔离的 CPU。 但是,隔离的 CPU 不会参与负载平衡,并且除非明确分配,否则不会在其上运行任务。
默认的跨所有 CPU 的负载平衡不适用于以下两种情况
在大型系统上,跨多个 CPU 进行负载平衡的成本很高。 如果系统使用 cpusets 管理以将独立的作业放置在单独的 CPU 集上,则无需完全负载平衡。
在某些 CPU 上支持实时的系统需要最大限度地减少这些 CPU 上的系统开销,包括避免不需要的任务负载平衡。
当启用每 cpuset 标志“cpuset.sched_load_balance”(默认设置)时,它会请求该 cpusets 允许的“cpuset.cpus”中的所有 CPU 都包含在单个调度域中,从而确保负载平衡可以将任务(不由 sched_setaffinity 以其他方式固定)从该 cpuset 中的任何 CPU 移动到任何其他 CPU。
当禁用每 cpuset 标志“cpuset.sched_load_balance”时,调度程序将避免跨该 cpuset 中的 CPU 进行负载平衡,--除非--为了满足某些重叠 cpuset 启用了“sched_load_balance”这一需求。
因此,例如,如果顶部 cpuset 启用了标志“cpuset.sched_load_balance”,则调度程序将拥有一个覆盖所有 CPU 的调度域,并且任何其他 cpusets 中“cpuset.sched_load_balance”标志的设置都无关紧要,因为我们已经在进行完全负载平衡。
因此,在上述两种情况下,应禁用顶部 cpuset 标志“cpuset.sched_load_balance”,并且只有一些较小的子 cpuset 启用了此标志。
执行此操作时,您通常不希望在顶部 cpuset 中留下任何可能使用大量 CPU 的未固定任务,因为此类任务可能会人为地限制为 CPU 的某个子集,具体取决于后代 cpusets 中此标志设置的细节。 即使此类任务可以使用某些其他 CPU 中的空闲 CPU 周期,内核调度程序也可能不会考虑将该任务负载平衡到未充分利用的 CPU 的可能性。
当然,可以将固定到特定 CPU 的任务留在禁用“cpuset.sched_load_balance”的 cpuset 中,因为这些任务无论如何都不会去其他地方。
在 cpusets 和调度域之间存在阻抗不匹配。 Cpusets 是分层的和嵌套的。 调度域是扁平的; 它们不重叠,并且每个 CPU 最多位于一个调度域中。
调度域必须是扁平的,因为跨部分重叠的 CPU 集进行负载平衡可能会导致不稳定的动态,这将超出我们的理解。 因此,如果两个部分重叠的 cpusets 都启用了标志“cpuset.sched_load_balance”,则我们形成一个单个调度域,该域是两者的超集。 我们不会将任务移动到其 cpuset 之外的 CPU,但调度程序负载平衡代码可能会浪费一些计算周期来考虑这种可能性。
这种不匹配是为什么在哪些 cpusets 启用了标志“cpuset.sched_load_balance”与调度域配置之间没有简单的一对一关系。 如果 cpuset 启用了该标志,它将在其所有 CPU 上进行平衡,但如果它禁用了该标志,则只有在没有其他重叠的 cpuset 启用该标志时,才能确保没有负载平衡。
如果两个 cpusets 具有部分重叠的“cpuset.cpus”允许的 CPU,并且只有其中一个启用了此标志,则另一个可能会发现其任务仅部分负载平衡,仅在重叠的 CPU 上。 这只是上面给出的一些段落的 top_cpuset 示例的常见情况。 在一般情况下,就像在顶部 cpuset 情况下一样,不要将可能使用大量 CPU 的任务留在这种部分负载平衡的 cpusets 中,因为由于缺少到其他 CPU 的负载平衡,它们可能会人为地限制为允许它们使用的 CPU 的某个子集。
“cpuset.isolcpus”中的 CPU 已通过 isolcpus= 内核引导选项从负载平衡中排除,并且无论任何 cpuset 中的“cpuset.sched_load_balance”的值如何,都永远不会进行负载平衡。
1.7.1 sched_load_balance 实现细节。¶
每 cpuset 标志“cpuset.sched_load_balance”默认为启用(与大多数 cpuset 标志相反)。 启用 cpuset 时,内核将确保它可以跨该 cpuset 中的所有 CPU 进行负载平衡(确保该 cpuset 的 cpus_allowed 中的所有 CPU 都位于同一调度域中)。
如果两个重叠的 cpusets 都启用了“cpuset.sched_load_balance”,则它们将(必须)位于同一调度域中。
如果顶部 cpuset(如默认情况)启用了“cpuset.sched_load_balance”,则根据上述内容,这意味着存在一个覆盖整个系统的调度域,而与任何其他 cpuset 设置无关。
内核承诺用户空间,它将在尽可能的情况下避免负载平衡。 它将选择调度域的最精细粒度分区,同时仍为允许具有“cpuset.sched_load_balance”的 cpuset 的任何 CPU 集提供负载平衡。
内部内核 cpuset 到调度程序的接口将系统中负载平衡的 CPU 的分区从 cpuset 代码传递到调度程序代码。 此分区是 CPU 的子集集(表示为 struct cpumask 数组),成对不相交,覆盖所有必须进行负载平衡的 CPU。
每当以下情况发生时,cpuset 代码都会构建一个新的此类分区并将其传递给调度程序调度域设置代码,以便根据需要重建调度域
具有非空 CPU 的 cpuset 的“cpuset.sched_load_balance”标志发生更改,
或 CPU 进入或离开启用了此标志的 cpuset,
或具有非空 CPU 且启用了此标志的 cpuset 的“cpuset.sched_relax_domain_level”值发生更改,
或删除了具有非空 CPU 且启用了此标志的 cpuset,
或 CPU 已离线/上线。
此分区精确定义了调度程序应设置的调度域 - 分区中每个元素(struct cpumask)一个调度域。
调度程序会记住当前活动的调度域分区。 当从 cpuset 代码调用调度程序例程 partition_sched_domains() 以更新这些调度域时,它会将请求的新分区与当前分区进行比较,并更新其调度域,删除旧的并添加新的,以进行每次更改。
1.8 什么是 sched_relax_domain_level?¶
在调度域中,调度程序以两种方式迁移任务; 滴答声上的定期负载平衡,以及在某些调度事件时。
唤醒任务时,调度程序会尝试将任务移动到空闲 CPU 上。 例如,如果在 CPU X 上运行的任务 A 激活了同一 CPU X 上的另一个任务 B,并且如果 CPU Y 是 X 的同级并且正在执行空闲,则调度程序会将任务 B 迁移到 CPU Y,以便任务 B 可以在 CPU Y 上启动,而无需等待 CPU X 上的任务 A。
并且如果 CPU 在其运行队列中耗尽任务,则 CPU 会尝试从其他繁忙的 CPU 拉取额外的任务以帮助它们,然后再进入空闲状态。
当然,找到可移动的任务和/或空闲 CPU 需要一些搜索成本,调度程序可能不会每次都搜索域中的所有 CPU。 实际上,在某些体系结构中,事件上的搜索范围限制在 CPU 所在的同一套接字或节点中,而滴答声上的负载平衡会搜索所有 CPU。
例如,假设 CPU Z 离 CPU X 相对较远。即使 CPU Z 处于空闲状态,而 CPU X 和其兄弟 CPU 处于繁忙状态,调度器也无法将唤醒的任务 B 从 X 迁移到 Z,因为它超出了其搜索范围。 结果,CPU X 上的任务 B 需要等待任务 A 完成,或者等待下一个时钟节拍的负载均衡。 对于某些特殊情况下的应用程序,等待一个时钟节拍可能太长。
‘cpuset.sched_relax_domain_level’ 文件允许你根据需要请求更改此搜索范围。 该文件接受整数值,该值指示搜索范围的大小,级别大致如下,否则初始值为 -1,表示 cpuset 没有请求。
-1 |
没有请求。 使用系统默认值或遵循其他请求。 |
0 |
没有搜索。 |
1 |
搜索兄弟(核心中的超线程)。 |
2 |
搜索包中的核心。 |
3 |
搜索节点中的 CPU [= 在非 NUMA 系统上是系统范围的] |
4 |
搜索节点块中的节点 [在 NUMA 系统上] |
5 |
系统范围搜索 [在 NUMA 系统上] |
并非所有级别都存在,并且值可能会因系统架构和内核配置而异。 检查 /sys/kernel/debug/sched/domains/cpu*/domain*/ 获取系统特定详细信息。
系统默认值取决于架构。 可以使用 relax_domain_level= 启动参数更改系统默认值。
此文件是每个 cpuset 的,并且会影响 cpuset 所属的调度域。 因此,如果禁用了 cpuset 的标志 ‘cpuset.sched_load_balance’,则 ‘cpuset.sched_relax_domain_level’ 不起作用,因为没有属于该 cpuset 的调度域。
如果多个 cpusets 重叠,因此它们形成单个调度域,则使用其中最大的值。 请注意,如果一个请求 0,而其他请求 -1,则使用 0。
请注意,修改此文件会产生好的和坏的影响,是否可以接受取决于你的情况。 如果不确定,请勿修改此文件。
如果你的情况是:
由于你的特殊应用程序行为或对 CPU 缓存的特殊硬件支持等原因,可以认为每个 CPU 之间的迁移成本非常小(对你而言)。
搜索成本没有影响(对你而言),或者你可以通过管理 cpuset 使其紧凑等方式来使搜索成本足够小。
即使牺牲缓存命中率等,也需要延迟。那么增加 ‘sched_relax_domain_level’ 会对你有利。
1.9 如何使用 cpusets?¶
为了尽量减少 cpusets 对关键内核代码(如调度程序)的影响,并且由于内核不支持一个任务直接更新另一个任务的内存放置,因此更改任务的 cpuset CPU 或内存节点放置,或更改任务附加到的 cpuset,对任务的影响是微妙的。
如果一个 cpuset 的内存节点被修改,那么对于附加到该 cpuset 的每个任务,内核下次尝试为该任务分配内存页面时,内核将注意到任务 cpuset 中的更改,并更新其每个任务的内存放置,以保持在新 cpusets 内存放置范围内。 如果任务正在使用 mempolicy MPOL_BIND,并且其绑定的节点与新的 cpuset 重叠,那么该任务将继续使用新的 cpuset 中仍然允许的 MPOL_BIND 节点的任何子集。 如果任务正在使用 MPOL_BIND,并且现在新的 cpuset 中不允许其任何 MPOL_BIND 节点,那么该任务将基本上被视为 MPOL_BIND 绑定到新的 cpuset(即使其 NUMA 放置,如 get_mempolicy() 查询的那样,没有更改)。 如果一个任务从一个 cpuset 移动到另一个 cpuset,那么内核将调整任务的内存放置,如上所述,在内核下次尝试为该任务分配内存页面时。
如果一个 cpuset 的 ‘cpuset.cpus’ 被修改,那么该 cpuset 中的每个任务都将立即更改其允许的 CPU 放置。 同样,如果一个任务的 pid 被写入另一个 cpuset 的 ‘tasks’ 文件,那么它允许的 CPU 放置也会立即更改。 如果此类任务已使用 sched_setaffinity() 调用绑定到其 cpuset 的某个子集,则该任务将被允许在新的 cpuset 中允许的任何 CPU 上运行,从而否定先前 sched_setaffinity() 调用的效果。
总而言之,在下次为任务分配页面时,内核会更新更改了 cpuset 的任务的内存放置,并立即更新处理器放置。
通常,一旦分配了一个页面(给定了主内存的物理页面),那么只要它保持分配状态,该页面就会保留在其分配的任何节点上,即使 cpusets 内存放置策略 ‘cpuset.mems’ 随后发生更改。 如果 cpuset 标志文件 ‘cpuset.memory_migrate’ 设置为 true,那么当任务附加到该 cpuset 时,该任务之前在其先前 cpuset 的节点上分配给它的任何页面都会迁移到任务的新 cpuset。 如果可能,在这些迁移操作期间会保留页面在 cpuset 中的相对位置。 例如,如果页面位于先前 cpuset 的第二个有效节点上,那么该页面将放置在新 cpuset 的第二个有效节点上。
此外,如果 ‘cpuset.memory_migrate’ 设置为 true,那么如果该 cpuset 的 ‘cpuset.mems’ 文件被修改,分配给该 cpuset 中任务的页面,这些页面位于 ‘mems’ 的先前设置中的节点上,将被移动到 ‘mems’ 的新设置中的节点。 不在该任务的先前 cpuset 中,或在 cpuset 的先前 ‘cpuset.mems’ 设置中的页面,将不会被移动。
以上存在一个例外。 如果使用热插拔功能删除当前分配给 cpuset 的所有 CPU,那么该 cpuset 中的所有任务将被移动到具有非空 cpus 的最近祖先。 但是,如果 cpuset 与另一个 cgroup 子系统绑定,并且该子系统对任务附加有一些限制,那么移动某些(或全部)任务可能会失败。 在这种失败的情况下,这些任务将保留在原始 cpuset 中,并且内核将自动更新其 cpus_allowed 以允许所有在线 CPU。 当用于删除内存节点的内存热插拔功能可用时,预计也会应用类似的例外。 一般来说,内核宁愿违反 cpuset 放置,也不愿让所有允许的 CPU 或内存节点都已离线的任务挨饿。
以上存在第二个例外。 GFP_ATOMIC 请求是内核内部分配,必须立即满足。 如果 GFP_ATOMIC 分配失败,内核可能会删除一些请求,在极少数情况下甚至会崩溃。 如果请求无法在当前任务的 cpuset 中得到满足,那么我们会放宽 cpuset,并在我们可以找到它的任何地方寻找内存。 违反 cpuset 比使内核承受压力更好。
要启动一个将包含在 cpuset 中的新作业,步骤如下:
mkdir /sys/fs/cgroup/cpuset
mount -t cgroup -ocpuset cpuset /sys/fs/cgroup/cpuset
通过在 /sys/fs/cgroup/cpuset 虚拟文件系统中执行 mkdir 和 write(或 echo)来创建新的 cpuset。
启动一个将成为新作业的“创始之父”的任务。
通过将其 pid 写入该 cpuset 的 /sys/fs/cgroup/cpuset tasks 文件,将该任务附加到新的 cpuset。
从这个创始之父任务 fork、exec 或 clone 作业任务。
例如,以下命令序列将设置一个名为 “Charlie” 的 cpuset,其中仅包含 CPU 2 和 3,以及内存节点 1,然后在该 cpuset 中启动一个子 shell ‘sh’
mount -t cgroup -ocpuset cpuset /sys/fs/cgroup/cpuset
cd /sys/fs/cgroup/cpuset
mkdir Charlie
cd Charlie
/bin/echo 2-3 > cpuset.cpus
/bin/echo 1 > cpuset.mems
/bin/echo $$ > tasks
sh
# The subshell 'sh' is now running in cpuset Charlie
# The next line should display '/Charlie'
cat /proc/self/cpuset
有多种方法可以查询或修改 cpusets
直接通过 cpuset 文件系统,使用 shell 中的各种 cd、mkdir、echo、cat、rmdir 命令,或 C 中的等效命令。
通过 C 库 libcpuset。
通过 C 库 libcgroup。 (https://github.com/libcgroup/libcgroup/)
通过 python 应用程序 cset。 (http://code.google.com/p/cpuset/)
sched_setaffinity 调用也可以在 shell 提示符下完成,使用 SGI 的 runon 或 Robert Love 的 taskset。 mbind 和 set_mempolicy 调用可以在 shell 提示符下完成,使用 numactl 命令(Andi Kleen 的 numa 包的一部分)。
2. 用法示例和语法¶
2.1 基本用法¶
可以通过 cpuset 虚拟文件系统创建、修改和使用 cpusets。
要挂载它,请键入:# mount -t cgroup -o cpuset cpuset /sys/fs/cgroup/cpuset
然后在 /sys/fs/cgroup/cpuset 下,你可以找到一个树,它对应于系统中 cpusets 的树。 例如,/sys/fs/cgroup/cpuset 是保存整个系统的 cpuset。
如果你想在 /sys/fs/cgroup/cpuset 下创建一个新的 cpuset
# cd /sys/fs/cgroup/cpuset
# mkdir my_cpuset
现在你想用这个 cpuset 做点什么
# cd my_cpuset
在这个目录中你可以找到几个文件
# ls
cgroup.clone_children cpuset.memory_pressure
cgroup.event_control cpuset.memory_spread_page
cgroup.procs cpuset.memory_spread_slab
cpuset.cpu_exclusive cpuset.mems
cpuset.cpus cpuset.sched_load_balance
cpuset.mem_exclusive cpuset.sched_relax_domain_level
cpuset.mem_hardwall notify_on_release
cpuset.memory_migrate tasks
读取它们将为你提供有关此 cpuset 状态的信息:它可以使用的 CPU 和内存节点、正在使用它的进程及其属性。 通过写入这些文件,你可以操作 cpuset。
设置一些标志
# /bin/echo 1 > cpuset.cpu_exclusive
添加一些 cpus
# /bin/echo 0-7 > cpuset.cpus
添加一些 mems
# /bin/echo 0-7 > cpuset.mems
现在将你的 shell 附加到这个 cpuset
# /bin/echo $$ > tasks
你也可以通过在此目录中使用 mkdir 在你的 cpuset 中创建 cpusets
# mkdir my_sub_cs
要删除 cpuset,只需使用 rmdir
# rmdir my_sub_cs
如果 cpuset 正在使用中(内部有 cpusets,或者附加了进程),这将失败。
请注意,由于历史原因,“cpuset” 文件系统作为 cgroup 文件系统的包装存在。
命令
mount -t cpuset X /sys/fs/cgroup/cpuset
相当于
mount -t cgroup -ocpuset,noprefix X /sys/fs/cgroup/cpuset
echo "/sbin/cpuset_release_agent" > /sys/fs/cgroup/cpuset/release_agent
2.2 添加/删除 cpus¶
这是在 cpuset 目录中的 cpus 或 mems 文件中写入时使用的语法
# /bin/echo 1-4 > cpuset.cpus -> set cpus list to cpus 1,2,3,4
# /bin/echo 1,2,3,4 > cpuset.cpus -> set cpus list to cpus 1,2,3,4
要将 CPU 添加到 cpuset,请写入包含要添加的 CPU 的新 CPU 列表。 要将 6 添加到上面的 cpuset
# /bin/echo 1-4,6 > cpuset.cpus -> set cpus list to cpus 1,2,3,4,6
类似地,要从 cpuset 中删除 CPU,请写入不包含要删除的 CPU 的新 CPU 列表。
要删除所有 CPU
# /bin/echo "" > cpuset.cpus -> clear cpus list
2.3 设置标志¶
语法非常简单
# /bin/echo 1 > cpuset.cpu_exclusive -> set flag 'cpuset.cpu_exclusive'
# /bin/echo 0 > cpuset.cpu_exclusive -> unset flag 'cpuset.cpu_exclusive'
2.4 附加进程¶
# /bin/echo PID > tasks
请注意,它是 PID,而不是 PIDs。 你一次只能附加一个任务。 如果你有多个任务要附加,你必须一个接一个地完成
# /bin/echo PID1 > tasks
# /bin/echo PID2 > tasks
...
# /bin/echo PIDn > tasks
3. 问题¶
- Q
这个 ‘/bin/echo’ 是怎么回事?
- A
bash 的内置 ‘echo’ 命令不会检查对 write() 的调用是否存在错误。 如果你在 cpuset 文件系统中使用它,你将无法判断命令是成功还是失败。
- Q
当我附加进程时,只有该行的第一个进程真正被附加!
- A
我们每次调用 write() 只能返回一个错误代码。 因此,你也应该只放一个 pid。