CPUSETS

Copyright (C) 2004 BULL SA.

作者:Simon.Derr@bull.net

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 标志:如果设置,将页面移动到 cpusets 节点

  • 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 的命名层次结构允许将大型系统划分为嵌套的、可动态更改的“软分区”。

每个任务都附加到一个 cpuset,并且该任务的任何子任务在 fork 时都会自动继承此附加关系。这种机制允许将系统上的工作负载组织成相关的任务集,从而使每个集合都被限制为使用特定 cpuset 的 CPU 和内存节点。如果必要的 cpuset 文件系统目录的权限允许,任务可以重新附加到任何其他 cpuset。

这种“大规模”的系统管理可以与使用 sched_setaffinity、mbind 和 set_mempolicy 系统调用对单个任务和内存区域进行的详细放置无缝集成。

以下规则适用于每个 cpuset:

  • 它的 CPU 和内存节点必须是其父级的子集。

  • 除非其父级是独占的,否则它不能被标记为独占。

  • 如果它的 CPU 或内存是独占的,则它们不得与任何同级重叠。

这些规则以及 cpusets 的自然层次结构,可以有效地强制执行独占保证,而无需每次更改任何 cpuset 时都扫描所有 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,如果它是 cpuset.mem_exclusive *或* cpuset.mem_hardwall,则它将是“硬隔离”的,也就是说,它限制了内核对页面、缓冲区以及其他通常由内核在多个用户之间共享的数据的分配。所有 cpusets,无论是否硬隔离,都会限制用户空间内存的分配。这使得可以配置系统,以便多个独立的作业可以共享常见的内核数据(例如文件系统页面),同时将每个作业的用户分配隔离在其自己的 cpuset 中。为此,构建一个大型的 mem_exclusive cpuset 来容纳所有作业,并为每个单独的作业构建子级、非 mem_exclusive 的 cpusets。只允许少量典型的内核内存(例如来自中断处理程序的请求)在 mem_exclusive cpuset 之外获取。

1.5 什么是 memory_pressure?

cpuset 的 memory_pressure 提供了一个简单的每个 cpuset 的指标,该指标表示 cpuset 中的任务尝试释放 cpuset 节点上正在使用的内存以满足其他内存请求的速率。

这使得监视在专用 cpusets 中运行的作业的批处理管理器能够有效地检测该作业正在导致的内存压力级别。

这在严格管理的系统上运行各种提交的作业非常有用,这些作业可以选择终止或重新优先化尝试使用超过分配给它们的节点的允许内存的作业,对于紧密耦合、长时间运行、大规模并行科学计算作业也是如此,如果它们开始使用超过允许给它们的内存,则这些作业将无法达到所需的性能目标。

此机制为批处理管理器提供了一种非常经济的方式来监视 cpuset 中的内存压力迹象。是否对其采取措施以及采取什么措施取决于批处理管理器或其他用户代码。

==>

除非通过向特殊文件 /dev/cpuset/memory_pressure_enabled 写入“1”来启用此功能,否则 __alloc_pages() 的重新平衡代码中的此指标的钩子将简化为仅注意到 cpuset_memory_pressure_enabled 标志为零。因此,只有启用此功能的系统才会计算该指标。

为什么是每个 cpuset 的运行平均值

因为此计数器是每个 cpuset 的,而不是每个任务或 mm 的,所以在大型系统上,批处理调度程序监视此指标所施加的系统负载会大大减少,因为在每次查询时都可以避免扫描任务列表。

因为此计数器是一个运行平均值,而不是一个累加计数器,所以批处理调度程序可以通过一次读取来检测内存压力,而不必读取和累加一段时间的结果。

