CPU集
版权所有(C)2004 BULL SA。
作者:[email protected]
原文:https://www.kernel.org/doc/Documentation/cgroup-v1/cpusets.txt
部分版权所有(c)2004-2006 Silicon Graphics, Inc。
由Paul Jackson [email protected]修改
由Christoph Lameter [email protected]修改
由Paul Menage [email protected]修改
由Seto Hidetoshi [email protected]修改
1. CPU集
1.1 什么是CPU集?
CPU集提供了一种机制,用于将一组CPU和内存节点分配给一组任务。在本文档中,“内存节点”指的是包含内存的在线节点。
CPU集将任务的CPU和内存放置约束在任务当前CPU集中的资源之内。它们形成了在虚拟文件系统中可见的嵌套层次结构。这些是在大型系统上管理动态作业放置所需的基本钩子,超出了已有的内容。
CPU集使用文档中描述的通用cgroup子系统(请参阅Documentation/cgroup-v1/cgroups.txt)。
任务通过使用sched_setaffinity(2)系统调用来包含CPU亲和掩码中的CPU,并使用mbind(2)和set_mempolicy(2)系统调用来包含内存策略中的内存节点的请求,这两个请求都会通过该任务的CPU集进行过滤,过滤掉任何不在该CPU集中的CPU或内存节点。调度器不会在不允许的CPU上调度任务,内核页分配器不会在请求任务的mems_allowed向量中不允许的节点上分配页面。
用户级代码可以在cgroup虚拟文件系统中按名称创建和销毁CPU集,管理这些CPU集的属性和权限,以及分配给每个CPU集的CPU和内存节点,指定并查询分配给任务的CPU集,以及列出分配给CPU集的任务PID。
1.2 为什么需要CPU集?
管理具有许多处理器(CPU)、复杂的内存缓存层次结构和具有非均匀访问时间(NUMA)的多个内存节点的大型计算机系统对进程的有效调度和内存放置提出了额外的挑战。
通常,规模较小的系统可以通过让操作系统自动在请求的任务之间共享可用的CPU和内存资源来以足够的效率运行。
但是,更大的系统更有利于通过仔细的处理器和内存放置来减少内存访问时间和争用,并且通常代表了客户的更大投资,可以从明确地将作业放置在系统的适当大小的子集上中获益。
这在以下情况下尤其有价值:
- 运行多个相同Web应用程序实例的Web服务器,
- 运行不同应用程序的服务器(例如,Web服务器和数据库),或者
- 运行具有严格性能特性的大型HPC应用程序的NUMA系统。
这些子集或“软分区”必须能够在作业混合发生变化时进行动态调整,而不影响其他同时执行的作业。当内存位置发生变化时,正在运行的作业页面的位置也可能被移动。
内核CPU集补丁提供了有效实现这些子集所需的最低必要内核机制。它利用了Linux内核中现有的CPU和内存放置设施,以避免对关键调度器或内存分配器代码造成任何额外影响。
1.3 CPU集是如何实现的?
CPU集提供了一种Linux内核机制,用于限制进程或一组进程使用的CPU和内存节点。
Linux内核已经有一对机制,用于指定任务可能被调度到哪些CPU上(sched_setaffinity),以及它可能获得内存的哪些内存节点(mbind、set_mempolicy)。
CPU集扩展了这两种机制如下:
- CPU集是内核已知的允许的CPU和内存节点的集合。
- 系统中的每个任务都附加到一个CPU集,通过任务结构中的指向引用计数cgroup结构的指针。
- 对sched_setaffinity的调用被过滤,仅限于在该任务的CPU集中允许的CPU。
- 对mbind和set_mempolicy的调用被过滤,仅限于在该任务的CPU集中允许的内存节点。
- 根CPU集包含所有系统的CPU和内存节点。
- 对于任何CPU集,可以定义包含父CPU和内存节点资源子集的子CPU集。
- CPU集的层次结构可以挂载到/dev/cpuset,以便从用户空间进行浏览和操作。
- CPU集可以标记为独占,这确保没有其他CPU集(除了直接祖先和后代)可能包含任何重叠的CPU或内存节点。
- 您可以列出附加到任何CPU集的所有任务(按pid)。
cpusets 的实现需要在内核的一些简单挂钩中进行,这些挂钩不在性能关键路径上:
- 在 init/main.c 中,在系统引导时初始化根 cpuset。
- 在 fork 和 exit 中,将任务附加和分离到其 cpuset。
- 在 sched_setaffinity 中,通过任务的 cpuset 掩码掩盖请求的 CPU。
- 在 sched.c migrate_live_tasks() 中,在可能的情况下保持任务在其 cpuset 允许的 CPU 内迁移。
- 在 mbind 和 set_mempolicy 系统调用中,通过任务的 cpuset 掩码请求的 Memory Node。
- 在 page_alloc.c 中,将内存限制为允许的节点。
- 在 vmscan.c 中,将页面恢复限制为当前 cpuset。
为了启用浏览和修改内核当前已知的 cpusets,您应该挂载 “cgroup” 文件系统类型。对于 cpusets,没有添加新的系统调用 - 所有查询和修改 cpusets 的支持都通过这个 cpuset 文件系统进行。
每个任务的 /proc/<pid>/status
文件都有四行额外的内容,显示任务的 cpus_allowed(可以调度到哪些 CPU)和 mems_allowed(可以获取内存的哪些 Memory Node),格式如下所示:
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 中的 Memory Node 列表
- 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 标志:如果设置,将 slab 缓存均匀分布在允许的节点上
- cpuset.sched_load_balance 标志:如果设置,负载均衡在该 cpuset 的 CPU 内进行
- cpuset.sched_relax_domain_level:迁移任务时的搜索范围
此外,只有根 cpuset 具有以下文件:
- cpuset.memory_pressure_enabled 标志:计算内存压力?
新的 cpusets 是使用 mkdir 系统调用或 shell 命令创建的。通过写入该 cpuset 目录中适当的文件,例如其标志,允许的 CPU 和 Memory Node,以及附加的任务,可以修改 cpuset 的属性,如上所列。
嵌套 cpusets 的命名分层结构允许将大型系统分割成嵌套的、动态可变的 “软分区”。
每个任务的附加,由该任务的任何子任务在 fork 时自动继承,到一个 cpuset 中允许将系统工作负载组织成相关任务集,以便每个集合都受限于使用特定 cpuset 的 CPU 和 Memory Node。如果权限允许,任务可以重新附加到任何其他 cpuset,前提是必要的 cpuset 文件系统目录。
这样对系统的 “大规模” 管理与使用 sched_setaffinity、mbind 和 set_mempolicy 系统调用对单个任务和内存区域进行详细放置的集成非常平滑。
以下规则适用于每个 cpuset:
- 其 CPU 和 Memory Node 必须是其父节点的子集。
- 它不能被标记为独占,除非其父节点是独占的。
- 如果其 CPU 或内存是独占的,则它们不能与任何同级重叠。
这些规则和 cpusets 的自然层次结构使得可以有效地执行排他性保证,而无需每次任何 cpuset 发生更改时扫描所有 cpusets 来确保没有任何重叠到排他性 cpuset。此外,使用 Linux 虚拟文件系统(vfs)表示 cpuset 层次结构提供了熟悉的权限和名称空间,而只需最少的额外内核代码。
根(top_cpuset)cpuset 中的 cpus 和 mems 文件是只读的。cpus 文件使用 CPU 热插拔通知器自动跟踪 cpu_online_mask 的值,并且 mems 文件使用 cpuset_track_online_nodes() 钩子自动跟踪 node_states[N_MEMORY] 的值—即具有内存的节点。
1.4 什么是独占 cpusets?
如果一个 cpuset 是 CPU 或内存独占的,那么除了直接的祖先或后代之外,没有其他 cpuset 可以共享任何相同的 CPU 或 Memory Node。
一个具有 cpuset.mem_exclusive 或 cpuset.mem_hardwall 的 cpuset 是 “硬墙” 的,即它限制了内核对页、缓冲区和其他在多个用户之间共享的内核数据的分配。所有 cpuset,无论是否是硬墙,都会限制用户空间的内存分配。这使得可以配置系统,以便几个独立的作业可以共享通用的内核数据,例如文件系统页,同时将每个作业的用户分配隔离在其自己的 cpuset 中。为此,构建一个大型的 mem_exclusive cpuset 来容纳所有作业,并为每个单独的作业构建子级、非 mem_exclusive cpuset。即使是 mem_exclusive cpuset,也只允许一小部分典型的内核内存,例如来自中断处理程序的请求,被带到 cpuset 之外。
1.5 什么是内存压力?
一个 cpuset 的 memory_pressure 提供了一个简单的每 cpuset 指标,用于衡量 cpuset 中的任务试图释放使用中内存的速率,以满足额外的内存请求。
这使得批处理管理器能够监控在专用 cpuset 中运行的作业,以有效地检测该作业引起的内存压力水平。
这对于在运行各种提交的作业的严格管理的系统很有用,该系统可能选择终止或重新优先考虑试图使用超过分配给它们的节点上的内存的作业,也适用于紧密耦合、长时间运行、大规模科学计算作业,如果它们开始使用比允许的内存更多的内存,将无法满足所需的性能目标。
这种机制为批处理管理器提供了一种非常经济的方式来监视 cpuset 的内存压力迹象。如何处理以及采取何种行动取决于批处理管理器或其他用户代码。
==> 除非通过将 “1” 写入特殊文件 /dev/cpuset/memory_pressure_enabled
来启用此功能,否则在 __alloc_pages() 重平衡代码中的钩子仅简单地注意到 cpuset_memory_pressure_enabled 标志为零。因此,只有启用此功能的系统才会计算指标。
为什么使用每个 cpuset 的运行平均值:
- 因为这个仪表是每个 cpuset 的,而不是每个任务或 mm,所以在大型系统上,批处理调度器监视这个指标所施加的系统负载大大减少,因为可以避免在每组查询上扫描任务列表。
- 因为这个仪表是一个运行平均值,而不是一个累积计数器,一个批处理调度器可以在一个读取中检测到内存压力,而不是必须在一段时间内读取和累积结果。
- 因为这个仪表是每个 cpuset 而不是每个任务或 mm,批处理调度器可以用一个读取获取关键信息,即 cpuset 中的内存压力,而不是必须在 cpuset 中的所有(动态变化的)任务集上查询和累积结果。
每个 cpuset 维护一个简单的数字滤波器(需要一个自旋锁和每个 cpuset 3 个字的数据),并由附加到该 cpuset 的任何任务更新,如果它进入同步(直接)页面回收代码。
每个 cpuset 文件提供一个整数,表示最近(半衰期为 10 秒)由 cpuset 中的任务引起的直接页面回收率,以每秒尝试的回收单位,乘以 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 mempolicy 或 cpuset 配置修改了这一点,只要有足够的空闲内存页面可用。
创建新的 cpuset 时,它们会继承其父节点的内存分散设置。
设置内存分散会导致受影响的页面或 slab 缓存的分配忽略任务的 NUMA mempolicy 并进行分散分配。使用 mbind() 或 set_mempolicy()
调用设置 NUMA mempolicy 的任务不会因为它们所含任务的内存分散设置而注意到这些调用的任何变化。如果关闭内存分散,则当前指定的 NUMA mempolicy 再次适用于内存页面分配。
‘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() 例程也很简单。它使用每个任务的 mems_allowed 中的下一个节点来选择当前任务的 mems_allowed 中首选的节点。
这种内存放置策略在其他情况下也称为循环轮换或交错。
对于需要将线程本地数据放置在相应节点上的作业,并需要将大型文件系统数据集分布到作业 cpuset 中的几个节点以适应的作业来说,这种策略可以提供显著的改进。没有这种策略,特别是对于可能有一个线程读取数据集的作业来说,作业 cpuset 中节点上的内存分配可能会变得非常不均匀。
1.7 什么是 sched_load_balance?
内核调度程序(kernel/sched/core.c)会自动平衡任务的负载。如果一个 CPU 的利用率较低,运行在该 CPU 上的内核代码将寻找其他负载更重的 CPU 上的任务,并将这些任务移动到自己身上,在 cpusets 和 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 才启用此标志。
在这种情况下,您通常不希望留下任何可能使用大量 CPU 的未固定任务在顶层 cpuset 中,因为根据后代 cpuset 中此标志设置的特定情况,这些任务可能被人为地限制在一些 CPU 子集上。即使这样的任务可以在其他 CPU 上使用空闲的 CPU 周期,内核调度程序也可能不会考虑将该任务负载平衡到未使用的 CPU 上。
当然,固定到特定 CPU 的任务可以留在禁用了 “cpuset.sched_load_balance” 的 cpuset 中,因为这些任务无论如何都不会去其他地方。
在 cpusets 和调度域之间存在阻抗不匹配。Cpusets 是分层的和嵌套的。调度域是平面的;它们不重叠,每个 CPU 最多在一个调度域中。
调度域必须是平面的,因为在部分重叠的 CPU 集合之间进行负载平衡会导致我们理解之外的不稳定动态。因此,如果两个部分重叠的 cpuset 都启用了标志 ‘cpuset.sched_load_balance’,那么我们将形成一个单一的调度域,它是两者的超集。我们不会将任务移动到其 cpuset 之外的 CPU 上,但调度程序负载平衡代码可能会浪费一些计算周期来考虑这种可能性。
这种不匹配是为什么启用了 “cpuset.sched_load_balance” 标志的 cpuset 和调度域配置之间没有简单的一对一关系的原因。如果一个 cpuset 启用了该标志,它将在其所有 CPU 上进行平衡,但如果禁用该标志,则只能保证在没有其他重叠 cpuset 启用该标志的情况下不进行负载平衡。
如果两个 cpuset 具有部分重叠的 ‘cpuset.cpus’,并且只有一个启用了此标志,则另一个可能会发现其任务仅在重叠的 CPU 上部分负载平衡。这只是给出了上面几段中顶级 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’,那么根据上述,这意味着整个系统都有一个覆盖所有 CPU 的单个调度域,而不管任何其他 cpuset 设置如何。
内核向用户空间承诺将尽量避免负载平衡。它会尽可能细化调度域的分区,同时仍然为 cpuset 启用 ‘cpuset.sched_load_balance’ 的任何 CPU 集提供负载平衡。
内部内核 cpuset 到调度程序接口从 cpuset 代码传递到调度程序代码的负载平衡 CPU 的分区。此分区是一组子集(表示为 struct cpumask 的数组),两两不相交,覆盖必须进行负载平衡的所有 CPU。
cpuset 代码构建了一个新的这样的分区,并将其传递给调度程序调度域设置代码,以在必要时重建调度域,具体情况包括:
- 具有非空 CPU 的 cpuset 的 ‘cpuset.sched_load_balance’ 标志更改,
- 或者从启用此标志的 cpuset 中的 CPU 进入或退出,
- 或者具有非空 CPU 且启用了此标志的 cpuset 的 ‘cpuset.sched_relax_domain_level’ 值更改,
- 或者删除了具有非空 CPU 且启用了此标志的 cpuset,
- 或者 CPU 下线/上线。
此分区完全定义了调度程序应该设置的调度域 - 对于分区中的每个元素(struct cpumask),都会设置一个调度域。
调度程序记住当前活动的调度域分区。当来自 cpuset 代码的分区_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 Z 相对于 CPU X 距离较远。即使 CPU Z 空闲,而 CPU X 和兄弟忙碌,调度程序也无法将唤醒的任务 B 从 X 迁移到 Z,因为它超出了搜索范围。结果是,CPU X 上的任务 B 需要等待任务 A 或等待在下一个时钟滴答时进行负载平衡。对于一些特殊情况下的应用程序来说,等待 1 个时钟滴答可能太长。
‘cpuset.sched_relax_domain_level’ 文件允许您根据需要更改此搜索范围。此文件采用 int 值,该值理想情况下指示搜索范围的级别如下,否则初始值为 -1,表示 cpuset 没有请求。
- -1:无请求。使用系统默认值或遵循其他请求。
- 0:不搜索。
- 1:搜索兄弟(核心中的超线程)。
- 2:搜索套装中的核心。
- 3:搜索节点中的 CPU [= 非 NUMA 系统中的系统范围]
- 4:搜索节点的一部分中的 CPU [在 NUMA 系统上]
- 5:系统范围搜索 [在 NUMA 系统上]
系统默认值取决于体系结构。系统默认值可以使用 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 节点允许,那么该任务将被实际上视为已绑定到新 cpuset 的 MPOL_BIND(即使其 NUMA 位置,如 get_mempolicy() 查询的那样,没有更改)。如果一个任务从一个 cpuset 移动到另一个 cpuset,则内核将在下一次为该任务尝试分配内存页时调整任务的内存位置,如上所述。
如果一个 cpuset 的 ‘cpuset.cpus’ 被修改,那么该 cpuset 中的每个任务的允许 CPU 位置将立即发生更改。同样,如果一个任务的 pid 被写入另一个 cpuset 的 ‘tasks’ 文件中,则其允许的 CPU 位置将立即发生更改。如果这样的任务已使用 sched_setaffinity() 调用绑定到其 cpuset 的某个子集,则任务将被允许在其新 cpuset 中的任何允许 CPU 上运行,从而抵消了先前 sched_setaffinity() 调用的效果。
总之,更改 cpuset 的任务的内存位置将在下次为该任务分配页面时由内核更新,但处理器位置将立即更新。
通常,一旦分配了页面(给出了主内存的物理页面),则该页面将保持在分配的节点上,只要它保持分配状态,即使 cpuset 的内存位置策略 ‘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 中未设置)的页面,将不会移动。
上述有一个例外。如果使用热插拔功能移除当前分配给 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 任务文件,将该任务附加到新的 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
#子 shell 'sh' 现在正在 cpuset Charlie 中运行
#下一行应显示 '/Charlie'
cat /proc/self/cpuset
有多种方法可以查询或修改 cpusets:
- 直接通过 cpuset 文件系统,使用各种 cd、mkdir、echo、cat、rmdir 命令从 shell,或者等效的 C 代码。
- 通过 C 库 libcpuset。
- 通过 C 库 libcgroup。
(http://sourceforge.net/projects/libcg/) - 通过 Python 应用程序 cset。
(http://code.google.com/p/cpuset/)
也可以使用 SGI 的 runon 或 Robert Love 的 taskset 在 shell 提示符下执行 sched_setaffinity 调用。可以使用 numactl 命令(Andi Kleen 的 numa 软件包的一部分)在 shell 提示符下执行 mbind 和 set_mempolicy 调用。
2. 用法示例和语法
2.1 基本用法
创建、修改、使用 cpusets 可以通过 cpuset 虚拟文件系统完成。
要挂载它,请键入:
# 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 正在使用(包含 cpuset,或者有进程附加),则会失败。
请注意,出于遗留原因,存在将 “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 -> 将 cpus 列表设置为 cpu 1,2,3,4
# /bin/echo 1,2,3,4 > cpuset.cpus -> 将 cpus 列表设置为 cpu 1,2,3,4
要向 cpuset 添加 CPU,请写入包含要添加的 CPU 的新 CPU 列表。要将 6 添加到上述 cpuset:
# /bin/echo 1-4
,6 > cpuset.cpus -> 将 cpus 列表设置为 cpu 1,2,3,4,6
类似地,要从 cpuset 中删除 CPU,请写入不包含要移除的 CPU 的新 CPU 列表。
要移除所有 CPU:
# /bin/echo "" > cpuset.cpus -> 清除 cpus 列表
2.3 设置标志
语法非常简单:
# /bin/echo 1 > cpuset.cpu_exclusive -> 设置标志 'cpuset.cpu_exclusive'
# /bin/echo 0 > cpuset.cpu_exclusive -> 取消设置标志 '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。