• Linux之Cgroup


    cgroup和namespace类似,也是将进程进行分组,但它的目的和namespace不一样,namespace是为了隔离进程组之间的资源,而cgroup是为了对一组进程进行统一的资源监控和限制。

    cgroup分v1v2两个版本,v1实现较早,功能比较多,但是由于它里面的功能都是零零散散的实现的,所以规划的不是很好,导致了一些使用和维护上的不便,v2的出现就是为了解决v1中这方面的问题,在最新的4.5内核中,cgroup v2声称已经可以用于生产环境了,但它所支持的功能还很有限,随着v2一起引入内核的还有cgroup namespace。v1和v2可以混合使用,但是这样会更复杂,所以一般没人会这样用。

    本系列只介绍v1,因为这是目前大家正在用的版本,包括systemd,docker等。如果对v1比较熟悉的话,适应v2也不是问题。

    本篇所有例子都在ubuntu-server-x86_64 16.04下执行通过

    为什么需要cgroup

    在Linux里,一直以来就有对进程进行分组的概念和需求,比如session group, progress group等,后来随着人们对这方面的需求越来越多,比如需要追踪一组进程的内存和IO使用情况等,于是出现了cgroup,用来统一将进程进行分组,并在分组的基础上对进程进行监控和资源控制管理等。

    什么是cgroup

    术语cgroup在不同的上下文中代表不同的意思,可以指整个Linux的cgroup技术,也可以指一个具体进程组。

    cgroup是Linux下的一种将进程按组进行管理的机制,在用户层看来,cgroup技术就是把系统中的所有进程组织成一颗一颗独立的树,每棵树都包含系统的所有进程,树的每个节点是一个进程组,而每颗树又和一个或者多个subsystem关联,树的作用是将进程分组,而subsystem的作用就是对这些组进行操作。cgroup主要包括下面两部分:

    • subsystem 一个subsystem就是一个内核模块,他被关联到一颗cgroup树之后,就会在树的每个节点(进程组)上做具体的操作。subsystem经常被称作"resource controller",因为它主要被用来调度或者限制每个进程组的资源,但是这个说法不完全准确,因为有时我们将进程分组只是为了做一些监控,观察一下他们的状态,比如perf_event subsystem。到目前为止,Linux支持12种subsystem,比如限制CPU的使用时间,限制使用的内存,统计CPU的使用情况,冻结和恢复一组进程等,后续会对它们一一进行介绍。

    • hierarchy 一个hierarchy可以理解为一棵cgroup树,树的每个节点就是一个进程组,每棵树都会与零到多个subsystem关联。在一颗树里面,会包含Linux系统中的所有进程,但每个进程只能属于一个节点(进程组)。系统中可以有很多颗cgroup树,每棵树都和不同的subsystem关联,一个进程可以属于多颗树,即一个进程可以属于多个进程组,只是这些进程组和不同的subsystem关联。目前Linux支持12种subsystem,如果不考虑不与任何subsystem关联的情况(systemd就属于这种情况),Linux里面最多可以建12颗cgroup树,每棵树关联一个subsystem,当然也可以只建一棵树,然后让这棵树关联所有的subsystem。当一颗cgroup树不和任何subsystem关联的时候,意味着这棵树只是将进程进行分组,至于要在分组的基础上做些什么,将由应用程序自己决定,systemd就是一个这样的例子。

    如何查看当前系统支持哪些subsystem

    可以通过查看/proc/cgroups(since Linux 2.6.24)知道当前系统支持哪些subsystem,下面是一个例子

    #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

    从左到右,字段的含义分别是:

    1. subsystem的名字

    2. subsystem所关联到的cgroup树的ID,如果多个subsystem关联到同一颗cgroup树,那么他们的这个字段将一样,比如这里的cpu和cpuacct就一样,表示他们绑定到了同一颗树。如果出现下面的情况,这个字段将为0:

      • 当前subsystem没有和任何cgroup树绑定

      • 当前subsystem已经和cgroup v2的树绑定

      • 当前subsystem没有被内核开启

    3. subsystem所关联的cgroup树中进程组的个数,也即树上节点的个数

    4. 1表示开启,0表示没有被开启(可以通过设置内核的启动参数“cgroup_disable”来控制subsystem的开启).

    如何使用cgroup

    cgroup相关的所有操作都是基于内核中的cgroup virtual filesystem,使用cgroup很简单,挂载这个文件系统就可以了。一般情况下都是挂载到/sys/fs/cgroup目录下,当然挂载到其它任何目录都没关系。

    这里假设目录/sys/fs/cgroup已经存在,下面用到的xxx为任意字符串,取一个有意义的名字就可以了,当用mount命令查看的时候,xxx会显示在第一列

    • 挂载一颗和所有subsystem关联的cgroup树到/sys/fs/cgroup

    mount -t cgroup xxx /sys/fs/cgroup

      挂载一颗和cpuset subsystem关联的cgroup树到/sys/fs/cgroup/cpuset

    mkdir /sys/fs/cgroup/cpuset
    mount -t cgroup -o cpuset xxx /sys/fs/cgroup/cpuset

      挂载一颗与cpu和cpuacct subsystem关联的cgroup树到/sys/fs/cgroup/cpu,cpuacct

    mkdir /sys/fs/cgroup/cpu,cpuacct
    mount -t cgroup -o cpu,cpuacct xxx /sys/fs/cgroup/cpu,cpuacct

      挂载一棵cgroup树,但不关联任何subsystem,下面就是systemd所用到的方式

    mkdir /sys/fs/cgroup/systemd
    mount -t cgroup -o none,name=systemd xxx /sys/fs/cgroup/systemd

    在很多使用systemd的系统中,比如ubuntu 16.04,systemd已经帮我们将各个subsystem和cgroup树关联并挂载好了

    dev@ubuntu:~$ mount|grep cgroup
    tmpfs on /sys/fs/cgroup type tmpfs (ro,nosuid,nodev,noexec,mode=755)
    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)

    创建并挂载好一颗cgroup树之后,就有了树的根节点,也即根cgroup,这时候就可以通过创建文件夹的方式创建子cgroup,然后再往每个子cgroup中添加进程。在后续介绍具体的subsystem的时候会详细介绍如何操作cgroup。

    注意

    • 第一次挂载一颗和指定subsystem关联的cgroup树时,会创建一颗新的cgroup树,当再一次用同样的参数挂载时,会重用现有的cgroup树,也即两个挂载点看到的内容是一样的。

    #在ubuntu 16.04中,systemd已经将和cpu,cpuacct绑定的cgroup树挂载到了/sys/fs/cgroup/cpu,cpuacct
    dev@ubuntu:~$ mount|grep /sys/fs/cgroup/cpu,cpuacct
    cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpu,cpuacct,nsroot=/)
    
    #创建一个子目录,用于后面的测试
    dev@ubuntu:~$ sudo mkdir /sys/fs/cgroup/cpu,cpuacct/test
    dev@ubuntu:~$ ls -l /sys/fs/cgroup/cpu,cpuacct/|grep test
    drwxr-xr-x  2 root root 0 Oct  9 02:27 test
    
    #将和cpu,cpuacct关联的cgroup树重新mount到另外一个目录
    dev@ubuntu:~$ mkdir -p ./cgroup/cpu,cpuacct && cd ./cgroup/
    dev@ubuntu:~/cgroup$ sudo mount -t cgroup -o cpu,cpuacct new-cpu-cpuacct ./cpu,cpuacct
    
    #在新目录中看到的内容和/sys/fs/cgroup/cpu,cpuacct的一样,
    #说明我们将同一颗cgroup树mount到了系统中的不同两个目录,
    #这颗cgroup树和subsystem的关联关系不变,
    #这点类似于mount同一块硬盘到多个目录
    dev@ubuntu:~/cgroup$ ls -l ./cpu,cpuacct/ |grep test
    drwxr-xr-x  2 root root 0 Oct  9 02:27 test
    
    #清理
    dev@ubuntu:~/cgroup$ sudo umount new-cpu-cpuacct

      挂载一颗cgroup树时,可以指定多个subsystem与之关联,但一个subsystem只能关联到一颗cgroup树,一旦关联并在这颗树上创建了子cgroup,subsystems和这棵cgroup树就成了一个整体,不能再重新组合。以上面ubuntu 16.04为例,由于已经将cpu,cpuacct和一颗cgroup树关联并且他们下面有子cgroup了,所以就不能单独的将cpu和另一颗cgroup树关联。

    #尝试将cpu subsystem重新关联一颗cgroup树并且将这棵树mount到./cpu目录
    dev@ubuntu:~/cgroup$ mkdir cpu
    dev@ubuntu:~/cgroup$ sudo mount -t cgroup -o cpu new-cpu ./cpu
    mount: new-cpu is already mounted or /home/dev/cgroup/cpu busy
    #由于cpu和cpuacct已经和一颗cgroup树关联了,所以这里mount失败
    
    #尝试将devices和pids关联到同一颗树上,由于他们各自已经关联到了不同的cgroup树,所以mount失败
    dev@ubuntu:~/cgroup$ mkdir devices,pids
    dev@ubuntu:~/cgroup$ sudo mount -t cgroup -o devices,pids new-devices-pids ./devices,pids
    mount: new-devices-pids is already mounted or /home/dev/cgroup/devices,pids busy

      但由于/sys/fs/cgroup/hugetlb和/sys/fs/cgroup/perf_event下没有子cgroup,我们可以将他们重新组合。一般情况下不会用到这个功能,一但最开始关联好了之后,就不会去重新修改它,也即我们一般不会去修改systemd给我们设置好的subsystem和cgroup树的关联关系。

    #/sys/fs/cgroup/hugetlb和/sys/fs/cgroup/perf_event里面没有子目录,说明没有子cgroup
    dev@ubuntu:~$ ls -l /sys/fs/cgroup/hugetlb|grep ^d
    dev@ubuntu:~$ ls -l /sys/fs/cgroup/perf_event|grep ^d
    
    #直接mount不行,因为perf_event,hugetlb已经被系统单独mount过了
    dev@ubuntu:~$ sudo mount -t cgroup -operf_event,hugetlb xxx /mnt
    mount: xxx is already mounted or /mnt busy
    
    #先umount
    dev@ubuntu:~$ sudo umount /sys/fs/cgroup/perf_event
    dev@ubuntu:~$ sudo umount /sys/fs/cgroup/hugetlb
    #如果系统默认安装了lxcfs的话,lxcfs会将它们挂载在自己的目录,
    #所以需要umount lxcfs及下面这两个目录,否则就没有真正的umount掉perf_event和hugetlb
    dev@ubuntu:~$ sudo umount lxcfs
    dev@ubuntu:~$ sudo umount /run/lxcfs/controllers/hugetlb
    dev@ubuntu:~$ sudo umount /run/lxcfs/controllers/perf_event
    
    #再mount,成功
    dev@ubuntu:~$ sudo mount -t cgroup -operf_event,hugetlb xxx /mnt
    dev@ubuntu:~$ ls /mnt/
    cgroup.clone_children  cgroup.sane_behavior  hugetlb.2MB.limit_in_bytes      hugetlb.2MB.usage_in_bytes  release_agent
    cgroup.procs           hugetlb.2MB.failcnt   hugetlb.2MB.max_usage_in_bytes  notify_on_release           tasks
    
    #清理
    dev@ubuntu:~$ sudo reboot

      可以创建任意多个不和任何subsystem关联的cgroup树,name是这棵树的唯一标记,当name指定的是一个新的名字时,将创建一颗新的cgroup树,但如果内核中已经存在一颗一样name的cgroup树,那么将mount已存在的这颗cgroup树

    #由于name=test的cgroup树在系统中不存在,所以这里会创建一颗新的name=test的cgroup树
    dev@ubuntu:~$ mkdir -p cgroup/test && cd cgroup
    dev@ubuntu:~/cgroup$ sudo mount -t cgroup -o none,name=test test ./test
    #系统为新创建的cgroup树的root cgroup生成了默认文件
    dev@ubuntu:~/cgroup$ ls ./test/
    cgroup.clone_children  cgroup.procs  cgroup.sane_behavior  notify_on_release  release_agent  tasks
    #新创建的cgroup树的root cgroup里包含系统中的所有进程
    dev@ubuntu:~/cgroup$ wc -l ./test/cgroup.procs
    131 ./test/cgroup.procs
    
    #创建子cgroup
    dev@ubuntu:~/cgroup$ cd test && sudo mkdir aaaa
    #系统已经为新的子cgroup生成了默认文件
    dev@ubuntu:~/cgroup/test$ ls aaaa
    cgroup.clone_children  cgroup.procs  notify_on_release  tasks
    #新创建的子cgroup中没有任何进程
    dev@ubuntu:~/cgroup/test$ wc -l aaaa/cgroup.procs
    0 aaaa/cgroup.procs
    
    #重新挂载这棵树到test1,由于mount的时候指定的name=test,所以和上面挂载的是同一颗cgroup树,于是test1目录下的内容和test目录下的内容一样
    dev@ubuntu:~/cgroup/test$ cd .. && mkdir test1
    dev@ubuntu:~/cgroup$ sudo mount -t cgroup -o none,name=test test ./test1
    dev@ubuntu:~/cgroup$ ls ./test1
    aaaa  cgroup.clone_children  cgroup.procs  cgroup.sane_behavior  notify_on_release  release_agent  tasks
    
    #清理
    dev@ubuntu:~/cgroup$ sudo umount ./test1
    dev@ubuntu:~/cgroup$ sudo umount ./test
    dev@ubuntu:~/cgroup$ cd .. && rm -r ./cgroup

    如何查看当前进程属于哪些cgroup

    可以通过查看/proc/[pid]/cgroup(since Linux 2.6.24)知道指定进程属于哪些cgroup。

    dev@ubuntu:~$ cat /proc/777/cgroup
    11:cpuset:/
    10:freezer:/
    9:memory:/system.slice/cron.service
    8:blkio:/system.slice/cron.service
    7:perf_event:/
    6:net_cls,net_prio:/
    5:devices:/system.slice/cron.service
    4:hugetlb:/
    3:cpu,cpuacct:/system.slice/cron.service
    2:pids:/system.slice/cron.service
    1:name=systemd:/system.slice/cron.service

    每一行包含用冒号隔开的三列,他们的意思分别是

    1. cgroup树的ID, 和/proc/cgroups文件中的ID一一对应。

    2. 和cgroup树绑定的所有subsystem,多个subsystem之间用逗号隔开。这里name=systemd表示没有和任何subsystem绑定,只是给他起了个名字叫systemd。

    3. 进程在cgroup树中的路径,即进程所属的cgroup,这个路径是相对于挂载点的相对路径。

    所有的subsystems

    目前Linux支持下面12种subsystem

    • cpu (since Linux 2.6.24; CONFIG_CGROUP_SCHED)
      用来限制cgroup的CPU使用率。

    • cpuacct (since Linux 2.6.24; CONFIG_CGROUP_CPUACCT)
      统计cgroup的CPU的使用率。

    • cpuset (since Linux 2.6.24; CONFIG_CPUSETS)
      绑定cgroup到指定CPUs和NUMA节点。

    • memory (since Linux 2.6.25; CONFIG_MEMCG)
      统计和限制cgroup的内存的使用率,包括process memory, kernel memory, 和swap。

    • devices (since Linux 2.6.26; CONFIG_CGROUP_DEVICE)
      限制cgroup创建(mknod)和访问设备的权限。

    • freezer (since Linux 2.6.28; CONFIG_CGROUP_FREEZER)
      suspend和restore一个cgroup中的所有进程。

    • net_cls (since Linux 2.6.29; CONFIG_CGROUP_NET_CLASSID)
      将一个cgroup中进程创建的所有网络包加上一个classid标记,用于tc和iptables。 只对发出去的网络包生效,对收到的网络包不起作用。

    • blkio (since Linux 2.6.33; CONFIG_BLK_CGROUP)
      限制cgroup访问块设备的IO速度。

    • perf_event (since Linux 2.6.39; CONFIG_CGROUP_PERF)
      对cgroup进行性能监控

    • net_prio (since Linux 3.3; CONFIG_CGROUP_NET_PRIO)
      针对每个网络接口设置cgroup的访问优先级。

    • hugetlb (since Linux 3.5; CONFIG_CGROUP_HUGETLB)
      限制cgroup的huge pages的使用量。

    • pids (since Linux 4.3; CONFIG_CGROUP_PIDS)
      限制一个cgroup及其子孙cgroup中的总进程数。

    上面这些subsystem,有些需要做资源统计,有些需要做资源控制,有些即不统计也不控制。对于cgroup树来说,有些subsystem严重依赖继承关系,有些subsystem完全用不到继承关系,而有些对继承关系没有严格要求。

    不同subsystem的工作方式可能差别较大,对系统性能的影响也不一样,本人不是这方面的专家,后续文章中只会从功能的角度来介绍不同的subsystem,不会涉及到他们内部的实现。

    结束语

    本文介绍了cgroup的一些概念,包括subsystem和hierarchy,然后介绍了怎么挂载cgroup文件系统以及12个subsystem的功能。从下一篇开始,将介绍cgroup具体的用法和不同的subsystem。

    本文将创建并挂载一颗不和任何subsystem绑定的cgroup树,用来演示怎么创建、删除子cgroup,以及如何往cgroup中添加和删除进程。

    由于不和任何subsystem绑定,所以这棵树没有任何实际的功能,但这不影响我们的演示,还有一个好处就是我们不会受subsystem功能的影响,可以将精力集中在cgroup树上。

    本篇所有例子都在ubuntu-server-x86_64 16.04下执行通过

    挂载cgroup树

    开始使用cgroup前需要先挂载cgroup树,下面先看看如何挂载一颗cgroup树,然后再查看其根目录下生成的文件

    #准备需要的目录
    dev@ubuntu:~$ mkdir cgroup && cd cgroup
    dev@ubuntu:~/cgroup$ mkdir demo
    
    #由于name=demo的cgroup树不存在,所以系统会创建一颗新的cgroup树,然后挂载到demo目录
    dev@ubuntu:~/cgroup$ sudo mount -t cgroup -o none,name=demo demo ./demo
    
    #挂载点所在目录就是这颗cgroup树的root cgroup,在root cgroup下面,系统生成了一些默认文件
    dev@ubuntu:~/cgroup$ ls ./demo/
    cgroup.clone_children  cgroup.procs  cgroup.sane_behavior  notify_on_release  release_agent  tasks
    
    #cgroup.procs里包含系统中的所有进程
    dev@ubuntu:~/cgroup$ wc -l ./demo/cgroup.procs
    131 ./demo/cgroup.procs

    下面是每个文件的含义:

    • cgroup.clone_children
      这个文件只对cpuset(subsystem)有影响,当该文件的内容为1时,新创建的cgroup将会继承父cgroup的配置,即从父cgroup里面拷贝配置文件来初始化新cgroup,可以参考这里

    • cgroup.procs
      当前cgroup中的所有进程ID,系统不保证ID是顺序排列的,且ID有可能重复

    • cgroup.sane_behavior
      具体功能不详,可以参考这里这里

    • notify_on_release
      该文件的内容为1时,当cgroup退出时(不再包含任何进程和子cgroup),将调用release_agent里面配置的命令。新cgroup被创建时将默认继承父cgroup的这项配置。

    • release_agent
      里面包含了cgroup退出时将会执行的命令,系统调用该命令时会将相应cgroup的相对路径当作参数传进去。 注意:这个文件只会存在于root cgroup下面,其他cgroup里面不会有这个文件。

    • tasks
      当前cgroup中的所有线程ID,系统不保证ID是顺序排列的

    后面在介绍如何往cgroup中添加进程时会介绍cgroup.procs和tasks的差别。

    创建和删除cgroup

    挂载好上面的cgroup树之后,就可以在里面建子cgroup了

    #创建子cgroup很简单,新建一个目录就可以了
    dev@ubuntu:~/cgroup$ cd demo
    dev@ubuntu:~/cgroup/demo$ sudo mkdir cgroup1
    
    #在新创建的cgroup里面,系统默认也生成了一些文件,这些文件的意义和root cgroup里面的一样
    dev@ubuntu:~/cgroup/demo$ ls cgroup1/
    cgroup.clone_children  cgroup.procs  notify_on_release  tasks
    
    #新创建的cgroup里没有任何进程和线程
    dev@ubuntu:~/cgroup/demo$ wc -l cgroup1/cgroup.procs
    0 cgroup1/cgroup.procs
    dev@ubuntu:~/cgroup/demo$ wc -l cgroup1/tasks
    0 cgroup1/tasks
    
    #每个cgroup都可以创建自己的子cgroup,所以我们也可以在cgroup1里面创建子cgroup
    dev@ubuntu:~/cgroup/demo$ sudo mkdir cgroup1/cgroup11
    dev@ubuntu:~/cgroup/demo$ ls cgroup1/cgroup11
    cgroup.clone_children  cgroup.procs  notify_on_release  tasks
    
    #删除cgroup也很简单,删除掉相应的目录就可以了
    dev@ubuntu:~/cgroup/demo$ sudo rmdir cgroup1/
    rmdir: failed to remove 'cgroup1/': Device or resource busy
    #这里删除cgroup1失败,是因为它里面包含了子cgroup,所以不能删除,
    #如果cgroup1包含有进程或者线程,也会删除失败
    
    #先删除cgroup11,再删除cgroup1就可以了
    dev@ubuntu:~/cgroup/demo$ sudo rmdir cgroup1/cgroup11/
    dev@ubuntu:~/cgroup/demo$ sudo rmdir cgroup1/

    添加进程

    创建新的cgroup后,就可以往里面添加进程了。注意下面几点:

    • 在一颗cgroup树里面,一个进程必须要属于一个cgroup。

    • 新创建的子进程将会自动加入父进程所在的cgroup。

    • 从一个cgroup移动一个进程到另一个cgroup时,只要有目的cgroup的写入权限就可以了,系统不会检查源cgroup里的权限。

    • 用户只能操作属于自己的进程,不能操作其他用户的进程,root账号除外。

    #--------------------------第一个shell窗口----------------------
    #创建一个新的cgroup
    dev@ubuntu:~/cgroup/demo$ sudo mkdir test
    dev@ubuntu:~/cgroup/demo$ cd test
    
    #将当前bash加入到上面新创建的cgroup中
    dev@ubuntu:~/cgroup/demo/test$ echo $$
    1421
    dev@ubuntu:~/cgroup/demo/test$ sudo sh -c 'echo 1421 > cgroup.procs'
    #注意:一次只能往这个文件中写一个进程ID,如果需要写多个的话,需要多次调用这个命令
    
    #--------------------------第二个shell窗口----------------------
    #重新打开一个shell窗口,避免第一个shell里面运行的命令影响输出结果
    #这时可以看到cgroup.procs里面包含了上面的第一个shell进程
    dev@ubuntu:~/cgroup/demo/test$ cat cgroup.procs
    1421
    
    #--------------------------第一个shell窗口----------------------
    #回到第一个窗口,运行top命令
    dev@ubuntu:~/cgroup/demo/test$ top
    #这里省略输出内容
    
    #--------------------------第二个shell窗口----------------------
    #这时再在第二个窗口查看,发现top进程自动和它的父进程(1421)属于同一个cgroup
    dev@ubuntu:~/cgroup/demo/test$ cat cgroup.procs
    1421
    16515
    dev@ubuntu:~/cgroup/demo/test$ ps -ef|grep top
    dev      16515  1421  0 04:02 pts/0    00:00:00 top
    dev@ubuntu:~/cgroup/demo/test$
    
    #在一颗cgroup树里面,一个进程必须要属于一个cgroup,
    #所以我们不能凭空从一个cgroup里面删除一个进程,只能将一个进程从一个cgroup移到另一个cgroup,
    #这里我们将1421移动到root cgroup
    dev@ubuntu:~/cgroup/demo/test$ sudo sh -c 'echo 1421 > ../cgroup.procs'
    dev@ubuntu:~/cgroup/demo/test$ cat cgroup.procs
    16515
    #移动1421到另一个cgroup之后,它的子进程不会随着移动
    
    #--------------------------第一个shell窗口----------------------
    ##回到第一个shell窗口,进行清理工作
    #先用ctrl+c退出top命令
    dev@ubuntu:~/cgroup/demo/test$ cd ..
    #然后删除创建的cgroup
    dev@ubuntu:~/cgroup/demo$ sudo rmdir test

    权限

    上面我们都是用sudo(root账号)来操作的,但实际上普通账号也可以操作cgroup

    #创建一个新的cgroup,并修改他的owner
    dev@ubuntu:~/cgroup/demo$ sudo mkdir permission
    dev@ubuntu:~/cgroup/demo$ sudo chown -R dev:dev ./permission/
    
    #1421原来属于root cgroup,虽然dev没有root cgroup的权限,但还是可以将1421移动到新的cgroup下,
    #说明在移动进程的时候,系统不会检查源cgroup里的权限。
    dev@ubuntu:~/cgroup/demo$ echo 1421 > ./permission/cgroup.procs
    
    #由于dev没有root cgroup的权限,再把1421移回root cgroup失败
    dev@ubuntu:~/cgroup/demo$ echo 1421 > ./cgroup.procs
    -bash: ./cgroup.procs: Permission denied
    
    #找一个root账号的进程
    dev@ubuntu:~/cgroup/demo$ ps -ef|grep /lib/systemd/systemd-logind
    root       839     1  0 01:52 ?        00:00:00 /lib/systemd/systemd-logind
    #因为该进程属于root,dev没有操作它的权限,所以将该进程加入到permission中失败
    dev@ubuntu:~/cgroup/demo$ echo 839 >./permission/cgroup.procs
    -bash: echo: write error: Permission denied
    #只能由root账号添加
    dev@ubuntu:~/cgroup/demo$ sudo sh -c 'echo 839 >./permission/cgroup.procs'
    
    #dev还可以在permission下创建子cgroup
    dev@ubuntu:~/cgroup/demo$ mkdir permission/c1
    dev@ubuntu:~/cgroup/demo$ ls permission/c1
    cgroup.clone_children  cgroup.procs  notify_on_release  tasks
    
    #清理
    dev@ubuntu:~/cgroup/demo$ sudo sh -c 'echo 839 >./cgroup.procs'
    dev@ubuntu:~/cgroup/demo$ sudo sh -c 'echo 1421 >./cgroup.procs'
    dev@ubuntu:~/cgroup/demo$ rmdir permission/c1
    dev@ubuntu:~/cgroup/demo$ sudo rmdir permission

    cgroup.procs vs tasks

    上面提到cgroup.procs包含的是进程ID, 而tasks里面包含的是线程ID,那么他们有什么区别呢?

    #创建两个新的cgroup用于演示
    dev@ubuntu:~/cgroup/demo$ sudo mkdir c1 c2
    
    #为了便于操作,先给root账号设置一个密码,然后切换到root账号
    dev@ubuntu:~/cgroup/demo$ sudo passwd root
    dev@ubuntu:~/cgroup/demo$ su root
    root@ubuntu:/home/dev/cgroup/demo#
    
    #系统中找一个有多个线程的进程
    root@ubuntu:/home/dev/cgroup/demo# ps -efL|grep /lib/systemd/systemd-timesyncd
    systemd+   610     1   610  0    2 01:52 ?        00:00:00 /lib/systemd/systemd-timesyncd
    systemd+   610     1   616  0    2 01:52 ?        00:00:00 /lib/systemd/systemd-timesyncd
    #进程610有两个线程,分别是610和616
    
    #将616加入c1/cgroup.procs
    root@ubuntu:/home/dev/cgroup/demo# echo 616 > c1/cgroup.procs
    #由于cgroup.procs存放的是进程ID,所以这里看到的是616所属的进程ID(610)
    root@ubuntu:/home/dev/cgroup/demo# cat c1/cgroup.procs
    610
    #从tasks中的内容可以看出,虽然只往cgroup.procs中加了线程616,
    #但系统已经将这个线程所属的进程的所有线程都加入到了tasks中,
    #说明现在整个进程的所有线程已经处于c1中了
    root@ubuntu:/home/dev/cgroup/demo# cat c1/tasks
    610
    616
    
    #将616加入c2/tasks中
    root@ubuntu:/home/dev/cgroup/demo# echo 616 > c2/tasks
    
    #这时我们看到虽然在c1/cgroup.procs和c2/cgroup.procs里面都有610,
    #但c1/tasks和c2/tasks中包含了不同的线程,说明这个进程的两个线程分别属于不同的cgroup
    root@ubuntu:/home/dev/cgroup/demo# cat c1/cgroup.procs
    610
    root@ubuntu:/home/dev/cgroup/demo# cat c1/tasks
    610
    root@ubuntu:/home/dev/cgroup/demo# cat c2/cgroup.procs
    610
    root@ubuntu:/home/dev/cgroup/demo# cat c2/tasks
    616
    #通过tasks,我们可以实现线程级别的管理,但通常情况下不会这么用,
    #并且在cgroup V2以后,将不再支持该功能,只能以进程为单位来配置cgroup
    
    #清理
    root@ubuntu:/home/dev/cgroup/demo# echo 610 > ./cgroup.procs
    root@ubuntu:/home/dev/cgroup/demo# rmdir c1
    root@ubuntu:/home/dev/cgroup/demo# rmdir c2
    root@ubuntu:/home/dev/cgroup/demo# exit
    exit

    release_agent

    当一个cgroup里没有进程也没有子cgroup时,release_agent将被调用来执行cgroup的清理工作。

    #创建新的cgroup用于演示
    dev@ubuntu:~/cgroup/demo$ sudo mkdir test
    #先enable release_agent
    dev@ubuntu:~/cgroup/demo$ sudo sh -c 'echo 1 > ./test/notify_on_release'
    
    #然后创建一个脚本/home/dev/cgroup/release_demo.sh,
    #一般情况下都会利用这个脚本执行一些cgroup的清理工作,但我们这里为了演示简单,仅仅只写了一条日志到指定文件
    dev@ubuntu:~/cgroup/demo$ cat > /home/dev/cgroup/release_demo.sh << EOF
    #!/bin/bash
    echo \$0:\$1 >> /home/dev/release_demo.log
    EOF
    
    #添加可执行权限
    dev@ubuntu:~/cgroup/demo$ chmod +x ../release_demo.sh
    
    #将该脚本设置进文件release_agent
    dev@ubuntu:~/cgroup/demo$ sudo sh -c 'echo /home/dev/cgroup/release_demo.sh > ./release_agent'
    dev@ubuntu:~/cgroup/demo$ cat release_agent
    /home/dev/cgroup/release_demo.sh
    
    #往test里面添加一个进程,然后再移除,这样就会触发release_demo.sh
    dev@ubuntu:~/cgroup/demo$ echo $$
    27597
    dev@ubuntu:~/cgroup/demo$ sudo sh -c 'echo 27597 > ./test/cgroup.procs'
    dev@ubuntu:~/cgroup/demo$ sudo sh -c 'echo 27597 > ./cgroup.procs'
    
    #从日志可以看出,release_agent被触发了,/test是cgroup的相对路径
    dev@ubuntu:~/cgroup/demo$ cat /home/dev/release_demo.log
    /home/dev/cgroup/release_demo.sh:/test

    结束语

    本文介绍了如何操作cgroup,由于没有和任何subsystem关联,所以在这颗树上的所有操作都没有实际的功能,不会对系统有影响。从下一篇开始,将介绍具体的subsystem。

     

    本篇将介绍一个简单的subsystem,名字叫pids,功能是限制cgroup及其所有子孙cgroup里面能创建的总的task数量。

    注意:这里的task指通过fork和clone函数创建的进程,由于clone函数也能创建线程(在Linux里面,线程是一种特殊的进程),所以这里的task也包含线程,本文统一以进程来代表task,即本文中的进程代表了进程和线程

    本篇所有例子都在ubuntu-server-x86_64 16.04下执行通过

    创建子cgroup

    在ubuntu 16.04里面,systemd已经帮我们将各个subsystem和cgroup树绑定并挂载好了,我们直接用现成的就可以了。

    #从这里的输出可以看到,pids已经被挂载在了/sys/fs/cgroup/pids,这是systemd做的
    dev@dev:~$ mount|grep pids
    cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids)

    创建子cgroup,取名为test

    #进入目录/sys/fs/cgroup/pids/并新建一个目录,即创建了一个子cgroup
    dev@dev:~$ cd /sys/fs/cgroup/pids/
    dev@dev:/sys/fs/cgroup/pids$ sudo mkdir test
    #这里将test目录的owner设置成dev账号,这样后续操作就不用每次都敲sudo了,省去麻烦
    dev@dev:/sys/fs/cgroup/pids$ sudo chown -R dev:dev ./test/

    再来看看test目录下的文件

    #除了上一篇中介绍的那些文件外,多了两个文件
    dev@dev:/sys/fs/cgroup/pids$ cd test
    dev@dev:/sys/fs/cgroup/pids/test$ ls
    cgroup.clone_children  cgroup.procs  notify_on_release  pids.current  pids.max  tasks

    下面是这两个文件的含义:

    • pids.current: 表示当前cgroup及其所有子孙cgroup中现有的总的进程数量

    #由于这是个新创建的cgroup,所以里面还没有任何进程
    dev@dev:/sys/fs/cgroup/pids/test$ cat pids.current 
    0

      pids.max: 当前cgroup及其所有子孙cgroup中所允许创建的总的最大进程数量,在根cgroup下没有这个文件,原因显而易见,因为我们没有必要限制整个系统所能创建的进程数量。

    #max表示没做任何限制
    dev@dev:/sys/fs/cgroup/pids/test$ cat pids.max 
    max

    限制进程数

    这里我们演示一下如何让限制功能生效

    #--------------------------第一个shell窗口----------------------
    #将pids.max设置为1,即当前cgroup只允许有一个进程
    dev@dev:/sys/fs/cgroup/pids/test$ echo 1 > pids.max
    #将当前bash进程加入到该cgroup
    dev@dev:/sys/fs/cgroup/pids/test$ echo $$ > cgroup.procs
    #--------------------------第二个shell窗口----------------------
    #重新打开一个bash窗口,在里面看看cgroup “test”里面的一些数据
    #因为这是一个新开的bash,跟cgroup ”test“没有任何关系,所以在这里运行命令不会影响cgroup “test”
    dev@dev:~$ cd /sys/fs/cgroup/pids/test
    #设置的最大进程数是1
    dev@dev:/sys/fs/cgroup/pids/test$ cat pids.max
    1
    #目前test里面已经有了一个进程,说明不能在fork或者clone进程了
    dev@dev:/sys/fs/cgroup/pids/test$ cat pids.current
    1
    #这个进程就是第一个窗口的bash
    dev@dev:/sys/fs/cgroup/pids/test$ cat cgroup.procs
    3083
    #--------------------------第一个shell窗口----------------------
    #回到第一个窗口,随便运行一个命令,由于当前pids.current已经等于pids.max了,
    #所以创建新进程失败,于是命令运行失败,说明限制生效
    dev@dev:/sys/fs/cgroup/pids/test$ ls
    -bash: fork: retry: No child processes
    -bash: fork: retry: No child processes
    -bash: fork: retry: No child processes
    -bash: fork: retry: No child processes
    -bash: fork: Resource temporarily unavailable

    当前cgroup和子cgroup之间的关系

    当前cgroup中的pids.current和pids.max代表了当前cgroup及所有子孙cgroup的所有进程,所以子孙cgroup中的pids.max大小不能超过父cgroup中的大小,如果子cgroup中的pids.max设置的大于父cgroup里的大小,会怎么样?请看下面的演示

    #继续使用上面的两个窗口
    #--------------------------第二个shell窗口----------------------
    #将pids.max设置成2
    dev@dev:/sys/fs/cgroup/pids/test$ echo 2 > pids.max
    #在test下面创建一个子cgroup
    dev@dev:/sys/fs/cgroup/pids/test$ mkdir subtest
    dev@dev:/sys/fs/cgroup/pids/test$ cd subtest/
    #将subtest的pids.max设置为5
    dev@dev:/sys/fs/cgroup/pids/test/subtest$ echo 5 > pids.max
    #将当前bash进程加入到subtest中
    dev@dev:/sys/fs/cgroup/pids/test/subtest$ echo $$ > cgroup.procs
    #--------------------------第三个shell窗口----------------------
    #重新打开一个bash窗口,看一下test和subtest里面的数据
    #test里面的数据如下:
    dev@dev:~$ cd /sys/fs/cgroup/pids/test
    dev@dev:/sys/fs/cgroup/pids/test$ cat pids.max
    2
    #这里为2表示目前test和subtest里面总的进程数为2
    dev@dev:/sys/fs/cgroup/pids/test$ cat pids.current
    2
    dev@dev:/sys/fs/cgroup/pids/test$ cat cgroup.procs
    3083
    
    #subtest里面的数据如下:
    dev@dev:/sys/fs/cgroup/pids/test$ cat subtest/pids.max
    5
    dev@dev:/sys/fs/cgroup/pids/test$ cat subtest/pids.current
    1
    dev@dev:/sys/fs/cgroup/pids/test$ cat subtest/cgroup.procs
    3185
    #--------------------------第一个shell窗口----------------------
    #回到第一个窗口,随便运行一个命令,由于test里面的pids.current已经等于pids.max了,
    #所以创建新进程失败,于是命令运行失败,说明限制生效
    dev@dev:/sys/fs/cgroup/pids/test$ ls
    -bash: fork: retry: No child processes
    -bash: fork: retry: No child processes
    -bash: fork: retry: No child processes
    -bash: fork: retry: No child processes
    -bash: fork: Resource temporarily unavailable
    #--------------------------第二个shell窗口----------------------
    #回到第二个窗口,随便运行一个命令,虽然subtest里面的pids.max还大于pids.current,
    #但由于其父cgroup “test”里面的pids.current已经等于pids.max了,
    #所以创建新进程失败,于是命令运行失败,说明子cgroup中的进程数不仅受自己的pids.max的限制,
    #还受祖先cgroup的限制
    dev@dev:/sys/fs/cgroup/pids/test/subtest$ ls
    -bash: fork: retry: No child processes
    -bash: fork: retry: No child processes
    -bash: fork: retry: No child processes
    -bash: fork: retry: No child processes
    -bash: fork: Resource temporarily unavailable

    pids.current > pids.max的情况

    并不是所有情况下都是pids.max >= pids.current,在下面两种情况下,会出现pids.max < pids.current 的情况:

    • 设置pids.max时,将其值设置的比pids.current小

    • #继续使用上面的三个窗口
      #--------------------------第三个shell窗口----------------------
      #将test的pids.max设置为1
      dev@dev:/sys/fs/cgroup/pids/test$ echo 1 > pids.max
      dev@dev:/sys/fs/cgroup/pids/test$ cat pids.max
      1
      #这个时候就会出现pids.current > pids.max的情况
      dev@dev:/sys/fs/cgroup/pids/test$ cat pids.current
      2
      
      #--------------------------第一个shell窗口----------------------
      #回到第一个shell
      #还是运行失败,说明虽然pids.current > pids.max,但限制创建新进程的功能还是会生效
      dev@dev:/sys/fs/cgroup/pids/test$ ls
      -bash: fork: retry: No child processes
      -bash: fork: retry: No child processes
      -bash: fork: retry: No child processes
      -bash: fork: retry: No child processes
      -bash: fork: Resource temporarily unavailable

      pids.max只会在当前cgroup中的进程fork、clone的时候生效,将其他进程加入到当前cgroup时,不会检测pids.max,所以将其他进程加入到当前cgroup有可能会导致pids.current > pids.max

    • #继续使用上面的三个窗口
      #--------------------------第三个shell窗口----------------------
      #将subtest中的进程移动到根cgroup下,然后删除subtest
      dev@dev:/sys/fs/cgroup/pids/test$ sudo sh -c 'echo 3185 > /sys/fs/cgroup/pids/cgroup.procs'
      #里面没有进程了,说明移动成功
      dev@dev:/sys/fs/cgroup/pids/test$ cat subtest/cgroup.procs
      #移除成功
      dev@dev:/sys/fs/cgroup/pids/test$ rmdir subtest/
      
      #这时候test下的pids.max等于pids.current了
      dev@dev:/sys/fs/cgroup/pids/test$ cat pids.max
      1
      dev@dev:/sys/fs/cgroup/pids/test$ cat pids.current
      1
      
      #--------------------------第二个shell窗口----------------------
      #将当前bash加入到test中
      dev@dev:/sys/fs/cgroup/pids/test/subtest$ cd ..
      dev@dev:/sys/fs/cgroup/pids/test$ echo $$ > cgroup.procs
      
      #--------------------------第三个shell窗口----------------------
      #回到第三个窗口,查看相关信息
      #第一个和第二个窗口的bash都属于test
      dev@dev:/sys/fs/cgroup/pids/test$ cat cgroup.procs
      3083
      3185
      dev@dev:/sys/fs/cgroup/pids/test$ cat pids.max
      1
      #出现了pids.current > pids.max的情况,这是因为我们将第二个窗口的shell加入了test
      dev@dev:/sys/fs/cgroup/pids/test$ cat pids.current
      2
      #--------------------------第二个shell窗口----------------------
      #对fork调用的限制仍然生效
      dev@dev:/sys/fs/cgroup/pids/test$ ls
      -bash: fork: retry: No child processes
      -bash: fork: retry: No child processes
      -bash: fork: retry: No child processes
      -bash: fork: retry: No child processes
      -bash: fork: Resource temporarily unavailable

      清理

    • #--------------------------第三个shell窗口----------------------
      dev@dev:/sys/fs/cgroup/pids/test$ sudo sh -c 'echo 3185 > /sys/fs/cgroup/pids/cgroup.procs'
      dev@dev:/sys/fs/cgroup/pids/test$ sudo sh -c 'echo 3083 > /sys/fs/cgroup/pids/cgroup.procs'
      dev@dev:/sys/fs/cgroup/pids/test$ cd ..
      dev@dev:/sys/fs/cgroup/pids$ sudo rmdir test/

    结束语

    本文介绍了如何利用pids这个subsystem来限制cgroup中的进程数,以及一些要注意的地方,总的来说pids比较简单。下一篇将介绍稍微复杂点的内存控制。

     

    为什么需要内存控制?

    代码总会有bug,有时会有内存泄漏,或者有意想不到的内存分配情况,或者这是个恶意程序,运行起来就是为了榨干系统内存,让其它进程无法分配到足够的内存而出现异常,如果系统配置了交换分区,会导致系统大量使用交换分区,从而系统运行很慢。

    • 站在一个普通Linux开发者的角度,如果能控制一个或者一组进程所能使用的内存数,那么就算代码有bug,内存泄漏也不会对系统造成影响,因为可以设置内存使用量的上限,当到达这个值之后可以将进程重启。

    • 站在一个系统管理者的角度,如果能限制每组进程所能使用的内存量,那么不管程序的质量如何,都能将它们对系统的影响降到最低,从而保证整个系统的稳定性。

    内存控制能控制些什么?

    • 限制cgroup中所有进程所能使用的物理内存总量

    • 限制cgroup中所有进程所能使用的物理内存+交换空间总量(CONFIG_MEMCG_SWAP): 一般在server上,不太会用到swap空间,所以不在这里介绍这部分内容。

    • 限制cgroup中所有进程所能使用的内核内存总量及其它一些内核资源(CONFIG_MEMCG_KMEM): 限制内核内存有什么用呢?其实限制内核内存就是限制当前cgroup所能使用的内核资源,比如进程的内核栈空间,socket所占用的内存空间等,通过限制内核内存,当内存吃紧时,可以阻止当前cgroup继续创建进程以及向内核申请分配更多的内核资源。由于这块功能被使用的较少,本篇中也不对它做介绍。

    内核相关的配置

    • 由于memory subsystem比较耗资源,所以内核专门添加了一个参数cgroup_disable=memory来禁用整个memory subsystem,这个参数可以通过GRUB在启动系统的时候传给内核,加了这个参数后内核将不再进行memory subsystem相关的计算工作,在系统中也不能挂载memory subsystem。

    • 上面提到的CONFIG_MEMCG_SWAP和CONFIG_MEMCG_KMEM都是扩展功能,在使用前请确认当前内核是否支持,下面看看ubuntu 16.04的内核:

    • #这里CONFIG_MEMCG_SWAP和CONFIG_MEMCG_KMEM等于y表示内核已经编译了该模块,即支持相关功能
      dev@dev:~$ cat /boot/config-`uname -r`|grep CONFIG_MEMCG
      CONFIG_MEMCG=y
      CONFIG_MEMCG_SWAP=y
      # CONFIG_MEMCG_SWAP_ENABLED is not set
      CONFIG_MEMCG_KMEM=y
      • CONFIG_MEMCG_SWAP控制内核是否支持Swap Extension,而CONFIG_MEMCG_SWAP_ENABLED(3.6以后的内核新加的参数)控制默认情况下是否使用Swap Extension,由于Swap Extension比较耗资源,所以很多发行版(比如ubuntu)默认情况下会禁用该功能(这也是上面那行被注释掉的原因),当然用户也可以根据实际情况,通过设置内核参数swapaccount=0或者1来手动禁用和启用Swap Extension。

      怎么控制?

      在ubuntu 16.04里面,systemd已经帮我们将memory绑定到了/sys/fs/cgroup/memory

    • #如果这里发现有多行结果,说明这颗cgroup数被绑定到了多个地方,
      #不过不要担心,由于它们都是指向同一颗cgroup树,所以它们里面的内容是一模一样的
      dev@dev:~$ mount|grep memory
      cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)

      创建子cgroup

      在/sys/fs/cgroup/memory下创建一个子目录即创建了一个子cgroup

    • #--------------------------第一个shell窗口----------------------
      dev@dev:~$ cd /sys/fs/cgroup/memory
      dev@dev:/sys/fs/cgroup/memory$ sudo mkdir test
      dev@dev:/sys/fs/cgroup/memory$ ls test
      cgroup.clone_children  memory.kmem.failcnt             memory.kmem.tcp.limit_in_bytes      memory.max_usage_in_bytes        memory.soft_limit_in_bytes  notify_on_release
      cgroup.event_control   memory.kmem.limit_in_bytes      memory.kmem.tcp.max_usage_in_bytes  memory.move_charge_at_immigrate  memory.stat                 tasks
      cgroup.procs           memory.kmem.max_usage_in_bytes  memory.kmem.tcp.usage_in_bytes      memory.numa_stat                 memory.swappiness
      memory.failcnt         memory.kmem.slabinfo            memory.kmem.usage_in_bytes          memory.oom_control               memory.usage_in_bytes
      memory.force_empty     memory.kmem.tcp.failcnt         memory.limit_in_bytes               memory.pressure_level            memory.use_hierarchy

      从上面ls的输出可以看出,除了每个cgroup都有的那几个文件外,和memory相关的文件还不少(由于ubuntu默认禁用了CONFIG_MEMCG_SWAP,所以这里看不到swap相关的文件),这里先做个大概介绍(kernel相关的文件除外),后面会详细介绍每个文件的作用

    •  cgroup.event_control       #用于eventfd的接口
       memory.usage_in_bytes      #显示当前已用的内存
       memory.limit_in_bytes      #设置/显示当前限制的内存额度
       memory.failcnt             #显示内存使用量达到限制值的次数
       memory.max_usage_in_bytes  #历史内存最大使用量
       memory.soft_limit_in_bytes #设置/显示当前限制的内存软额度
       memory.stat                #显示当前cgroup的内存使用情况
       memory.use_hierarchy       #设置/显示是否将子cgroup的内存使用情况统计到当前cgroup里面
       memory.force_empty         #触发系统立即尽可能的回收当前cgroup中可以回收的内存
       memory.pressure_level      #设置内存压力的通知事件,配合cgroup.event_control一起使用
       memory.swappiness          #设置和显示当前的swappiness
       memory.move_charge_at_immigrate #设置当进程移动到其他cgroup中时,它所占用的内存是否也随着移动过去
       memory.oom_control         #设置/显示oom controls相关的配置
       memory.numa_stat           #显示numa相关的内存

      参考:eventfdnuma

      添加进程

      “创建并管理cgroup”中介绍的一样,往cgroup中添加进程只要将进程号写入cgroup.procs就可以了

      注意:本篇将以进程为单位进行操作,不考虑以线程为单位进行管理(原因见“创建并管理cgroup”中cgroup.pro与tasks的区别),也即只写cgroup.procs文件,不会写tasks文件

       

    #--------------------------第二个shell窗口----------------------
    #重新打开一个shell窗口,避免相互影响
    dev@dev:~$ cd /sys/fs/cgroup/memory/test/
    dev@dev:/sys/fs/cgroup/memory/test$ echo $$
    4589
    dev@dev:/sys/fs/cgroup/memory/test$ sudo sh -c "echo $$ >> cgroup.procs"
    #运行top命令,这样这个cgroup消耗的内存会多点,便于观察
    dev@dev:/sys/fs/cgroup/memory/test$ top
    #后续操作不再在这个窗口进行,避免在这个bash中运行进程影响cgropu里面的进程数及相关统计

    设置限额

    设置限额很简单,写文件memory.limit_in_bytes就可以了,请仔细看示例

    #--------------------------第一个shell窗口----------------------
    #回到第一个shell窗口
    dev@dev:/sys/fs/cgroup/memory$ cd test
    #这里两个进程id分别时第二个窗口的bash和top进程
    dev@dev:/sys/fs/cgroup/memory/test$ cat cgroup.procs
    4589
    4664
    #开始设置之前,看看当前使用的内存数量,这里的单位是字节
    dev@dev:/sys/fs/cgroup/memory/test$ cat memory.usage_in_bytes
    835584
    
    #设置1M的限额
    dev@dev:/sys/fs/cgroup/memory/test$ sudo sh -c "echo 1M > memory.limit_in_bytes"
    #设置完之后记得要查看一下这个文件,因为内核要考虑页对齐, 所以生效的数量不一定完全等于设置的数量
    dev@dev:/sys/fs/cgroup/memory/test$ cat memory.limit_in_bytes
    1048576
    
    #如果不再需要限制这个cgroup,写-1到文件memory.limit_in_bytes即可
    dev@dev:/sys/fs/cgroup/memory/test$ sudo sh -c "echo -1 > memory.limit_in_bytes"
    #这时可以看到limit被设置成了一个很大的数字
    dev@dev:/sys/fs/cgroup/memory/test$ cat memory.limit_in_bytes
    9223372036854771712

    如果设置的限额比当前已经使用的内存少呢?如上面显示当前bash用了800多k,如果我设置limit为400K会怎么样?

    #--------------------------第一个shell窗口----------------------
    #先用free看下当前swap被用了多少
    dev@dev:/sys/fs/cgroup/memory/test$ free
                  total        used        free      shared  buff/cache   available
    Mem:         500192       45000       34200        2644      420992      424020
    Swap:        524284          16      524268
    #设置内存限额为400K
    dev@dev:/sys/fs/cgroup/memory/test$ sudo sh -c "echo 400K > memory.limit_in_bytes"
    
    #再看当前cgroup的内存使用情况
    #发现内存占用少了很多,刚好在400K以内,原来用的那些内存都去哪了呢?
    dev@dev:/sys/fs/cgroup/memory/test$ cat memory.usage_in_bytes
    401408
    
    #再看swap空间的占用情况,和刚开始比,多了500-16=384K,说明内存中的数据被移到了swap上
    dev@dev:/sys/fs/cgroup/memory/test$ free
                  total        used        free      shared  buff/cache   available
    Mem:         500192       43324       35132        2644      421736      425688
    Swap:        524284         500      523784
    
    #这个时候再来看failcnt,发现有453次之多(隔几秒再看这个文件,发现次数在增长)
    dev@dev:/sys/fs/cgroup/memory/test$ cat memory.failcnt
    453
    
    #再看看memory.stat(这里只显示部分内容),发现物理内存用了400K,
    #但有很多pgmajfault以及pgpgin和pgpgout,说明发生了很多的swap in和swap out
    dev@dev:/sys/fs/cgroup/memory/test$ cat memory.stat
    rss 409600
    total_pgpgin 4166
    total_pgpgout 4066
    total_pgfault 7558
    total_pgmajfault 419
    
    #从上面的结果可以看出,当物理内存不够时,就会触发memory.failcnt里面的数量加1,
    #但进程不会被kill掉,那是因为内核会尝试将物理内存中的数据移动到swap空间中,从而让内存分配成功

    如果设置的限额过小,就算swap out部分内存后还是不够会怎么样?

    #--------------------------第一个shell窗口----------------------
    dev@dev:/sys/fs/cgroup/memory/test$ sudo sh -c "echo 1K > memory.limit_in_bytes"
    #进程已经不在了(第二个窗口已经挂掉了)
    dev@dev:/sys/fs/cgroup/memory/test$ cat cgroup.procs
    dev@dev:/sys/fs/cgroup/memory/test$ cat memory.usage_in_bytes
    0
    #从这里的结果可以看出,第二个窗口的bash和top都被kill掉了

    从上面的这些测试可以看出,一旦设置了内存限制,将立即生效,并且当物理内存使用量达到limit的时候,memory.failcnt的内容会加1,但这时进程不一定就会被kill掉,内核会尽量将物理内存中的数据移到swap空间上去,如果实在是没办法移动了(设置的limit过小,或者swap空间不足),默认情况下,就会kill掉cgroup里面继续申请内存的进程。

    触发控制

    当物理内存达到上限后,系统的默认行为是kill掉cgroup中继续申请内存的进程,那么怎么控制这样的行为呢?答案是配置memory.oom_control

    这个文件里面包含了一个控制是否为当前cgroup启动OOM-killer的标识。如果写0到这个文件,将启动OOM-killer,当内核无法给进程分配足够的内存时,将会直接kill掉该进程;如果写1到这个文件,表示不启动OOM-killer,当内核无法给进程分配足够的内存时,将会暂停该进程直到有空余的内存之后再继续运行;同时,memory.oom_control还包含一个只读的under_oom字段,用来表示当前是否已经进入oom状态,也即是否有进程被暂停了。

    注意:root cgroup的oom killer是不能被禁用的

    为了演示OOM-killer的功能,创建了下面这样一个程序,用来向系统申请内存,它会每秒消耗1M的内存。

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    
    #define MB (1024 * 1024)
    
    int main(int argc, char *argv[])
    {
        char *p;
        int i = 0;
        while(1) {
            p = (char *)malloc(MB);
            memset(p, 0, MB);
            printf("%dM memory allocated\n", ++i);
            sleep(1);
        }
    
        return 0;
    }

    保存上面的程序到文件~/mem-allocate.c,然后编译并测试

    #--------------------------第一个shell窗口----------------------
    #编译上面的文件
    dev@dev:/sys/fs/cgroup/memory/test$ gcc ~/mem-allocate.c -o ~/mem-allocate
    #设置内存限额为5M
    dev@dev:/sys/fs/cgroup/memory/test$ sudo sh -c "echo 5M > memory.limit_in_bytes"
    #将当前bash加入到test中,这样这个bash创建的所有进程都会自动加入到test中
    dev@dev:/sys/fs/cgroup/memory/test$ sudo sh -c "echo $$ >> cgroup.procs"
    #默认情况下,memory.oom_control的值为0,即默认启用oom killer
    dev@dev:/sys/fs/cgroup/memory/test$ cat memory.oom_control
    oom_kill_disable 0
    under_oom 0
    #为了避免受swap空间的影响,设置swappiness为0来禁止当前cgroup使用swap
    dev@dev:/sys/fs/cgroup/memory/test$ sudo sh -c "echo 0 > memory.swappiness"
    #当分配第5M内存时,由于总内存量超过了5M,所以进程被kill了
    dev@dev:/sys/fs/cgroup/memory/test$ ~/mem-allocate
    1M memory allocated
    2M memory allocated
    3M memory allocated
    4M memory allocated
    Killed
    
    #设置oom_control为1,这样内存达到限额的时候会暂停
    dev@dev:/sys/fs/cgroup/memory/test$ sudo sh -c "echo 1 >> memory.oom_control"
    #跟预期的一样,程序被暂停了
    dev@dev:/sys/fs/cgroup/memory/test$ ~/mem-allocate
    1M memory allocated
    2M memory allocated
    3M memory allocated
    4M memory allocated
    
    #--------------------------第二个shell窗口----------------------
    #再打开一个窗口
    dev@dev:~$ cd /sys/fs/cgroup/memory/test/
    #这时候可以看到memory.oom_control里面under_oom的值为1,表示当前已经oom了
    dev@dev:/sys/fs/cgroup/memory/test$ cat memory.oom_control
    oom_kill_disable 1
    under_oom 1
    #修改test的额度为7M
    dev@dev:/sys/fs/cgroup/memory/test$ sudo sh -c "echo 7M > memory.limit_in_bytes"
    
    #--------------------------第一个shell窗口----------------------
    #再回到第一个窗口,会发现进程mem-allocate继续执行了两步,然后暂停在6M那里了
    dev@dev:/sys/fs/cgroup/memory/test$ ~/mem-allocate
    1M memory allocated
    2M memory allocated
    3M memory allocated
    4M memory allocated
    5M memory allocated
    6M memory allocated

    该文件还可以配合cgroup.event_control实现OOM的通知,当OOM发生时,可以收到相关的事件,下面是用于测试的程序,流程大概如下:

    1. 利用函数eventfd()创建一个efd;

    2. 打开文件memory.oom_control,得到ofd;

    3. 往cgroup.event_control中写入这么一串:<efd> <ofd>

    4. 通过读efd得到通知,然后打印一句话到终端

    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <sys/eventfd.h>
    #include <errno.h>
    #include <string.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    
    static inline void die(const char *msg)
    {
        fprintf(stderr, "error: %s: %s(%d)\n", msg, strerror(errno), errno);
        exit(EXIT_FAILURE);
    }
    
    #define BUFSIZE 256
    
    int main(int argc, char *argv[])
    {
        char buf[BUFSIZE];
        int efd, cfd, ofd;
        uint64_t u;
    
        if ((efd = eventfd(0, 0)) == -1)
            die("eventfd");
    
        snprintf(buf, BUFSIZE, "%s/%s", argv[1], "cgroup.event_control");
        if ((cfd = open(buf, O_WRONLY)) == -1)
            die("cgroup.event_control");
    
        snprintf(buf, BUFSIZE, "%s/%s", argv[1], "memory.oom_control");
        if ((ofd = open(buf, O_RDONLY)) == -1)
            die("memory.oom_control");
    
        snprintf(buf, BUFSIZE, "%d %d", efd, ofd);
        if (write(cfd, buf, strlen(buf)) == -1)
            die("write cgroup.event_control");
    
        if (close(cfd) == -1)
            die("close cgroup.event_control");
    
        for (;;) {
            if (read(efd, &u, sizeof(uint64_t)) != sizeof(uint64_t))
                die("read eventfd");
            printf("mem_cgroup oom event received\n");
        }
    
        return 0;
    }

    将上面的文件保存为~/oom_listen.c,然后测试如下

    #--------------------------第二个shell窗口----------------------
    #编译程序
    dev@dev:/sys/fs/cgroup/memory/test$ gcc ~/oom_listen.c -o ~/oom_listen
    #启用oom killer
    dev@dev:/sys/fs/cgroup/memory/test$ sudo sh -c "echo 0 >> memory.oom_control"
    #设置限额为2M,缩短测试周期
    dev@dev:/sys/fs/cgroup/memory/test$ sudo sh -c "echo 2M > memory.limit_in_bytes"
    #启动监听程序
    dev@dev:/sys/fs/cgroup/memory/test$ ~/oom_listen /sys/fs/cgroup/memory/test
    
    #--------------------------第一个shell窗口----------------------
    #连续运行两次mem-allocate,使它触发oom killer
    dev@dev:/sys/fs/cgroup/memory/test$ ~/mem-allocate
    1M memory allocated
    Killed
    dev@dev:/sys/fs/cgroup/memory/test$ ~/mem-allocate
    1M memory allocated
    Killed
    
    #--------------------------第二个shell窗口----------------------
    #回到第二个窗口可以看到,收到了两次oom事件
    dev@dev:/sys/fs/cgroup/memory/test$ ~/oom_listen /sys/fs/cgroup/memory/test
    mem_cgroup oom event received
    mem_cgroup oom event received

    其他

    进程迁移(migration)

    当一个进程从一个cgroup移动到另一个cgroup时,默认情况下,该进程已经占用的内存还是统计在原来的cgroup里面,不会占用新cgroup的配额,但新分配的内存会统计到新的cgroup中(包括swap out到交换空间后再swap in到物理内存中的部分)。

    我们可以通过设置memory.move_charge_at_immigrate让进程所占用的内存随着进程的迁移一起迁移到新的cgroup中。

    enable: echo 1 > memory.move_charge_at_immigrate
    disable:echo 0 > memory.move_charge_at_immigrate

    注意: 就算设置为1,但如果不是thread group的leader,这个task占用的内存也不能被迁移过去。换句话说,如果以线程为单位进行迁移,必须是进程的第一个线程,如果以进程为单位进行迁移,就没有这个问题。

    当memory.move_charge_at_immigrate被设置成1之后,进程占用的内存将会被统计到目的cgroup中,如果目的cgroup没有足够的内存,系统将尝试回收目的cgroup的部分内存(和系统内存紧张时的机制一样,删除不常用的file backed的内存或者swap out到交换空间上,请参考Linux内存管理),如果回收不成功,那么进程迁移将失败。

    注意:迁移内存占用数据是比较耗时的操作。

    移除cgroup

    当memory.move_charge_at_immigrate为0时,就算当前cgroup中里面的进程都已经移动到其它cgropu中去了,由于进程已经占用的内存没有被统计过去,当前cgroup有可能还占用很多内存,当移除该cgroup时,占用的内存需要统计到谁头上呢?答案是依赖memory.use_hierarchy的值,如果该值为0,将会统计到root cgroup里;如果值为1,将统计到它的父cgroup里面。

    force_empty

    当向memory.force_empty文件写入0时(echo 0 > memory.force_empty),将会立即触发系统尽可能的回收该cgroup占用的内存。该功能主要使用场景是移除cgroup前(cgroup中没有进程),先执行该命令,可以尽可能的回收该cgropu占用的内存,这样迁移内存的占用数据到父cgroup或者root cgroup时会快些。

    memory.swappiness

    该文件的值默认和全局的swappiness(/proc/sys/vm/swappiness)一样,修改该文件只对当前cgroup生效,其功能和全局的swappiness一样,请参考Linux交换空间中关于swappiness的介绍。

    注意:有一点和全局的swappiness不同,那就是如果这个文件被设置成0,就算系统配置的有交换空间,当前cgroup也不会使用交换空间。

    memory.use_hierarchy

    该文件内容为0时,表示不使用继承,即父子cgroup之间没有关系;当该文件内容为1时,子cgroup所占用的内存会统计到所有祖先cgroup中。

    如果该文件内容为1,当一个cgroup内存吃紧时,会触发系统回收它以及它所有子孙cgroup的内存。

    注意: 当该cgroup下面有子cgroup或者父cgroup已经将该文件设置成了1,那么当前cgroup中的该文件就不能被修改。

    #当前cgroup和父cgroup里都是1
    dev@dev:/sys/fs/cgroup/memory/test$ cat memory.use_hierarchy
    1
    dev@dev:/sys/fs/cgroup/memory/test$ cat ../memory.use_hierarchy
    1
    
    #由于父cgroup里面的值为1,所以修改当前cgroup的值失败
    dev@dev:/sys/fs/cgroup/memory/test$ sudo sh -c "echo 0 > ./memory.use_hierarchy"
    sh: echo: I/O error
    
    #由于父cgroup里面有子cgroup(至少有当前cgroup这么一个子cgroup),
    #修改父cgroup里面的值也失败
    dev@dev:/sys/fs/cgroup/memory/test$ sudo sh -c "echo 0 > ../memory.use_hierarchy"
    sh: echo: I/O error

    memory.soft_limit_in_bytes

    有了hard limit(memory.limit_in_bytes),为什么还要soft limit呢?hard limit是一个硬性标准,绝对不能超过这个值,而soft limit可以被超越,既然能被超越,要这个配置还有啥用?先看看它的特点

    1. 当系统内存充裕时,soft limit不起任何作用

    2. 当系统内存吃紧时,系统会尽量的将cgroup的内存限制在soft limit值之下(内核会尽量,但不100%保证)

    从它的特点可以看出,它的作用主要发生在系统内存吃紧时,如果没有soft limit,那么所有的cgroup一起竞争内存资源,占用内存多的cgroup不会让着内存占用少的cgroup,这样就会出现某些cgroup内存饥饿的情况。如果配置了soft limit,那么当系统内存吃紧时,系统会让超过soft limit的cgroup释放出超过soft limit的那部分内存(有可能更多),这样其它cgroup就有了更多的机会分配到内存。

    从上面的分析看出,这其实是系统内存不足时的一种妥协机制,给次等重要的进程设置soft limit,当系统内存吃紧时,把机会让给其它重要的进程。

    注意: 当系统内存吃紧且cgroup达到soft limit时,系统为了把当前cgroup的内存使用量控制在soft limit下,在收到当前cgroup新的内存分配请求时,就会触发回收内存操作,所以一旦到达这个状态,就会频繁的触发对当前cgroup的内存回收操作,会严重影响当前cgroup的性能。

    memory.pressure_level

    这个文件主要用来监控当前cgroup的内存压力,当内存压力大时(即已使用内存快达到设置的限额),在分配内存之前需要先回收部分内存,从而影响内存分配速度,影响性能,而通过监控当前cgroup的内存压力,可以在有压力的时候采取一定的行动来改善当前cgroup的性能,比如关闭当前cgroup中不重要的服务等。目前有三种压力水平:

    low

    意味着系统在开始为当前cgroup分配内存之前,需要先回收内存中的数据了,这时候回收的是在磁盘上有对应文件的内存数据。

    medium

    意味着系统已经开始频繁为当前cgroup使用交换空间了。

    critical

    快撑不住了,系统随时有可能kill掉cgroup中的进程。

    如何配置相关的监听事件呢?和memory.oom_control类似,大概步骤如下:

    1. 利用函数eventfd(2)创建一个event_fd

    2. 打开文件memory.pressure_level,得到pressure_level_fd

    3. 往cgroup.event_control中写入这么一串:<event_fd> <pressure_level_fd> <level>

    4. 然后通过读event_fd得到通知

    注意: 多个level可能要创建多个event_fd,好像没有办法共用一个(本人没有测试过)

    Memory thresholds

    我们可以通过cgroup的事件通知机制来实现对内存的监控,当内存使用量穿过(变得高于或者低于)我们设置的值时,就会收到通知。使用方法和memory.oom_control类似,大概步骤如下:

    1. 利用函数eventfd(2)创建一个event_fd

    2. 打开文件memory.usage_in_bytes,得到usage_in_bytes_fd

    3. 往cgroup.event_control中写入这么一串:<event_fd> <usage_in_bytes_fd> <threshold>

    4. 然后通过读event_fd得到通知

    stat file

    这个文件包含的统计项比较细,需要一些内核的内存管理知识才能看懂,这里就不介绍了(怕说错)。详细信息可以参考Memory Resource Controller中的“5.2 stat file”。这里有几个需要注意的地方:

    • 里面total开头的统计项包含了子cgroup的数据(前提条件是memory.use_hierarchy等于1)。

    • 里面的'rss + file_mapped"才约等于是我们常说的RSS(ps aux命令看到的RSS)

    • 文件(动态库和可执行文件)及共享内存可以在多个进程之间共享,不过它们只会统计到他们的owner cgroup中的file_mapped去。(不确定是怎么定义owner的,但如果看到当前cgroup的file_mapped值很小,说明共享的数据没有算到它头上,而是其它的cgroup)

    结束语

    本篇没有介绍swap和kernel相关的内容,不过在实际使用过程中一定要留意swap空间,如果系统使用了交换空间,那么设置限额时一定要注意一点,那就是当cgroup的物理空间不够时,内核会将不常用的内存swap out到交换空间上,从而导致一直不触发oom killer,而是不停的swap out/in,导致cgroup中的进程运行速度很慢。如果一定要用交换空间,最好的办法是限制swap+物理内存的额度,虽然我们在这篇中没有介绍这部分内容,但其使用方法和限制物理内存是一样的,只是换做写文件memory.memsw.limit_in_bytes罢了。

    在cgroup里面,跟CPU相关的子系统有cpusetscpuacctcpu

    其中cpuset主要用于设置CPU的亲和性,可以限制cgroup中的进程只能在指定的CPU上运行,或者不能在指定的CPU上运行,同时cpuset还能设置内存的亲和性。设置亲和性一般只在比较特殊的情况才用得着,所以这里不做介绍。

    cpuacct包含当前cgroup所使用的CPU的统计信息,信息量较少,有兴趣可以去看看它的文档,这里不做介绍。

    本篇只介绍cpu子系统,包括怎么限制cgroup的CPU使用上限及相对于其它cgroup的相对值。

    本篇所有例子都在ubuntu-server-x86_64 16.04下执行通过

    创建子cgroup

    在ubuntu下,systemd已经帮我们mount好了cpu子系统,我们只需要在相应的目录下创建子目录就可以了

    #从这里的输出可以看到,cpuset被挂载在了/sys/fs/cgroup/cpuset,
    #而cpu和cpuacct一起挂载到了/sys/fs/cgroup/cpu,cpuacct下面
    dev@ubuntu:~$ mount|grep cpu
    cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset)
    cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpu,cpuacct)
    
    #进入/sys/fs/cgroup/cpu,cpuacct并创建子cgroup
    dev@ubuntu:~$ cd /sys/fs/cgroup/cpu,cpuacct
    dev@ubuntu:/sys/fs/cgroup/cpu,cpuacct$ sudo mkdir test
    dev@ubuntu:/sys/fs/cgroup/cpu,cpuacct$ cd test
    dev@ubuntu:/sys/fs/cgroup/cpu,cpuacct/test$ ls
    cgroup.clone_children  cpuacct.stat   cpuacct.usage_percpu  cpu.cfs_quota_us  cpu.stat           tasks
    cgroup.procs           cpuacct.usage  cpu.cfs_period_us     cpu.shares        notify_on_release

    除了cgroup里面通用的cgroup.clone_children、tasks、cgroup.procs、notify_on_release这几个文件外,以cpuacct.开头的文件跟cpuacct子系统有关,我们这里只需要关注cpu.开头的文件。

    cpu.cfs_period_us & cpu.cfs_quota_us

    cfs_period_us用来配置时间周期长度,cfs_quota_us用来配置当前cgroup在设置的周期长度内所能使用的CPU时间数,两个文件配合起来设置CPU的使用上限。两个文件的单位都是微秒(us),cfs_period_us的取值范围为1毫秒(ms)到1秒(s),cfs_quota_us的取值大于1ms即可,如果cfs_quota_us的值为-1(默认值),表示不受cpu时间的限制。下面是几个例子:

    1.限制只能使用1个CPU(每250ms能使用250ms的CPU时间)
        # echo 250000 > cpu.cfs_quota_us /* quota = 250ms */
        # echo 250000 > cpu.cfs_period_us /* period = 250ms */
    
    2.限制使用2个CPU(内核)(每500ms能使用1000ms的CPU时间,即使用两个内核)
        # echo 1000000 > cpu.cfs_quota_us /* quota = 1000ms */
        # echo 500000 > cpu.cfs_period_us /* period = 500ms */
    
    3.限制使用1个CPU的20%(每50ms能使用10ms的CPU时间,即使用一个CPU核心的20%)
        # echo 10000 > cpu.cfs_quota_us /* quota = 10ms */
        # echo 50000 > cpu.cfs_period_us /* period = 50ms */

    cpu.shares

    shares用来设置CPU的相对值,并且是针对所有的CPU(内核),默认值是1024,假如系统中有两个cgroup,分别是A和B,A的shares值是1024,B的shares值是512,那么A将获得1024/(1204+512)=66%的CPU资源,而B将获得33%的CPU资源。shares有两个特点:

    • 如果A不忙,没有使用到66%的CPU时间,那么剩余的CPU时间将会被系统分配给B,即B的CPU使用率可以超过33%

    • 如果添加了一个新的cgroup C,且它的shares值是1024,那么A的限额变成了1024/(1204+512+1024)=40%,B的变成了20%

    从上面两个特点可以看出:

    • 在闲的时候,shares基本上不起作用,只有在CPU忙的时候起作用,这是一个优点。

    • 由于shares是一个绝对值,需要和其它cgroup的值进行比较才能得到自己的相对限额,而在一个部署很多容器的机器上,cgroup的数量是变化的,所以这个限额也是变化的,自己设置了一个高的值,但别人可能设置了一个更高的值,所以这个功能没法精确的控制CPU使用率。

    cpu.stat

    包含了下面三项统计结果

    • nr_periods: 表示过去了多少个cpu.cfs_period_us里面配置的时间周期

    • nr_throttled: 在上面的这些周期中,有多少次是受到了限制(即cgroup中的进程在指定的时间周期中用光了它的配额)

    • throttled_time: cgroup中的进程被限制使用CPU持续了多长时间(纳秒)

    示例

    这里以cfs_period_us & cfs_quota_us为例,演示一下如何控制CPU的使用率。

    #继续使用上面创建的子cgroup: test
    #设置只能使用1个cpu的20%的时间
    dev@ubuntu:/sys/fs/cgroup/cpu,cpuacct/test$ sudo sh -c "echo 50000 > cpu.cfs_period_us"
    dev@ubuntu:/sys/fs/cgroup/cpu,cpuacct/test$ sudo sh -c "echo 10000 > cpu.cfs_quota_us"
    
    #将当前bash加入到该cgroup
    dev@ubuntu:/sys/fs/cgroup/cpu,cpuacct/test$ echo $$
    5456
    dev@ubuntu:/sys/fs/cgroup/cpu,cpuacct/test$ sudo sh -c "echo 5456 > cgroup.procs"
    
    #在bash中启动一个死循环来消耗cpu,正常情况下应该使用100%的cpu(即消耗一个内核)
    dev@ubuntu:/sys/fs/cgroup/cpu,cpuacct/test$ while :; do echo test > /dev/null; done
    
    #--------------------------重新打开一个shell窗口----------------------
    #通过top命令可以看到5456的CPU使用率为20%左右,说明被限制住了
    #不过这时系统的%us+%sy在10%左右,那是因为我测试的机器上cpu是双核的,
    #所以系统整体的cpu使用率为10%左右
    dev@ubuntu:~$ top
    Tasks: 139 total,   2 running, 137 sleeping,   0 stopped,   0 zombie
    %Cpu(s):  5.6 us,  6.2 sy,  0.0 ni, 88.2 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
    KiB Mem :   499984 total,    15472 free,    81488 used,   403024 buff/cache
    KiB Swap:        0 total,        0 free,        0 used.   383332 avail Mem
    
      PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
     5456 dev       20   0   22640   5472   3524 R  20.3  1.1   0:04.62 bash
    
    #这时可以看到被限制的统计结果
    dev@ubuntu:~$ cat /sys/fs/cgroup/cpu,cpuacct/test/cpu.stat
    nr_periods 1436
    nr_throttled 1304
    throttled_time 51542291833

    结束语

    使用cgroup限制CPU的使用率比较纠结,用cfs_period_us & cfs_quota_us吧,限制死了,没法充分利用空闲的CPU,用shares吧,又没法配置百分比,极其难控制。总之,使用cgroup的cpu子系统需谨慎。

     

    资料摘自:https://segmentfault.com/a/1190000009732550

  • 相关阅读:
    ADO.NET的记忆碎片(四)
    ADO.NET的记忆碎片(八)
    卡特兰数 应用
    hdu 1249 三角形
    hdu 1143
    nyist 93 汉诺塔(三)
    hdu 1123 Train Problem II
    hdu 1133 Buy the Ticket
    hdu 1022 Train Problem I
    nyist 610 定长覆盖
  • 原文地址:https://www.cnblogs.com/hanzeng1993/p/16004432.html
Copyright © 2020-2023  润新知