因为此计数器是每个 cpuset 的,而不是每个任务或 mm 的,所以批处理调度程序可以通过一次读取获得关键信息(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 的默认负载均衡不适用于以下两种情况:

  1. 在大型系统中,跨多个 CPU 进行负载均衡的开销很大。如果系统使用 cpusets 将独立的作业放置在单独的 CPU 集上进行管理,则完全负载均衡是不必要的。

  2. 在某些 CPU 上支持实时的系统需要最大限度地减少这些 CPU 上的系统开销,包括在不需要时避免任务负载均衡。

当启用每个 cpuset 的标志 “cpuset.sched_load_balance”(默认设置)时,它会请求该 cpuset 允许的 ‘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 的调度域,并且任何其他 cpuset 中 “cpuset.sched_load_balance” 标志的设置都无关紧要,因为我们已经完全负载均衡。

因此,在上述两种情况下,应禁用顶层 cpuset 标志 “cpuset.sched_load_balance”,并且只有一些较小的子 cpuset 启用此标志。

执行此操作时,通常不希望在顶层 cpuset 中留下任何可能使用大量 CPU 的未固定任务,因为此类任务可能会人为地限制在某些 CPU 子集上,具体取决于后代 cpuset 中此标志设置的细节。即使这样的任务可以使用某些其他 CPU 中的空闲 CPU 周期,内核调度程序也可能不会考虑将该任务负载均衡到未充分利用的 CPU 的可能性。

当然,固定到特定 CPU 的任务可以留在禁用 “cpuset.sched_load_balance” 的 cpuset 中,因为这些任务无论如何都不会移动到其他地方。

这里存在 cpuset 和调度域之间的阻抗失配。Cpusets 是分层的并嵌套。调度域是扁平的;它们不重叠,并且每个 CPU 最多位于一个调度域中。

调度域必须是扁平的,因为跨部分重叠的 CPU 集进行负载均衡会带来不稳定的动态风险,而这些动态将超出我们的理解范围。因此,如果两个部分重叠的 cpuset 都启用了标志 ‘cpuset.sched_load_balance’,那么我们将形成一个包含两者的超集的单个调度域。我们不会将任务移动到其 cpuset 之外的 CPU,但是调度程序负载均衡代码可能会浪费一些计算周期来考虑这种可能性。

这种不匹配是为什么启用标志 “cpuset.sched_load_balance” 的 cpuset 与调度域配置之间没有简单的一对一关系的原因。如果 cpuset 启用该标志,它将在其所有 CPU 上进行负载均衡,但如果禁用该标志,则只有在没有其他重叠的 cpuset 启用该标志时才能确保不进行负载均衡。

如果两个 cpuset 具有部分重叠的允许的 ‘cpuset.cpus’,并且只有其中一个启用了此标志,则另一个可能会发现其任务仅在重叠的 CPU 上部分负载均衡。这只是上面几个段落中给出的 top_cpuset 示例的一般情况。在一般情况下,就像在顶层 cpuset 情况中一样,不要在部分负载均衡的 cpuset 中留下可能使用大量 CPU 的任务,因为由于缺乏与其他 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 都位于同一调度域中)。

如果两个重叠的 cpuset 都启用了 ‘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 X 和同级繁忙时 CPU Z 空闲,调度程序也无法将唤醒的任务 B 从 X 迁移到 Z,因为它超出了其搜索范围。因此,CPU X 上的任务 B 需要等待任务 A 或等待下一个时钟节拍上的负载均衡。对于某些特殊情况下的应用程序,等待 1 个时钟节拍可能太长。

‘cpuset.sched_relax_domain_level’ 文件允许您根据需要请求更改此搜索范围。此文件采用 int 值,该值表示搜索范围的大小,级别大致如下,否则初始值 -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 的调度域。

如果多个 cpuset 重叠,因此它们形成一个调度域,则使用其中最大的值。请注意,如果一个请求 0,而其他请求 -1,则使用 0。

请注意,修改此文件会产生好的和坏的影响,而是否可以接受取决于您的情况。如果您不确定,请不要修改此文件。

如果您的处境是

  • 由于您特殊应用程序的行为或对 CPU 缓存等的特殊硬件支持,可以认为每个 CPU 之间的迁移成本相当小(对您而言)。

  • 搜索成本没有影响(对您而言),或者您可以通过管理 cpuset 来压缩等方式使搜索成本足够小。

  • 即使牺牲缓存命中率等,也需要延迟,那么增加 ‘sched_relax_domain_level’ 将对您有好处。

1.9 如何使用 cpusets?

