简介

cgroups是control groups的缩写,它是linux内核提供的一种可以管理进程对资源(cpu、内存、网络设备等)的使用的机制。cgroups被LXC(Linux Containers)用于实现虚拟化的资源管理手段,可以说是LXC的实现基础。cgroups主要的作用针对一组进程(进程组)声明一个控制组,在这个控制组下可以设置各种控制参数,然后通过不同的子系统(资源控制器)来解析各自的控制参数来对进程组内的进程的资源使用进行控制。

cgroups主要提供以下几个方面的功能:

  • 限制资源使用。比如:memory子系统可以为进程组设定一个memory使用上限,一旦进程组使用的内存达到限额再申请内存,就会出发OOM(out of memory)
  • 优先级控制。比如:可以使用cpu子系统为某个进程组分配特定cpu share
  • 资源使用审计。比如:可以使用cpuacct子系统记录某个进程组使用的cpu时间
  • 进程隔离。比如:使用ns子系统可以使不同的进程组使用不同的namespace,以达到隔离的目的,不同的进程组有各自的进程、网络、文件系统挂载空间
  • 进程控制。比如:使用freezer子系统可以将进程组挂起和恢复

概念

cgroups中有许多概念,这里集中列举如下:

  • 任务(task),也就是系统中的一个进程
  • 控制组(control group),控制组就是一组进程。cgroups中的资源控制都以控制组为单位。一个进程可以加入某个控制组,也可以从某个控制组转移到另一个控制组。一个进程组的进程可以使用cgroups以控制组为单位分配的资源,同时受到cgroups以控制组为单位设定的限制
  • 层级(hierarchy),各个控制组实际上是通过树形的结构关联起来的,控制组树上的子节点会继承父节点的属性。
  • 子系统(subsystem),一个子系统实际上就是一个资源控制器。前面提到cgroup会声明各种资源的使用限制,而具体负责执行这些限制的就是各个子系统,比如CPU子系统就是控制CPU时间分配的一个控制器。cgroups的子系统有很多,并且也可以支持新增。子系统必须附加(attach)到一个层级上才能起作用,一个子系统attach到某个层级以后,这个层级上的所有控制族群都受到这个子系统的控制。

各个子系统

  1. cpu 子系统,主要限制进程的 cpu 使用率。
  2. cpuacct 子系统,可以统计 cgroups 中的进程的 cpu 使用报告。
  3. cpuset 子系统,可以为 cgroups 中的进程分配单独的 cpu 节点或者内存节点。
  4. memory 子系统,可以限制进程的 memory 使用量。
  5. blkio 子系统,可以限制进程的块设备 io。
  6. devices 子系统,可以控制进程能够访问某些设备。
  7. net_cls 子系统,可以标记 cgroups 中进程的网络数据包,然后可以使用 tc 模块(traffic control)对数据包进行控制。
  8. freezer 子系统,可以挂起或者恢复 cgroups 中的进程。
  9. ns 子系统,可以使不同 cgroups 下面的进程使用不同的 namespace。

使用示例

说了这么多概念和介绍,接下来看一下如果查看和操作cgroup的相关配置。

查看cgroup文件系统

cgroup在实现上把用户接口实现为文件系统的形式,我们可以通过执行mount -t cgroup来查看cgroup挂载的文件系统:

~$ mount -t cgroup
cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,release_agent=/lib/systemd/systemd-cgroups-agent,name=systemd)
cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpu,cpuacct)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,hugetlb)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices)
cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,net_cls,net_prio)
cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,perf_event)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer)
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset)

可以看到,在/sys/fs下有一个cgroup的目录,这是cgroup默认挂载的位置,下边有许多子目录,比如cpu、cpuset等等,这些就是不同的子系统的挂载点。每个挂载点即表示一棵与对应的子系统关联的cgroup树。

在一棵cgroup树里面,会包含系统中的所有进程,但每个进程只能属于树的其中一个节点(进程组)。系统中可以有很多cgroup树,每棵树都和不同的subsystem关联,一个进程可以属于多棵树,即一个进程可以属于多个进程组,只是这些进程组和不同的subsystem关联。

