CPUSETS
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 或内存节点。调度程序不会在不允许的 CPU 上调度任务,内核页分配器也不会在不允许的内存节点上分配页面。
用户级代码可以在 cgroup 虚拟文件系统中按名称创建和销毁 cpusets,管理这些 cpusets 的属性和权限,以及为每个 cpuset 指定和查询分配的 CPU 和内存节点,列出分配给 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 和内存节点的集合。
- 系统中的每个任务都附加到一个 cpuset,通过任务结构中指向引用计数 cgroup 结构的指针。
- 调用 sched_setaffinity 仅限于该任务 cpuset 中允许的 CPU。
- 调用 mbind 和 set_mempolicy 仅限于该任务 cpuset 中允许的内存节点。
- 根 cpuset 包含系统的所有 CPU 和内存节点。
- 对于任何 cpuset,可以定义包含父级 CPU 和内存节点子集的子 cpusets。
- cpusets 的层次结构可以挂载在 /dev/cpuset,以便从用户空间进行浏览和操作。
- 可以将 cpuset 标记为独占,以确保没有其他 cpuset(除了直接祖先和后代)可以包含任何重叠的 CPU 或内存节点。
- 您可以列出附加到任何 cpuset 的所有任务(按 PID)。
cpusets 的实现需要在内核的其余部分中添加一些简单的钩子,不会影响性能关键路径:
- 在 init/main.c 中,初始化系统引导时的根 cpuset。
- 在 fork 和 exit 中,将任务附加到其 cpuset 和从其 cpuset 中分离。
- 在 sched_setaffinity 中,通过任务 cpuset 中允许的 CPU 掩码屏蔽请求的 CPU。
- 在 sched.c migrate_live_tasks() 中,如果可能,保持任务在其 cpuset 允许的 CPU 中迁移。
- 在 mbind 和 set_mempolicy 系统调用中,通过任务 cpuset 中允许的内存节点屏蔽请求的内存节点。
- 在 page_alloc.c 中,将内存限制为允许的节点。
- 在 vmscan.c 中,将页面恢复限制为当前 cpuset。
为了启用浏览和修改内核当前已知的 cpusets,应该挂载 "cgroup" 文件系统类型。对于 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 文件系统中由一个目录表示,该目录包含以下描述该 cpuset 的文件(除了标准 cgroup 文件):
- 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 标志:如果设置,将 slab 缓存均匀分布在允许的节点上
- cpuset.sched_load_balance 标志:如果设置,在该 cpuset 上的 CPU 内部进行负载平衡
- cpuset.sched_relax_domain_level:迁移任务时的搜索范围
此外,只有根 cpuset 具有以下文件:
- cpuset.memory_pressure_enabled 标志:计算内存压力?
使用 mkdir 系统调用或 shell 命令创建新的 cpusets。通过写入该 cpuset 目录中的适当文件来修改 cpuset 的属性,例如其标志、允许的 CPU 和内存节点以及附加的任务,如上所列。
嵌套 cpusets 的命名层次结构允许将大型系统划分为嵌套的、动态可变的“软分区”。
每个任务的附加(在 fork 时由该任务的任何子任务自动继承)到 cpuset,允许将系统上的工作负载组织成相关的任务集,以便每个集合都受限于使用特定 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_v2_mode" 选项挂载 cpuset cgroup 文件系统,则这些文件的行为将变得类似于 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 中运行的作业,有效地检测作业造成的内存压力水平。
这对于在运行各种提交作业的严格管理系统非常有用,该系统可能选择终止或重新安排试图在分配给它们的节点上使用更多内存的作业,并且对于紧密耦合、长时间运行的大规模科学计算作业也是如此,如果它们开始使用超过分配给它们的内存,将明显无法满足所需的性能目标。
这种机制为批处理管理器提供了一种非常经济的方式来监视 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 什么是 memory spread?
每个 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 中的下一个节点的值来选择要优先分配的节点。
这种内存放置策略在其他情况下也被称为循环或交错。
这种策略可以为需要将线程本地数据放置在相应节点上的作业提供实质性的改进,但需要访问需要分布在作业 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 的负载平衡不适用于以下两种情况:
- 在大型系统上,跨许多 CPU 进行负载平衡是昂贵的。如果系统使用 cpusets 来将独立作业放置在不同的 CPU 集上,完全的负载平衡是不必要的。
- 支持某些 CPU 上的实时性能的系统需要最小化这些 CPU 上的系统开销,包括避免任务负载平衡,如果不需要的话。
当每个 cpuset 标志 "cpuset.sched_load_balance" 被启用(默认设置)时,它要求该 cpuset 允许的所有 CPU 都包含在一个单独的调度域中,以确保负载平衡可以将任务(不是通过 sched_setaffinity 固定的)从该 cpuset 中的任何 CPU 移动到任何其他 CPU。
当每个 cpuset 标志 "cpuset.sched_load_balance" 被禁用时,调度程序将避免在该 cpuset 中的 CPU 之间进行负载平衡,除非因为一些重叠的 cpuset 启用了 "sched_load_balance"。
因此,在上述两种情况下,顶层 cpuset 标志 "cpuset.sched_load_balance" 应该被禁用,只有一些较小的子 cpuset 启用了该标志。
在这种情况下,通常不希望在顶层 cpuset 中留下任何未固定的任务,因为这些任务可能会被人为地限制在一些 CPU 子集上,具体取决于后代 cpuset 中该标志设置的特定情况。即使这样的任务可以在其他 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" 的值如何,这些 CPU 都不会进行负载平衡。
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.sched_load_balance' 的 cpuset 的任何 CPU 集提供负载平衡。
内部内核 cpuset 到调度程序接口从 cpuset 代码传递到调度程序代码一个负载平衡的 CPU 分区。这个分区是一组子集(表示为 struct cpumask 的数组),两两不相交,覆盖了必须进行负载平衡的所有 CPU。
cpuset 代码构建一个新的这样的分区,并将其传递给调度程序调度域设置代码,以便在必要时重建调度域,每当:
- 具有非空 CPU 的 cpuset 的 'cpuset.sched_load_balance' 标志更改,
- 或者 CPU 加入或离开启用了该标志的 cpuset,
- 或者具有非空 CPU 并且启用了该标志的 'cpuset.sched_relax_domain_level' 值更改,
- 或者删除了具有非空 CPU 并且启用了该标志的 cpuset,
- 或者 CPU 下线/上线。
这个分区确切地定义了调度程序应该设置的调度域 - 对于分区中的每个元素(struct cpumask),调度程序应该设置一个调度域。
调度程序记住当前活动的调度域分区。当调度程序例程 partition_sched_domains() 从 cpuset 代码中被调用以更新这些调度域时,它会比较所请求的新分区与当前分区,并更新其调度域,对于每个更改,删除旧的并添加新的。
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 或等待下一个时钟周期的负载平衡。对于特定情况下的某些应用程序来说,等待 1 个时钟周期可能太长。
'cpuset.sched_relax_domain_level' 文件允许您根据需要请求更改此搜索范围。该文件采用 int 值,理想情况下表示搜索范围的级别大小,否则采用初始值 -1,表示 cpuset 没有请求。
-1: 无请求。使用系统默认值或遵循其他请求。
0: 不进行搜索。
1: 搜索兄弟 CPU(核心内的超线程)。
2: 搜索套装内的核心。
3: 搜索节点内的 CPU(非 NUMA 系统中的系统范围)。
4: 搜索节点块内的节点(在 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?
为了最小化 cpuset 对关键内核代码(如调度程序)的影响,并且由于内核不支持一个任务直接更新另一个任务的内存位置,更改任务的 cpuset CPU 或内存节点位置,或更改任务附加到的 cpuset,对任务的影响是微妙的。
如果一个 cpuset 的内存节点被修改,那么对于附加到该 cpuset 的每个任务,在下一次内核尝试为该任务分配内存页时,内核将注意到任务的 cpuset 的变化,并更新其每个任务的内存位置,以保持在新的 cpuset 的内存位置内。如果任务正在使用 mempolicy MPOL_BIND,并且它绑定的节点与其新 cpuset 中的节点重叠,那么任务将继续使用在新 cpuset 中仍然允许的 MPOL_BIND 节点的子集。如果任务正在使用 MPOL_BIND,并且现在其 MPOL_BIND 节点在新 cpuset 中都不允许,则该任务将被视为实际上绑定到新 cpuset(即使其 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 的所有 CPU,则该 cpuset 中的所有任务将被移动到具有非空 CPU 的最近祖先。但是,如果某些(或全部)任务的移动失败,如果 cpuset 与另一个 cgroup 子系统绑定,并且该子系统对任务附加有一些限制,则在此失败情况下,这些任务将留在原始 cpuset 中,内核将自动更新其 cpus_allowed 以允许所有在线 CPU。当存在用于移除内存节点的内存热插拔功能时,预计也会应用类似的例外。一般来说,内核更倾向于违反 cpuset 的位置,而不是让所有允许的 CPU 或内存节点被下线的任务处于饥饿状态。
上述还有第二个例外。GFP_ATOMIC 请求是必须立即满足的内核内部分配。如果 GFP_ATOMIC 分配失败,内核可能会放弃某些请求,甚至在极少数情况下发生紧急情况。如果无法在当前任务的 cpuset 中满足请求,则我们会放宽 cpuset,并在任何可以找到内存的地方寻找内存。违反 cpuset 要比给内核带来压力更好。
要启动一个要包含在 cpuset 中的新作业,需要执行以下步骤:
- 创建 /sys/fs/cgroup/cpuset 目录
- 使用以下命令挂载 cpuset 文件系统:mount -t cgroup -ocpuset cpuset /sys/fs/cgroup/cpuset
- 通过在 /sys/fs/cgroup/cpuset 虚拟文件系统中进行 mkdir 和 write(或 echo)操作来创建新的 cpuset。
- 启动一个将成为新作业的“创始父任务”。
- 通过将其 pid 写入该 cpuset 的 /sys/fs/cgroup/cpuset 任务文件,将该任务附加到新的 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 echo 2-3 > cpuset.cpus echo 1 > cpuset.mems echo $$ > tasks sh # 子 shell 'sh' 现在在 cpuset Charlie 中运行 # 下一行应显示 '/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 基本用法
创建、修改、使用 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 正在使用中(包含子 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 -> 将 CPU 列表设置为 CPU 1、2、3、4 # /bin/echo 1,2,3,4 > cpuset.cpus -> 将 CPU 列表设置为 CPU 1、2、3、4
要向 cpuset 添加一个 CPU,写入包括要添加的 CPU 的新 CPU 列表。要将 6 添加到上述 cpuset:
# /bin/echo 1-4,6 > cpuset.cpus -> 将 CPU 列表设置为 CPU 1、2、3、4、6
类似地,要从 cpuset 中移除一个 CPU,写入不包括要移除的 CPU 的新 CPU 列表。
要移除所有 CPU:
# /bin/echo "" > cpuset.cpus -> 清空 CPU 列表
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。