为了最大限度地减少 cpusets 对关键内核代码(如调度程序)的影响,并且由于内核不支持一个任务直接更新另一个任务的内存放置,因此更改其 cpuset CPU 或内存节点放置,或者更改任务所附加到的 cpuset 对任务的影响是微妙的。

如果 cpuset 的内存节点被修改,则对于附加到该 cpuset 的每个任务,下次内核尝试为该任务分配内存页时,内核会注意到该任务的 cpuset 中的更改,并更新其每个任务的内存放置以保持在新 cpuset 的内存放置内。如果任务使用的是 mempolicy MPOL_BIND,并且它绑定到的节点与其新的 cpuset 重叠,那么该任务将继续使用新 cpuset 中仍然允许的任何 MPOL_BIND 节点子集。如果任务使用的是 MPOL_BIND,并且现在新的 cpuset 中不允许其任何 MPOL_BIND 节点,那么该任务实际上将被视为 MPOL_BIND 绑定到新的 cpuset(即使通过 get_mempolicy() 查询的其 NUMA 放置没有改变)。如果一个任务从一个 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 中任务的、且位于 'cpuset.mems' 先前设置中的节点上的页面将被移动到 'mems' 新设置中的节点上。不在任务先前 cpuset 中或 cpuset 先前 'cpuset.mems' 设置中的页面将不会被移动。

以上情况有一个例外。如果使用热插拔功能删除当前分配给 cpuset 的所有 CPU,那么该 cpuset 中的所有任务都将被移动到具有非空 CPU 的最近的祖先。但是,如果 cpuset 与另一个 cgroup 子系统绑定,而该子系统对任务附加有一些限制,则某些(或全部)任务的移动可能会失败。在这种失败的情况下,这些任务将保留在原始 cpuset 中,内核会自动更新它们的 cpus_allowed,以允许所有在线 CPU。当删除内存节点的热插拔功能可用时,预计也会出现类似的例外情况。一般来说,内核倾向于违反 cpuset 的放置规则,而不是让所有允许的 CPU 或内存节点都被离线的任务处于饥饿状态。

以上情况还有第二个例外。GFP_ATOMIC 请求是内核内部的分配,必须立即满足。如果 GFP_ATOMIC 分配失败,内核可能会在极少数情况下丢弃某些请求,甚至会崩溃。如果请求无法在当前任务的 cpuset 中得到满足,那么我们将放宽 cpuset 的限制,并在我们能找到的任何地方查找内存。违反 cpuset 比给内核带来压力要好。

要启动一个将包含在 cpuset 中的新作业,步骤如下:

  1. mkdir /sys/fs/cgroup/cpuset

  2. mount -t cgroup -ocpuset cpuset /sys/fs/cgroup/cpuset

  3. 通过在 /sys/fs/cgroup/cpuset 虚拟文件系统中执行 mkdir 和 write(或 echo)命令来创建新的 cpuset。

  4. 启动一个将成为新作业“创始者”的任务。

  5. 通过将该任务的 pid 写入该 cpuset 的 /sys/fs/cgroup/cpuset tasks 文件,将该任务附加到新的 cpuset。

  6. 从这个创始者任务 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:

还可以使用 SGI 的 runon 或 Robert Love 的 taskset 在 shell 提示符下完成 sched_setaffinity 调用。可以使用 numactl 命令(Andi Kleen 的 numa 包的一部分)在 shell 提示符下完成 mbind 和 set_mempolicy 调用。

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

添加一些 CPU

# /bin/echo 0-7 > cpuset.cpus

添加一些内存

# /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 添加/删除 CPU

这是在 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

要向 cpuset 添加 CPU,请写入包含要添加的 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. 问题

这个 '/bin/echo' 是怎么回事?

bash 的内置 'echo' 命令不检查对 write() 的调用是否存在错误。如果您在 cpuset 文件系统中使用它,您将无法知道命令是成功还是失败。

当我附加进程时,只有该行的第一个真正被附加!

我们每次调用 write() 只能返回一个错误代码。因此,您也应该只放一个 pid。

4. 联系方式

网址:http://www.bullopensource.org/cpuset