cgroups(control groups)资源控制组,它不仅可以限制被namespace隔离起来的资源,还可以为资源设置权重、计算使用量、操控任务(进程或线程)启停等。一般来说,cgroup(单数形式)用于指定整个功能,当需要明确表示多个资源控制组的时候,用cgruops(复数形式)。
以下根据Docker容器与容器与描述统一使用cgroups
官方定义如下:内核cgroup官方文档
cgroups是Linux内核提供的一种机制,这种机制可以根据需求把一系列系统任务及其子任务整合(或分隔)到按资源划分等级的不同组内,从而为系统资源管理提供一个统一的框架。
通俗地说,cgorups可以限制、记录任务组所使用的物理资源(包括CPU、Memory、IO等),为容器实现虚拟化提供了基本保证,是构建Docker等一系列虚拟化管理工具的基石。
对开发者来说,cgroups有以下4个特点:
本质上来说,cgroups是内核附加在程序上的一系列钩子(hook),通过程序运行时对资源的调度触发相应的钩子以达到资源追踪和限制的目的。
实现cgroups的主要目的是为不同用户层面的资源管理,提供一个统一化的接口。从单个任务的资源控制到操作系统层面的虚拟化,cgroups提供了以下四大功能。
传统的Unix任务管理,实际上是先启动init任务作为根节点,再由init节点创建子任务作为子节点,而每个子节点又可以创建新的子节点,如此往复,形成一个树状结构。而系统中的多个cgroup也构成类似的树状结构,子节点从父节点继承属性。
它们最大的不同在于,系统中的多个cgroup构成的层级并非单根结构,可以允许存在多个。如果任务模型是由init作为根节点构成的一棵树,那么系统中的多个cgroup则是由多个层级构成的森林。这样做的目的很好理解,如果只有一个层级,那么所有的任务都将被迫绑定其上的所有子系统,这会给某些任务造成不必要的限制。在Docker中,每个子系统独自构成一个层级,这样做非常易于管理。
上面介绍的是cgroups的组织结构,再来看看cgroup、任务、子系统、层级四者间的关系及其基本规则。
root cgroup
。对于创建的每个层级,任务只能存在于其中一个cgorup中,即一个任务不能存在于同一个层级的不同cgroup中,但一个任务可以存在于不同层级中的多个cgroup中。如果操作时把一个任务添加到同一个层级中的另一个cgorup中欧冠,则会将它从第一个cgroup中移除。在下图可以看到,httpd任务已经加入到层级A中的/cg1(①),而不能加入同一个层级中的/cg2(②),但是可以加入层级B中的/cg3(③)。如果需要第二步成功,则httpd任务就会从/cg1的task中移除。子系统实际上就是cgroups的资源控制系统,每种子系统独立地控制一种资源,目前Docker使用如下9种子系统,其中,net_cls子系统在内核中已经广泛实现,但是Docker尚未使用。
Docker本身并没有对cgroup本身做增强,容器用户一般也不需要直接操作cgroup
Linux中cgroup的实现形式表现为一个文件系统,因此需要mount这个文件系统才能够使用(/sys/fs/cgroup),挂在成功后,就能看到各类子系统。
[root@wxtest062vm6 ~]# mount -t cgroup
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpuacct,cpu)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,net_prio,net_cls)
cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids)
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices)
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer)
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)
以cpu子系统为例,先看一下挂载了这个子系统的控制组下的文件,其中system.slice是systemd创建的cgroup组。具体参考深入理解 Linux Cgroup 系列(二):玩转 CPU
[root@wxtest062vm6 cpu]# ls /sys/fs/cgroup/cpu
cgroup.clone_children cpuacct.stat cpu.cfs_quota_us cpu.stat release_agent
cgroup.event_control cpuacct.usage cpu.rt_period_us docker system.slice
cgroup.procs cpuacct.usage_percpu cpu.rt_runtime_us machine.slice tasks
cgroup.sane_behavior cpu.cfs_period_us cpu.shares notify_on_release user.slice
在/sys/fs/cgroup
的cpu子目录下创建控制组cgroup,控制组目录创建成功后,它下面就会有很多类似的文件了。可以看到/sys/fs/cgroup/cpu
和/sys/fs/cgroup/cpu/cg1
下的文件基本相同,少了release_agent
和cgroup.sane_behavior
两个文件,其中release_agent
里面存放着cgroup退出时将会执行的命令
[root@wxtest062vm6 cpu]# mkdir cg1
[root@wxtest062vm6 cpu]# ls cg1
cgroup.clone_children cgroup.procs cpuacct.usage cpu.cfs_period_us cpu.rt_period_us cpu.shares notify_on_release
cgroup.event_control cpuacct.stat cpuacct.usage_percpu cpu.cfs_quota_us cpu.rt_runtime_us cpu.stat tasks
下面举一个例子来展示如何限制PID为18828的进程的cpu使用配额:
# 限制18828进程
$ echo 18828 >> /sys/fs/cgroup/cpu/cg1/tasks
# 将cpu限制为最高使用20%
$ echo 20000 > /sys/fs/cgroup/cpu/cg1/cpu.cfs_quota_us
在Docker的实现中,Docker daemon会在每个子系统控制组目录(比如/sys/fs/cgroup/cpu
)下创建一个名为docker的控制组,然后在docker控制组里面,再为每一个容器创建一个以容器ID为名称的容器控制组,这个容器里的所有进程的PID都会写到该控制组的tasks目录中,并且在控制文件(比如cpu.cfs_quota_us
)中写入预设的限制参数值。综上,docker控制组的层级结构如下。
[root@wxtest062vm6 cpu]# tree /sys/fs/cgroup/cpu/docker
/sys/fs/cgroup/cpu/docker
├── 5d086ec0d09a7dffa7c127ed3a345fa0351e2bdcab8eff598100239f0556e826
│ ├── cgroup.clone_children
│ ├── cgroup.event_control
│ ├── cgroup.procs
│ ├── cpuacct.stat
│ ├── cpuacct.usage
│ ├── cpuacct.usage_percpu
│ ├── cpu.cfs_period_us
│ ├── cpu.cfs_quota_us
│ ├── cpu.rt_period_us
│ ├── cpu.rt_runtime_us
│ ├── cpu.shares
│ ├── cpu.stat
│ ├── notify_on_release
│ └── tasks
├── cgroup.clone_children
├── cgroup.event_control
├── cgroup.procs
├── cpuacct.stat
├── cpuacct.usage
├── cpuacct.usage_percpu
├── cpu.cfs_period_us
├── cpu.cfs_quota_us
├── cpu.rt_period_us
├── cpu.rt_runtime_us
├── cpu.shares
├── cpu.stat
├── notify_on_release
└── tasks
cgroups的实现本质上是给任务挂上钩子(hook),当任务运行的过程中涉及某种资源时,就会触发钩子上所附带的子系统进行检测,根据资源类别的不同,使用对应的技术进行资源限制和优先级分配
对于不同的系统资源,cgroups提供了统一个接口对资源进行控制和设计,但限制的具体方式则不尽相同。比如memory子系统,会在描述内存状态的"mm_struct"结构体中记录它所属的cgroup,当进程需要申请更多内存时,就会触发cgroup用量检测,用量超过cgroup限定的限额,则拒绝用户的内存申请,否则就给予相应内存并在cgroup的统计信息中记录。实际实现要比上述描述地要复杂得多,不仅需要考虑内存的分配与回收,还需要考虑不同类似的内存如cache(缓存)和swap(交换区与内存拓展)等。
进程所需的内存超过它所属的cgroup的最大限额以后,如果设置了OOM Control
(Out of Memory Control),那么进程就会收到OOM信号并结束;否则进程就会被挂起,进入睡眠状态,直到cgroup中其他进程释放了足够的内存资源为止。Docker中默认是开启OOM Control
的。其他子系统的实现与此类似,cgroups提供了多种资源限制的策略供用户选择。
实现上,cgroup与任务之间是多对多的关系,所以它们并不直接关联,而是通过一个中间结构把双向的关联信息记录起来。每个任务结构体task_struct中都包含了一个指针,可以查询到对应cgroup的情况,同时也可以查询到各个子系统的状态,这些子系统状态中也包含了找到任务的指针,不同类型的子系统按需定义本身的控制信息结构体,最终在自定义的结构体中把子系统状态指针包含进去,然后内核通过container_of
(这个宏可以通过一个结构体的成员找到结构体自身)等宏定义来获取对应的结构体,关联到任务,以此达到资源限制的目的。
同时,为了让cgroups便于用户理解和使用,也为了用精简的内核代码为cgroup提供熟悉的权限和命名空间管理,内核按照Linux虚拟文件系统转换器(Virtual FileSystem Switsh,VFS)接口实现了一套名为cgroup的文件系统,非常巧妙地用来表示cgroups的层级概念,把各个子系统的实现都封装到文件系统的各项操作中。
在实际的使用过程中,Docker需要通过挂载cgroup文件系统新建一个层级结构,挂载时指定要绑定的子系统。把cgroup文件系统挂载上以后,就可以像操作文件一样对cgroups的层级进行浏览和操作管理(包括权限管理、子文件管理等)。除了cgroup文件系统以外,内核没有为cgroups的访问和操作添加任何系统调用(即用户只能够通过操作cgroup文件系统来访问cgroups)。
如果新建的层级结构要绑定的子系统与目前已经存在的层级结构完全相同,那么新的挂载会重用原来已经存在的那一套(指向相同的css_set)。否则,如果要绑定的子系统已经被别的层级绑定,就会返回挂载失败的错误。如果一切顺利,挂载完成后层级就被激活并与相应子系统关联起来,可以开始使用了。
目前无法将一个新的子系统绑定到激活的层级上,或者从一个激活的层级中解除某个子系统的绑定。
当一个顶层的cgroup文件系统被卸载(umount)时,如果其中创建过深层次的后代cgroup目录,那么就算上层的cgroup被卸载了,层级也是激活状态,其后代cgroup中的配置依旧有效。只有递归式地卸载层级中的所有cgruop,那个层级才会被真正地删除。
在创建的层级中创建文件夹,就类似于fork了一个后代cgroup,后代cgroup中默认继承原有cgroup中的配置属性,但是可以根据需求对配置参数进行调整。这样就把一个大的cgroup系统分割成一个个嵌套的、可动态变化的“软分区”。
有些文件是cpu子系统特有的,有些文件是每个子系统都有的。
以资源开头(比如cpu.shares)的文件都是用来限制这个cgroup下任务的可用的配置文件。
一个cgroup创建完成,不管绑定了何种子系统,其目录下都会生成以下几个文件,用来描述cgroup的相应信息。同样,把相应信息写入这些配置文件就可以生效,内容如下:
cgroup.procs
文件里记录一个该任务所在任务组的TGID值,但是该任务组的其他任务并不受影响root cgroup
下面,其他cgroup里面不会有这个文件。这个脚本通常用于自动化卸载无用的cgroup。root cgroup
会有这个目录