控制组

由 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@gentwo.org> 修改

1. 控制组

1.1 什么是 cgroup?

控制组提供了一种机制,用于将任务集及其所有未来子任务聚合/分区到具有特定行为的层次结构组中。

定义

一个 cgroup 将一组任务与一个或多个子系统的参数集关联起来。

一个 子系统 是利用 cgroup 提供的任务分组功能,以特定方式处理任务组的模块。子系统通常是“资源控制器”,负责调度资源或应用每个 cgroup 的限制,但它也可以是任何希望对一组进程进行操作的模块,例如虚拟化子系统。

一个 层级 是一组以树状结构排列的 cgroup,系统中的每个任务都精确地位于该层级中的一个 cgroup 中,并且有一组子系统;每个子系统都将系统特定的状态附加到该层级中的每个 cgroup。每个层级都关联着一个 cgroup 虚拟文件系统实例。

在任何给定时间,可能存在多个活跃的任务 cgroup 层级。每个层级都是系统中所有任务的一个分区。

用户级代码可以在 cgroup 虚拟文件系统实例中按名称创建和销毁 cgroup,指定和查询任务分配给哪个 cgroup,以及列出分配给 cgroup 的任务 PID。这些创建和分配只影响与该 cgroup 文件系统实例关联的层级。

cgroup 本身唯一的使用场景是简单的作业跟踪。其目的是让其他子系统挂接到通用 cgroup 支持中,为 cgroup 提供新的属性,例如核算/限制 cgroup 中进程可以访问的资源。例如,cpuset(参见 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 将根据启动者的身份(教授/学生)共享相应的 CPU/内存类别。

借助为不同资源对任务进行不同分类的能力(通过将这些资源子系统置于不同的层级中),管理员可以轻松设置一个脚本,该脚本接收执行通知,并根据启动浏览器的用户进行操作,他可以

# echo browser_pid > /sys/fs/cgroup/<restype>/<userclass>/tasks

如果只有一个层级,他现在可能不得不为每个启动的浏览器创建一个单独的 cgroup,并将其与适当的网络和其他资源类别关联起来。这可能导致此类 cgroup 的泛滥。

此外,假设管理员希望暂时为学生的浏览器提供增强的网络访问(因为现在是晚上,用户想玩在线游戏:D)或者给学生的某个模拟应用增强 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 目录中添加额外的文件。

新的 cgroup 使用 mkdir 系统调用或 shell 命令创建。cgroup 的属性(例如其标志)通过写入该 cgroup 目录中的相应文件进行修改,如上所述。

嵌套 cgroup 的命名分层结构允许将大型系统划分为嵌套的、动态可变的“软分区”。

每个任务对 cgroup 的附件(在 fork 时自动由其子进程继承)允许将系统上的工作负载组织成相关的任务集。如果必要 cgroup 文件系统目录上的权限允许,任务可以重新附加到任何其他 cgroup。

当一个任务从一个 cgroup 移动到另一个 cgroup 时,它会获得一个新的 css_set 指针——如果已经存在一个包含所需 cgroup 集合的 css_set,则该组将被重用,否则将分配一个新的 css_set。通过查找哈希表来定位适当的现有 css_set。

为了允许从 cgroup 访问构成它的 css_set(以及任务),一组 cg_cgroup_link 对象形成一个格;每个 cg_cgroup_link 都链接到单个 cgroup 的 cg_cgroup_link 列表的 cgrp_link_list 字段中,以及单个 css_set 的 cg_cgroup_link 列表的 cg_link_list 字段中。

因此,cgroup 中的任务集可以通过遍历引用该 cgroup 的每个 css_set,并对每个 css_set 的任务集进行子遍历来列出。

使用 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?

要启动一个包含在 cgroup 中的新作业,并使用“cpuset”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

您可以通过 echoing 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 中添加一个条目

  • 定义一个名为 _cgrp_subsys 的 cgroup_subsys 对象

每个子系统可以导出以下方法。唯一强制的方法是 css_alloc/free。任何其他为空的方法都被假定为成功的无操作。

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() 的对应函数,并且仅在 css_online() 在 @cgrp 上成功后才调用。这标志着 @cgrp 结束的开始。@cgrp 正在被移除,子系统应该开始释放其对 @cgrp 的所有引用。当所有引用都释放后,cgroup 移除将进入下一步 - css_free()。在此回调之后,@cgrp 对子系统而言应被视为已死亡。

void css_free(struct cgroup *cgrp) (调用者持有 cgroup_mutex)

cgroup 系统即将释放 @cgrp;子系统应该释放其子系统状态对象。当此方法被调用时,@cgrp 已完全未使用;@cgrp->parent 仍然有效。(注意——如果在此子系统的 create() 方法为新 cgroup 调用后发生错误,也可以为新创建的 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.