Linux Cgroup v1(中文翻译)(1):Control Group
阅读原文时间:2023年07月10日阅读:2

英文原文:https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v1/cgroups.html

1 控制组

控制组(Control Group)提供一种机制,把一组任务(task)及其子任务整合/分割成具有特殊行为的分层化的分组(groups)。

定义:

控制组(cgroup),把一组任务跟一个或者多个子系统(subsystem)的一组参数进行关联。

分组(subsystem),是一种模块,利用cgroup提供的任务分组功能,以特定的方式来实现任务组。分组(subsystem)一般是资源控制器(resource controller),调度资源或者设置资源限制,但是他也可以是管理一组进程的任何方法,例如虚拟子系统。

分层(hierachy),是一组树形结构的控制组,系统中的每个任务都会处在分层的控制组和子系统中,每个子系统有控制组相关的系统状态。每个分层都有一个控制组虚拟文件系统实例。

在任何时候,可以有多个激活的任务组分层。每个分层是系统中所有任务的一种隔离。

用户级别的代码可以根据cgroup虚拟文件系统中的名称创建和销毁控制组,指定和查询任务被分配给哪个控制组,枚举分配给控制组的任务PID。这些创建和分配只影响该控制组文件系统相关的分层。

控制组的唯一作用就是简化任务跟踪。她的目标是其他子系统能挂载到通用控制组上,而这些通用控制组提供了新的控制组属性,例如对控制组进程能访问资源的统计和限制。举个例子,cpusets允许把cpu和内存节点和每个控制组的任务进行关联。

在linux内核中,有多种用来做资源跟踪的进程聚合方式,像cpusets,CKRM/ResGroups,UserBeanCounters和虚拟服务命名空间(namespace)。这些方式都需要了解进程分组/分割的基本概念,而这些在同一个分组(cgroup)中新产生的子进程作为他们的父进程。

内核的控制组补丁提供了最小的基本内核机制,根据需求有效的实现这种控制组。它对系统快速路径(system fast paths)只有很小的影响,并提供了针对特定子系统的钩子,像cpusets。

多级分层支持,允许分割任务到控制组,它明显不同于一些有平行分层的子系统,允许每个分层作为自然的任务分隔,不必处理复杂的任务组合,而这些任务组合是出现在几个不相关的子系统需要强制分配到同一个控制组树的情况下。

在极端情况下,每个资源控制器或者子系统可能在单独的分层中;另一种极端情况,所有的子系统可能隶属于同一个分层。

有一个应用场景示例可能对多分层的理解有好处。假设一个有很多用户(学生,教授,系统任务等)的大学服务器,这个服务器的资源规划应该是下面这样子的:

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/memory类。

为了能对不同的资源划分任务,管理员很容易就能运行脚本来接收执行通知,然后根据是谁来运行的浏览器,他可以执行下面的命令:

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

在只有单个分层情况下,他现在可能必须要为每个启动的浏览器创建一个单独的控制组,然后关联合适的网络和其他的资源类。这可能导致这种控制组的激增。

管理员可能临时为一个学生的浏览器增加网络访问,或者给一个学生的模拟器应用增加CPU算力。

下面的方式可以直接写PIDs到资源类中:

# echo pid > /sys/fs/cgroup/network/<new_class>/tasks
(after some time)
# echo pid > /sys/fs/cgroup/network/<orig_class>/tasks

没有这种机制的话,管理员将不得不切分控制组成多个单独的控制组,然后关联新的控制组和新的资源类。

控制组在内核中的扩展方式如下:

  • 系统中每个任务task一个引用计数指针指向css_set。
  • css_set包含一个指向cgroup_subsys_state对象的引用计数指针集合,每个控制组子系统在系统中注册一个该对象。(省略部分)
  • 一个控制组分层文件系统能被从用户空间挂载出来进行浏览和操作。
  • 可以根据PID枚举隶属于任何控制组的任务。

控制组的实现需要几个简单的钩子钩到内核其余部分,但是不在关键性能的路径上:

  • 在init/main.c中来初始化根控制组(root cgroups),在系统启动时初始化css_set.
  • 在fork和exit时,从css_set中attach和detach任务。

除此之外,一个新的文件系统类型cgroup可以被挂载出来,以便能够浏览和修改控制组。当挂载一个控制组分层(cgroup hierachy)的时候,你可以指定一个逗号分隔符的子系统列表来作为挂载选项。默认情况下,挂载控制组文件系统会试着挂载一个包含所有已注册子系统的分层。

