控制组¶
由 Paul Menage <menage@google.com> 基于 CPUSETS 编写
CPUSETS 的原始版权声明
部分版权 (C) 2004 BULL SA.
部分版权 (c) 2004-2006 Silicon Graphics, Inc.
由 Paul Jackson <pj@sgi.com> 修改
由 Christoph Lameter <cl@linux.com> 修改
1. 控制组¶
1.1 什么是 cgroup?¶
控制组提供了一种机制,用于将一组任务及其所有未来的子任务聚合/分区到具有专门行为的层次结构组中。
定义
一个 cgroup 将一组任务与一个或多个子系统的参数集关联起来。
一个 子系统 是一个模块,它利用 cgroup 提供的任务分组功能以特定方式处理任务组。子系统通常是调度资源或应用每个 cgroup 限制的“资源控制器”,但它可以是任何想要对进程组执行操作的东西,例如虚拟化子系统。
一个 层次结构 是一组排列成树状结构的 cgroup,使得系统中的每个任务都位于层次结构中的一个 cgroup 中,以及一组子系统;每个子系统都有附加到层次结构中每个 cgroup 的系统特定状态。每个层次结构都有与其关联的 cgroup 虚拟文件系统的实例。
任何时候都可能存在多个活动的任务 cgroup 层次结构。每个层次结构都是系统中所有任务的分区。
用户级代码可以通过名称在 cgroup 虚拟文件系统的实例中创建和销毁 cgroup,指定和查询任务分配到的 cgroup,并列出分配给 cgroup 的任务 PID。这些创建和分配仅影响与该 cgroup 文件系统实例关联的层次结构。
就其本身而言,cgroup 的唯一用途是用于简单的作业跟踪。目的是让其他子系统挂钩到通用 cgroup 支持,为 cgroup 提供新的属性,例如计算/限制 cgroup 中的进程可以访问的资源。例如,cpusets (请参阅 CPUSETS) 允许您将一组 CPU 和一组内存节点与每个 cgroup 中的任务关联起来。
1.2 为什么需要 cgroup?¶
在 Linux 内核中,有多个工作致力于提供进程聚合,主要用于资源跟踪目的。此类工作包括 cpusets、CKRM/ResGroups、UserBeanCounters 和虚拟服务器命名空间。这些都需要对进程进行分组/分区的基本概念,新 fork 的进程最终会与其父进程位于同一组 (cgroup) 中。
内核 cgroup 补丁提供了有效实现此类组所需的最小基本内核机制。它对系统快速路径的影响最小,并为 cpusets 等特定子系统提供了钩子,以便根据需要提供额外的行为。
提供多个层次结构支持是为了允许在不同子系统对任务进行 cgroup 划分明显不同的情况下 - 拥有并行的层次结构允许每个层次结构成为任务的自然划分,而无需处理如果几个不相关的子系统需要被强制进入同一 cgroup 树中,则会存在的复杂任务组合。
在一个极端情况下,每个资源控制器或子系统都可以在一个单独的层次结构中;在另一个极端情况下,所有子系统都将附加到同一层次结构中。
作为一个可以从多个层次结构中获益的场景示例(最初由 vatsa@in.ibm.com 提出),考虑一个拥有各种用户(学生、教授、系统任务等)的大型大学服务器。该服务器的资源规划可能遵循以下几点
CPU : "Top cpuset"
/ \
CPUSet1 CPUSet2
| |
(Professors) (Students)
In addition (system tasks) are attached to topcpuset (so
that they can run anywhere) with a limit of 20%
Memory : Professors (50%), Students (30%), system (20%)
Disk : Professors (50%), Students (30%), system (20%)
Network : WWW browsing (20%), Network File System (60%), others (20%)
/ \
Professors (15%) students (5%)
像 Firefox/Lynx 这样的浏览器进入 WWW 网络类,而 (k)nfsd 进入 NFS 网络类。
同时,Firefox/Lynx 将根据启动它的人 (prof/student) 共享适当的 CPU/内存类。
通过为不同的资源 (通过将这些资源子系统放在不同的层次结构中) 对任务进行不同的分类的能力,管理员可以轻松设置一个脚本,该脚本接收 exec 通知,并根据启动浏览器的人员,他可以
# echo browser_pid > /sys/fs/cgroup/<restype>/<userclass>/tasks
如果只有一个层次结构,他现在可能需要为每个启动的浏览器创建一个单独的 cgroup,并将其与适当的网络和其他资源类相关联。这可能会导致此类 cgroup 的激增。
此外,假设管理员希望暂时为学生的浏览器提供增强的网络访问权限 (因为现在是晚上,用户想要进行在线游戏 :)),或者为学生的一个模拟应用程序提供增强的 CPU 能力。
通过直接将 PID 写入资源类的能力,这只是一个
# echo pid > /sys/fs/cgroup/network/<new_class>/tasks
(after some time)
# echo pid > /sys/fs/cgroup/network/<orig_class>/tasks
如果没有这种能力,管理员将不得不将 cgroup 分成多个单独的 cgroup,然后将新的 cgroup 与新的资源类相关联。
1.3 cgroup 如何实现?¶
控制组按如下方式扩展内核
系统中的每个任务都有一个对 css_set 的引用计数指针。
一个 css_set 包含一组对 cgroup_subsys_state 对象的引用计数指针,每个对象对应系统中注册的每个 cgroup 子系统。从任务到其在每个层次结构中所属的 cgroup 没有直接链接,但是可以通过跟踪 cgroup_subsys_state 对象中的指针来确定。这是因为访问子系统状态是期望在性能关键代码中频繁发生的事情,而需要任务的实际 cgroup 分配 (特别是,在 cgroup 之间移动) 的操作则较少见。一个链表使用 css_set 在每个 task_struct 的 cg_list 字段中运行,锚定在 css_set->tasks。
可以安装 cgroup 层次结构文件系统,以便从用户空间浏览和操作。
您可以列出附加到任何 cgroup 的所有任务(按 PID)。
cgroup 的实现需要一些简单的钩子到内核的其余部分,在性能关键路径中没有钩子
在 init/main.c 中,在系统启动时初始化根 cgroup 和初始 css_set。
在 fork 和 exit 中,将任务附加和分离其 css_set。
此外,还可以挂载一个新的 “cgroup” 类型的文件系统,以便浏览和修改内核当前已知的 cgroup。挂载 cgroup 层级结构时,您可以指定一个以逗号分隔的子系统列表作为文件系统的挂载选项。默认情况下,挂载 cgroup 文件系统会尝试挂载包含所有已注册子系统的层级结构。
如果已经存在具有完全相同子系统集的活动层级结构,则将重用该层级结构进行新的挂载。如果没有现有的层级结构匹配,并且任何请求的子系统正在现有的层级结构中使用,则挂载将失败并返回 -EBUSY。否则,将激活一个新的层级结构,并将其与请求的子系统关联。
目前无法将新的子系统绑定到活动的 cgroup 层级结构,也无法从活动的 cgroup 层级结构中解除绑定子系统。未来可能会实现这一点,但会面临棘手的错误恢复问题。
卸载 cgroup 文件系统时,如果顶级 cgroup 下创建了任何子 cgroup,即使卸载后,该层级结构仍将保持活动状态;如果没有子 cgroup,则该层级结构将被停用。
没有为 cgroup 添加新的系统调用 - 对 cgroup 的所有查询和修改支持都通过此 cgroup 文件系统实现。
/proc 下的每个任务都有一个名为 ‘cgroup’ 的附加文件,该文件为每个活动的层级结构显示子系统名称和 cgroup 名称,后者为相对于 cgroup 文件系统根目录的路径。
每个 cgroup 在 cgroup 文件系统中都由一个目录表示,其中包含以下描述该 cgroup 的文件
tasks: 附加到该 cgroup 的任务列表(按 PID)。不能保证此列表已排序。将线程 ID 写入此文件会将该线程移动到此 cgroup。
cgroup.procs: cgroup 中的线程组 ID 列表。不能保证此列表已排序或没有重复的 TGID,如果需要此属性,用户空间应排序/去重该列表。将线程组 ID 写入此文件会将该组中的所有线程移动到此 cgroup。
notify_on_release 标志:退出时是否运行释放代理?
release_agent: 用于释放通知的路径(此文件仅存在于顶层 cgroup 中)
其他子系统(如 cpusets)可能会在每个 cgroup 目录中添加其他文件。
使用 mkdir 系统调用或 shell 命令创建新的 cgroup。通过写入该 cgroup 目录中的相应文件来修改 cgroup 的属性(例如其标志),如上所述。
嵌套 cgroup 的命名层级结构允许将大型系统划分为嵌套的、动态可更改的“软分区”。
每个任务(通过 fork 由该任务的任何子任务自动继承)附加到 cgroup 允许将系统上的工作负载组织成相关的任务集。如果必要 cgroup 文件系统目录的权限允许,可以将任务重新附加到任何其他 cgroup。
当任务从一个 cgroup 移动到另一个 cgroup 时,它会获得一个新的 css_set 指针 - 如果存在具有所需 cgroup 集合的现有 css_set,则会重用该组,否则会分配一个新的 css_set。通过查看哈希表来定位适当的现有 css_set。
为了允许从 cgroup 访问构成它的 css_set(以及任务),一组 cg_cgroup_link 对象形成一个格;每个 cg_cgroup_link 都链接到一个 cg_cgroup_links 列表,该列表是单个 cgroup 的 cgrp_link_list 字段,以及一个 cg_cgroup_links 列表,该列表是单个 css_set 的 cg_link_list。
因此,可以通过迭代引用该 cgroup 的每个 css_set,并在每个 css_set 的任务集上进行子迭代来列出 cgroup 中的任务集。
使用 Linux 虚拟文件系统 (vfs) 来表示 cgroup 层级结构为 cgroup 提供了熟悉的权限和命名空间,并以最少的额外内核代码实现。
1.4 notify_on_release 的作用是什么?¶
如果在 cgroup 中启用 notify_on_release 标志 (1),则当 cgroup 中的最后一个任务离开(退出或附加到其他 cgroup)并且该 cgroup 的最后一个子 cgroup 被删除时,内核将运行该层级结构根目录的“release_agent”文件内容指定的命令,并提供已放弃 cgroup 的路径名(相对于 cgroup 文件系统的挂载点)。这可以自动删除已放弃的 cgroup。系统启动时根 cgroup 中 notify_on_release 的默认值是禁用 (0)。创建时其他 cgroup 的默认值是其父级 notify_on_release 设置的当前值。cgroup 层级结构的 release_agent 路径的默认值为空。
1.5 clone_children 的作用是什么?¶
此标志仅影响 cpuset 控制器。如果在 cgroup 中启用 clone_children 标志 (1),则新的 cpuset cgroup 将在初始化期间从父级复制其配置。
1.6 如何使用 cgroup?¶
要使用“cpuset” cgroup 子系统启动要包含在 cgroup 中的新作业,步骤如下:
1) mount -t tmpfs cgroup_root /sys/fs/cgroup
2) mkdir /sys/fs/cgroup/cpuset
3) mount -t cgroup -ocpuset cpuset /sys/fs/cgroup/cpuset
4) Create the new cgroup by doing mkdir's and write's (or echo's) in
the /sys/fs/cgroup/cpuset virtual file system.
5) Start a task that will be the "founding father" of the new job.
6) Attach that task to the new cgroup by writing its PID to the
/sys/fs/cgroup/cpuset tasks file for that cgroup.
7) fork, exec or clone the job tasks from this founding father task.
例如,以下命令序列将设置一个名为 “Charlie” 的 cgroup,其中仅包含 CPU 2 和 3 以及内存节点 1,然后在该 cgroup 中启动子 shell ‘sh’
mount -t tmpfs cgroup_root /sys/fs/cgroup
mkdir /sys/fs/cgroup/cpuset
mount -t cgroup cpuset -ocpuset /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 cgroup Charlie
# The next line should display '/Charlie'
cat /proc/self/cgroup
2. 用法示例和语法¶
2.1 基本用法¶
可以通过 cgroup 虚拟文件系统创建、修改和使用 cgroup。
要挂载具有所有可用子系统的 cgroup 层级结构,请键入
# mount -t cgroup xxx /sys/fs/cgroup
“xxx” 不会被 cgroup 代码解释,但会出现在 /proc/mounts 中,因此可以是您喜欢的任何有用的标识字符串。
注意:某些子系统在没有用户输入的情况下无法工作。例如,如果启用了 cpusets,用户必须先为创建的每个新 cgroup 填充 cpus 和 mems 文件,然后才能使用该组。
如 1.2 为什么需要 cgroup? 一节中所述,您应该为要控制的每个单一资源或资源组创建不同的 cgroup 层级结构。因此,您应该在 /sys/fs/cgroup 上挂载 tmpfs,并为每个 cgroup 资源或资源组创建目录
# mount -t tmpfs cgroup_root /sys/fs/cgroup
# mkdir /sys/fs/cgroup/rg1
要挂载仅具有 cpuset 和内存子系统的 cgroup 层级结构,请键入
# mount -t cgroup -o cpuset,memory hier1 /sys/fs/cgroup/rg1
虽然当前支持重新挂载 cgroup,但不建议使用。重新挂载允许更改绑定的子系统和 release_agent。重新绑定几乎没有用处,因为它只在层级结构为空时才有效,并且 release_agent 本身应该替换为传统的 fsnotify。对重新挂载的支持将在未来移除。
要指定层级结构的 release_agent
# mount -t cgroup -o cpuset,release_agent="/sbin/cpuset_release_agent" \
xxx /sys/fs/cgroup/rg1
请注意,多次指定 ‘release_agent’ 将返回失败。
请注意,目前仅当层级结构由单个(根)cgroup 组成时才支持更改子系统集。未来将实现支持从现有 cgroup 层级结构任意绑定/解除绑定子系统的功能。
然后,在 /sys/fs/cgroup/rg1 下,您可以找到一个与系统中 cgroup 树对应的树。例如,/sys/fs/cgroup/rg1 是保存整个系统的 cgroup。
如果要更改 release_agent 的值
# echo "/sbin/new_release_agent" > /sys/fs/cgroup/rg1/release_agent
也可以通过重新挂载来更改。
如果要在 /sys/fs/cgroup/rg1 下创建新的 cgroup
# cd /sys/fs/cgroup/rg1
# mkdir my_cgroup
现在您想对此 cgroup 做一些事情
# cd my_cgroup
在此目录中,您可以找到几个文件
# ls
cgroup.procs notify_on_release tasks
(plus whatever files added by the attached subsystems)
现在将您的 shell 附加到此 cgroup
# /bin/echo $$ > tasks
您还可以通过在此目录中使用 mkdir 在您的 cgroup 中创建 cgroup
# mkdir my_sub_cs
要删除 cgroup,只需使用 rmdir
# rmdir my_sub_cs
如果 cgroup 正在使用(内部有 cgroup、附加了进程,或被其他特定于子系统的引用保持活动),则此操作将失败。
2.2 附加进程¶
# /bin/echo PID > tasks
请注意,它是 PID,而不是 PIDs。您一次只能附加一个任务。如果您有多个任务要附加,则必须一个接一个地执行
# /bin/echo PID1 > tasks
# /bin/echo PID2 > tasks
...
# /bin/echo PIDn > tasks
您可以通过回显 0 来附加当前的 shell 任务
# echo 0 > tasks
您可以使用 cgroup.procs 文件而不是 tasks 文件来一次移动线程组中的所有线程。将线程组中任何任务的 PID 回显到 cgroup.procs 会导致该线程组中的所有任务附加到该 cgroup。将 0 写入 cgroup.procs 会移动写入任务的线程组中的所有任务。
注意:由于每个任务始终是每个已挂载层级结构中恰好一个 cgroup 的成员,因此要从其当前 cgroup 中删除任务,您必须通过写入新 cgroup 的 tasks 文件将其移动到新的 cgroup(可能是根 cgroup)。
注意:由于某些 cgroup 子系统强制执行的一些限制,将进程移动到另一个 cgroup 可能会失败。
2.3 按名称挂载层级结构¶
在挂载 cgroup 层级结构时传递 name=<x> 选项会将给定的名称与该层级结构关联。这可以在挂载预先存在的层级结构时使用,以便按名称而不是其活动子系统集来引用它。每个层级结构要么是无名的,要么具有唯一的名称。
名称应匹配 [w.-]+
在为新层级结构传递 name=<x> 选项时,您需要手动指定子系统;当您为子系统命名时,不支持在未明确指定任何子系统的情况下挂载所有子系统的旧行为。
子系统的名称显示为 /proc/mounts 和 /proc/<pid>/cgroups 中层级结构描述的一部分。
3. 内核 API¶
3.1 概述¶
想要连接到通用 cgroup 系统的每个内核子系统都需要创建一个 cgroup_subsys 对象。该对象包含各种方法,这些方法是来自 cgroup 系统的回调,以及将由 cgroup 系统分配的子系统 ID。
cgroup_subsys 对象中的其他字段包括
subsys_id:子系统的唯一数组索引,指示此子系统应管理 cgroup->subsys[] 中的哪个条目。
name:应初始化为唯一的子系统名称。不应超过 MAX_CGROUP_TYPE_NAMELEN。
early_init:指示子系统是否需要在系统启动时进行早期初始化。
系统创建的每个 cgroup 对象都有一个由子系统 ID 索引的指针数组;此指针完全由子系统管理;通用 cgroup 代码永远不会接触此指针。
3.2 同步¶
cgroup 系统使用一个全局互斥锁 cgroup_mutex。任何想要修改 cgroup 的操作都应获取此互斥锁。也可以获取它来防止 cgroup 被修改,但在这种情况下,更具体的锁可能更合适。
有关更多详细信息,请参阅 kernel/cgroup.c。
子系统可以通过函数 cgroup_lock()/cgroup_unlock() 获取/释放 cgroup_mutex。
访问任务的 cgroup 指针可以通过以下方式完成:- 在持有 cgroup_mutex 时 - 在持有任务的 alloc_lock 时(通过 task_lock()) - 在通过 rcu_read_lock()
部分,使用 rcu_dereference()
3.3 子系统 API¶
每个子系统都应
在 linux/cgroup_subsys.h 中添加一个条目
定义一个名为 <name>_cgrp_subsys 的 cgroup_subsys 对象
每个子系统都可以导出以下方法。唯一强制性的方法是 css_alloc/free。任何其他为 null 的方法都假定为成功的空操作。
struct cgroup_subsys_state *css_alloc(struct cgroup *cgrp)
(调用者持有 cgroup_mutex)
此函数用于为一个 cgroup 分配子系统状态对象。子系统应为传入的 cgroup 分配其子系统状态对象,成功时返回指向新对象的指针,失败时返回 ERR_PTR()
值。成功时,子系统指针应指向一个类型为 cgroup_subsys_state 的结构(通常嵌入在更大的特定于子系统的对象中),该结构将由 cgroup 系统初始化。请注意,这将在初始化时被调用,以便为此子系统创建根子系统状态;这种情况可以通过传入的 cgroup 对象具有 NULL 父级(因为它是层次结构的根)来识别,并且可能是初始化代码的合适位置。
int css_online(struct cgroup *cgrp)
(调用者持有 cgroup_mutex)
在 @cgrp 成功完成所有分配并对 cgroup_for_each_child/descendant_*() 迭代器可见后调用。子系统可以选择通过返回 -errno 来使创建失败。此回调可用于实现可靠的状态共享和沿层次结构传播。有关详细信息,请参阅 cgroup_for_each_live_descendant_pre() 上的注释。
void css_offline(struct cgroup *cgrp);
(调用者持有 cgroup_mutex)
这是 css_online() 的对应项,仅在 @cgrp 上 css_online() 成功时调用。这表示 @cgrp 即将结束。@cgrp 正在被删除,子系统应开始删除其持有的所有对 @cgrp 的引用。当所有引用都被删除后,cgroup 删除将继续执行下一步 - css_free()。在此回调之后,@cgrp 应被子系统视为已死亡。
void css_free(struct cgroup *cgrp)
(调用者持有 cgroup_mutex)
cgroup 系统即将释放 @cgrp;子系统应释放其子系统状态对象。当此方法被调用时,@cgrp 完全未使用;@cgrp->parent 仍然有效。(注意 - 如果在为此新 cgroup 调用此子系统的 create() 方法后发生错误,也可以为新创建的 cgroup 调用此方法)。
int can_attach(struct cgroup *cgrp, struct cgroup_taskset *tset)
(调用者持有 cgroup_mutex)
在将一个或多个任务移动到 cgroup 之前调用;如果子系统返回错误,则会中止附加操作。@tset 包含要附加的任务,并保证其中至少有一个任务。
- 如果任务集中有多个任务,则
保证所有任务都来自同一个线程组
@tset 包含线程组中的所有任务,无论它们是否正在切换 cgroup
第一个任务是 leader
每个 @tset 条目还包含任务的旧 cgroup,并且可以使用 cgroup_taskset_for_each() 迭代器轻松跳过不切换 cgroup 的任务。请注意,这不会在 fork 上调用。如果此方法返回 0(成功),则在调用者持有 cgroup_mutex 时,此方法应保持有效,并确保将来调用 attach() 或 cancel_attach()。
void css_reset(struct cgroup_subsys_state *css)
(调用者持有 cgroup_mutex)
一个可选操作,应将 @css 的配置恢复到初始状态。目前仅在统一层次结构上使用,当通过“cgroup.subtree_control”在 cgroup 上禁用子系统,但由于其他子系统依赖它而保持启用时使用。cgroup 核心通过删除关联的接口文件来使这样的 css 不可见,并调用此回调,以便隐藏的子系统可以返回到初始的中性状态。这可以防止隐藏的 css 意外地控制资源,并确保配置在稍后再次可见时处于初始状态。
void cancel_attach(struct cgroup *cgrp, struct cgroup_taskset *tset)
(调用者持有 cgroup_mutex)
在 can_attach() 成功后,任务附加操作失败时调用。其 can_attach() 具有一些副作用的子系统应提供此函数,以便子系统可以实现回滚。如果不需要,则无需提供。这只会对 can_attach() 操作成功的子系统调用。参数与 can_attach() 相同。
void attach(struct cgroup *cgrp, struct cgroup_taskset *tset)
(调用者持有 cgroup_mutex)
在任务附加到 cgroup 后调用,以允许执行任何需要内存分配或阻塞的附加后活动。参数与 can_attach() 相同。
void fork(struct task_struct *task)
当一个任务被 fork 到一个 cgroup 中时调用。
void exit(struct task_struct *task)
在任务退出期间调用。
void free(struct task_struct *task)
当 task_struct 被释放时调用。
void bind(struct cgroup *root)
(调用者持有 cgroup_mutex)
当 cgroup 子系统被重新绑定到不同的层次结构和根 cgroup 时调用。目前,这只会涉及在默认层次结构(永远没有子 cgroup)和正在创建/销毁的层次结构(因此没有子 cgroup)之间移动。
4. 扩展属性用法¶
cgroup 文件系统在其目录和文件中支持某些类型的扩展属性。当前支持的类型有:
可信(XATTR_TRUSTED)
安全(XATTR_SECURITY)
两者都需要 CAP_SYS_ADMIN 能力才能设置。
与 tmpfs 中一样,cgroup 文件系统中的扩展属性使用内核内存存储,建议尽量减少使用。这就是不支持用户定义的扩展属性的原因,因为任何用户都可以执行此操作,并且值大小没有限制。
此功能的当前已知用户是 SELinux(用于限制容器中的 cgroup 使用)和 systemd(用于各种元数据,例如 cgroup 中的主 PID(systemd 为每个服务创建一个 cgroup))。
5. 问题¶
Q: what's up with this '/bin/echo' ?
A: bash's builtin 'echo' command does not check calls to write() against
errors. If you use it in the cgroup file system, you won't be
able to tell whether a command succeeded or failed.
Q: When I attach processes, only the first of the line gets really attached !
A: We can only return one error code per call to write(). So you should also
put only ONE PID.