可以通过查看/proc/cgroups文件来确认当前系统支持的子系统,以及各个子系统绑定的cgroup树,比如:

~$ cat /proc/cgroups
#subsys_name    hierarchy       num_cgroups     enabled
cpuset          11              1               1
cpu             3               64              1
cpuacct         3               64              1
blkio           8               64              1
memory          9               104             1
devices         5               64              1
freezer         10              4               1
net_cls         6               1               1
perf_event      7               1               1
net_prio        6               1               1
hugetlb         4               1               1
pids            2               68              1

输出结果的第一列是子系统的名称,比如cpuset等等,第二列是子系统绑定的cgroup树id,可以注意到cpu和cpuacct绑定到了同一个cgroup树,也就是说它们在/sys/fs/cgroup下挂载的路径是相同的,实际上是/sys/fs/cgroup/cpu,cpuacct,然后cgroup目录下还会有两个名为cpu和cpuacct的链接,链接到它们共同的挂载点。第三列和第四列的输出分别表示cgroup树下有多少控制组(也就是树的节点个数)以及是否开启(可以通过设置内核的启动参数“cgroup_disable”来控制子系统的开启)。

更新cgroup配置

假如我们想要控制某个进程的cpu使用不能超过30%,我们就可以这样操作:

1、在/sys/fs/cgroup/cpu下新建一个名为“demo”的目录,这个操作实际上就是在cpu这个子系统绑定的cgroup树下面创建了一个新的节点,也就是控制组。

创建完demo目录后,我们可以看到demo目录下会自动新增很多文件,实际上这些文件在上一层的cpu目录下也存在。这些自动生成的文件实际上就是cgroup的各个控制属性,而前面提到过,作为cgroup树的子节点会继承父节点的特定属性,所以这些自动生成的文件也就是从cpu目录继承过来的。

各个文件的作用简单介绍如下:

  • cgroup.clone_children:这个文件只对cpuset(subsystem)有影响,当该文件的内容为1时,新创建的cgroup将会继承父cgroup的配置,即从父cgroup里面拷贝配置文件来初始化新cgroup
  • cgroup.procs:当前cgroup中的所有进程ID,系统不保证ID是顺序排列的,且ID有可能重复
  • release_agent:里面包含了cgroup退出时将会执行的命令,系统调用该命令时会将相应cgroup的相对路径当作参数传进去。 注意:这个文件只会存在于root cgroup下面,其他cgroup里面不会有这个文件。
  • tasks当前cgroup中的所有线程ID,系统不保证ID是顺序排列的

2、将cpu限额设置为20%,这里需要修改cpu.cfs_quota_us文件里的内容:

cat /sys/fs/cgroup/cpu/demo/cpu.cfs_quota_us 
-1
echo 20000 > /sys/fs/cgroup/cpu/demo/cpu.cfs_quota_us

3、将要限制的进程id(比如3529)添加到cgroup下,这里需要修改tasks文件的内容:

echo 3529 >> /sys/fs/cgroup/cpu/haoel/tasks

这样一来3529这个进程就会被cgroup的cpu子系统限制cpu的使用百分比为20%了。

值得注意的是这里将进程id加入了tasks文件而不是cgroup.procs下,这两者的区别在于tasks支持线程级别的控制,而cgroup.proc是进程级别的控制。

cgroups的实现

cgroups通过实现cgroup文件系统来为用户提供管理cgroup的工具,而cgroup文件系统是基于Linux VFS实现的。相应地,cgroups为控制文件定义了相应的数据结构cftype,对其操作由cgroup文件系统定义的通过操作捕获,再调用cftype定义的具体实现。所以说上述的例子中我们对相关的文件进行操作时,实际上被cgroup捕获并转换成了对内核中cgroup相应的数据结构的操作,也就完成了用户态操作到内核态操作的映射。