如果存在一个激活的分层有相同的子系统集合,它将被重用做新的挂载。如果没有现存的分层匹配,以及现存分层中的任何子系统正在被使用,那么挂载将会失败(失败号-EBUSY),否则,一个新的分层就被激活,跟请求的子系统关联起来。

绑定新的子系统到激活的控制组分层中,或者从激活的控制组分层中解绑子系统,在当前是不可能,但是未来是可能的,但是它会充满着严重的错误恢复(errot-recovery)问题。

当控制组文件系统被卸载时,如果有任何子控制组被创建在顶级控制组下,即使已经卸载完毕,分层仍会保持激活;如果没有子控制组,那么分层将会被停用。

没有为控制组增加新的系统调用,对控制组的所有的查询和修改的操作支持都是通过控制组文件系统。

在/proc下的每个任务都有一个新增的cgroup文件,对每个激活的分层来说,子系统命名和控制组名称路径都是相对控制组文件系统根路径的。

每个控制组是由控制组文件系统中的目标表示的,它包含如下的文件来描述控制组:

  • tasks:隶属于控制组的任务列表(以PID来表示),这个列表不是按序排列的。写入线程ID到这个文件就表示移动线程到这个控制组。
  • cgroup.procs:线程组ID,这个列表不保证按序排列或者没有重复的TGIDs,如果需要的话,用户空间应当排序或者uniquify这个列表。写线程组ID到这个文件就会移动这个组中的所有线程到本控制组。
  • notify_on_release: 在exit退出时运行release agent。
  • release_agent: 用来释放通知的路径。(这个文件仅仅存在顶层控制组中)

其他的子系统,像cpusets可能会在每个控制组路目录下添加额外的文件。

新的控制组可以通过mkdir系统调用或者shell命令来创建。控制组属性,例如标签,可以通过写入该控制组目录下的文件来修改。

嵌套控制组的命名分层结构允许分割大系统成嵌套的、动态可变的软分区(soft-partitions)。

每个任务绑定到控制组时,在fork的时候会被该任务的子任务自动继承,允许在系统中组织工作负载到相关的任务集合中。如果必要的控制组文件系统目录允许,一个任务可以被重新绑定到任何控制组,

当任务从一个控制组移动到另一个控制组,他就会获得一个新的css_set指针。如果有现存的css_set,带有预期的可重用的控制组集合,那么就可以重用,否则,就分配一个新的css_set。现有的css_set通过查询哈希表来定位。

要允许从控制组来访问css_sets,一个g_cgroup_link对象集合形成一个格栅(lattice);每个g_cgroup_link被链接到一个g_cgroup_links列表(省略……)

控制组中的任务集合可以通过引用该控制组的css_set来列举。

使用linux虚拟文件系统vfs来表示控制组分层,最小化改动内核代码,为控制组提供了常见的权限和命名空间。

如果控制组中的notify_on_release标记被使能,那么只要控制组中的最后一个任务离开(退出或者绑定到其他的控制组)并且最后的子控制组被移除,内核就会运行分层根目录下的release_agent文件内容中定义的命令,提供废弃的控制组的路径名(相对控制组文件系统的挂载点)。这样能自动移除废弃的控制组。

在系统启动的时候,根控制组中的notify_on_release的默认值是diabaled(0).其他的控制组在创建时候的默认值是他们的父控制组的notify_on_release的当前值。

控制组分层的release_agent路径的默认值是空。

这个标签仅仅影响cpuset控制器。如果clone_children标记在控制组中被使能enbale(1),一个新的cpuset控制组在初始化的时候就能复制父控制组的配置。

要启动一个将要包含在某个控制组中的新工作任务,使用cpuset控制组子系统,操作步骤如下:

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”的控制组,仅仅包含CPU2和3,内存节点1,在控制组中启动一个子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 应用示例和语法

创建、修改和使用控制组可以通过控制组虚拟文件系统来完成。

要挂载一个所有子系统都可用的控制组分层,输入命令:

# mount -t cgroup xxx /sys/fs/cgroup

内核代码解读不了“xxx”,但是它会出现在/proc/mounts中,因此它就可以是你想用的有用的身份字符串。

注意:没有用户提前输入的话,一些子系统不能工作。例如,如果cpusets被使能,用户必须为每个已经创建但是还没使用的控制组写入数据到cpu和mem文件中。

正如1.2章节所述,我们为什么需要控制组?你应该为每个你想要控制的资源或者资源组创建不同的控制组分层。因此,你可以挂载在/sys/fs/cgroup中tmpfs,然后为每个控制组资源或者资源组创建目录:

# mount -t tmpfs cgroup_root /sys/fs/cgroup
# mkdir /sys/fs/cgroup/rg1

要挂载只有cpuset和memory子系统的控制组分层,输入如下命令:

# mount -t cgroup -o cpuset,memory hier1 /sys/fs/cgroup/rg1

重新挂载控制组当前是支持的,但是不推荐使用。重新挂载允许改变子系统和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,将会返回失败。

注意,子系统集合的变更当前被支持,仅限于由单个(根)控制组组成的分层。能够随时从现存的控制组分层绑定/解绑子系统,未来会考虑支持实现。

然后在/sys/fs/cgroup/rg1下,你能找到系统中的控制组树。例如/sys/fs/cgroup/rg1也可以是容纳整个系统的控制组。

如果你想要更改release_agent的值:

# echo "/sbin/new_release_agent" > /sys/fs/cgroup/rg1/release_agent

它也可以在重新挂载时更改。

如果你想在/sys/fs/cgroup/rg1下创建新的控制组:

# cd /sys/fs/cgroup/rg1
# mkdir my_cgroup

现在你想要用这个控制组来做些什么的话:

# cd my_cgroup

在这个目录下,你可以找到几个文件:

# ls
cgroup.procs notify_on_release tasks
(plus whatever files added by the attached subsystems)

现在绑定你的当前shell到这个控制组:

# /bin/echo $$ > tasks

你也能在你的控制组内部创建控制组,在这个目录下使用mkdir:

# mkdir my_sub_cs

要移除控制组,只要使用rmdir就可以:

# rmdir my_sub_cs

如果控制组正在使用中(内部有控制组,或者有进程绑定绑定,或者其他子系统相关的引用保持激活状态),这操作就会失败。

# /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中,线程组中的所有任务将会被绑定到该控制组。写入0到cgroup.procs中就会移动当前写任务的线程组中的所有任务。

注意:因为每个任务总是某个已挂载分层下的控制组的成员,要从当前控制组移除任务,你必须移动它到新的控制组(可能是根控制组),就是通过写入新控制组的tasks文件的方式。

注意:由于受到一些控制组子系统的强制限制,移动进程到另外的控制组可能会失败。

当挂载控制组分层时传递name=选项,就会以给定的名字来关联分层。这么做是有用的,对于挂载一个已存在(pre-existing)的分层时,为了按名引用而不是按激活子系统的集合引用。每个分层或者是无名的或者是有一个唯一的名字。

名字应当匹配 [w.-]+

当传递name=选项给新的分层时,你需要手动定义子系统;当你给一个子系统命名一个名字,挂载所有子系统而什么都没有定义,这种行为是不被支持的。

子系统的名字作为分层的一部分会出现在/proc/mounts和/proc//cgroups中。

3 Kernel API

省略原文大概90行!我个人并不关注这一部分,所以没有翻译!!!

4 扩展属性用法

控制组文件系统支持它的目录和文件中扩展属性的特定类型,当前支持的类型是:

Trusted (XATTR_TRUSTED)

Security (XATTR_SECURITY)

他们都需要设置CAP_SYS_ADMIN功能。

跟在tmpfs中一样,控制组文件系统中的扩展属性使用内核内存来存储,建议保持最小使用。这就是为什么用户定义的扩展属性不支持的原因,因为任何用户都能这么做并且没有大小限制。

这个功能当前的已知用户是SELINUX,用来限制控制组在容器中和systemd的使用,以便对诸如控制组(systemd为每个服务创建的控制组)中的主PID这样的meta数据进行分类。

5 答疑

Q: 为什么要使用'/bin/echo'?
A: bash内嵌的echo命令不会检查对write()调用的错误,如果你在控制组文件系统中使用它,你将不知道命令是否执行成功还是失败。  

Q: 当我绑定很多进程时,只有第一行被真正绑定?
A: 每次对write()的调用只能返回一个错误,所以你应该就放一个PID。

手机扫一扫

移动阅读更方便

阿里云服务器
腾讯云服务器
七牛云服务器