cgroup 比较有趣的地方是它没有提供任何的系统调用接口,所以你不能用 API Call 的方式使用 cgroup,实际上 cgroup 实现了 linux 虚拟文件系统 vfs,所以类似我们熟悉的 btfrs, ext4, 因此可以用类似文件系统的方式进行操作。

比如用 mount 命令看一下 linux 上挂载了哪些设备:

# mount -t cgroup
/dev/sda2 on / type ext4 (rw,relatime)

cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,name=systemd)
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/rdma type cgroup (rw,nosuid,nodev,noexec,relatime,rdma)

cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime,nsdelegate,memory_recursiveprot)

可以看到,

  • 第一行是磁盘 sda2 挂载在根目录 /, 它的类型是 ext4
  • 后面几行是 cgroup 挂载在了目录 /sys/fs/cgroup/,类型是 cgroup
  • 如果你的内核比较新的话,将看不到上面那些 cgroup 的行,而是只能看到最后这一行 cgroup2,这是因为新版本的内核使用了 cgroup v2 。

另外类似于 “net_cls”, “rdma” 这些都是 cgroup 子系统的名字,详见本文结尾的附录。

知道了上面这些,那么我们就能用操作文件系统的方式使用 cgroup 了,正好我有两台 linux VM,

  • 一个 Ubuntu Server 22.04,内核 5.15, 默认使用了 cgroup v2
  • 另一个是 Ubuntu Server 20.04, 内核 5.4,使用 cgroup v1

cgroup 和 cgroup2 有很多不一样的地方,具体见参考资料 1。本文所有例子都基于 cgroup v2。

直接使用 cgroup

既然 cgroup 实现了 VFS,那么就能用 mkdir 创建一个目录,比如

# cd /sys/fs/cgroup/
# mkdir test
# ls test 
cgroup.controllers      cgroup.type            cpu.stat                  hugetlb.1GB.rsvd.max      io.stat              memory.oom.group     pids.events
cgroup.events           cpu.idle               cpu.uclamp.max            hugetlb.2MB.current       io.weight            memory.pressure      pids.max
cgroup.freeze           cpu.max                cpu.uclamp.min            hugetlb.2MB.events        memory.current       memory.stat          rdma.current
cgroup.kill             cpu.max.burst
  1. cgroup.controllers:这个文件显示了当前cgoup可以限制的相关资源有哪些。
  2. cpu.max 限制了最大可用的 cpu 时间。
    1. 在 cgroup v1 中可以设置 cpu.cfs_quota_uscpu.cfs_period_us 的值来限制 cpu 使用率,参考资料里大多是用的 v1。
    2. 到了 cgroup v2 我们需要在 cpu.max 文件中写入这两个值。后面还会提到。

如果要设置某个 cgroup 的参数就是直接往对应的文件中写入特定格式的内容,比如要限制 cgroup 能够使用的 CPU 核数:

# echo 0-1 > /sys/fs/cgroup/test/cpuset.cpus

如果要用 “test” group 限制某个进程,只要把这个进程 PID 写入 cgroup.procs 文件

// $$ 表示当前 bash 的 PID,
$ echo $$ | sudo tee /sys/fs/cgroup/test/cgroup.procs
25982

上面这个例子的结果就是所有在当前 bash 创建的进程都会收到 cgroup “test” 的限制。

显然,这样直接的方式很不方便,因此就有了 cgroup-tools。

使用 cgroup-tools

在 ubuntu 系统中可以用下面这个命令安装

sudo apt-get install -y cgroup-tools

新版本的 tools 也加入了对 cgroup v2 的支持。下面通过 2 个例子来熟悉一下使用方式。

例 1 - 限制进程可用的 CPU 时间

还是沿用上面已经创建好的名为 “test” 的 group。

首先确认一下 root group 里面的 cgroup.controllers 和 subtree_control 包含了 cpu,如果没有的话需要手动加上 “cpu”。

root@loquat:/sys/fs/cgroup# cat cgroup.controllers
cpuset cpu io memory hugetlb pids rdma misc

root@loquat:/sys/fs/cgroup# cat cgroup.subtree_control
cpuset cpu io memory pids

# test group 的 cgroup.controllers 与上层 subtree_control 保持一致
root@loquat:/sys/fs/cgroup# cat test/cgroup.controllers
cpuset cpu io memory pids

再在 cpu.max 中写入

root@loquat:/sys/fs/cgroup# cat test/cpu.max
10000 100000

其含义是在linux CFS 进程调度的周期 1000000 us (也就是 100ms) 内,使用 10000 us (10 ms)。

再写一个程序循环很多次

void main(){
    unsigned int i, end;

    end = 1024 * 1024 * 1024;
    for(i = 0; i < end; )
    {
        i ++;
    }
}

结果:

root@loquat:/home/runzhen/code/cgroups# time ./cputime

real	0m1.937s
user	0m1.937s
sys	0m0.000s
root@loquat:/home/runzhen/code/cgroups# time cgexec -g cpu:test ./cputime

real	0m19.560s
user	0m1.975s
sys	0m0.000s

其中 user 表示的就是用户感知的时间,可见,当没有 cgroup 限制时大约 1.9 秒就结束了,当有 cgroup 限制时用了 19 秒,十倍的差距,符合我们之前设定的值

例 2 - 限制进程可用的最大内存

类似的,再 memory 相关文件中写入值。

root@loquat:/sys/fs/cgroup/test# cat memory.max
5242880
root@loquat:/sys/fs/cgroup/test# cat memory.swap.max
0
  1. memory.max 中值的单位是 “bytes”,因此 5242880 表示最大使用 500MB 内存。

  2. 另外需要注意的是关闭 swap,设置为 0

测试程序如下

#include<stdio.h>
#include<stdlib.h>
#include<string.h>

#define CHUNK_SIZE 1024 * 1024 * 100

void main()
{
    char *p;
    int i;

    for(i = 0; i < 5; i ++)
    {
        p = malloc(sizeof(char) * CHUNK_SIZE);
        if(p == NULL)
        {
            printf("fail to malloc!");
            return ;
        }

        memset(p, 0, CHUNK_SIZE);
        printf("malloc memory %d MB\n", (i + 1) * 100);
    }
}

运行结果

root@loquat:/home/runzhen/code/cgroups# ./mem
malloc memory 100 MB
malloc memory 200 MB
malloc memory 300 MB
malloc memory 400 MB
malloc memory 500 MB

root@loquat:/home/runzhen/code/cgroups# cgexec -g memory:test ./mem
Killed

测试程序因为 OOM 被 kill 了。

使用 stress 测试工具也能看到进程被 SIGKILL signal 9 了。

# cgexec -g memory:test stress --vm 1 --vm-bytes 500000000 --vm-keep --verbose
stress: info: [2684] dispatching hogs: 0 cpu, 0 io, 1 vm, 0 hdd
stress: dbug: [2684] using backoff sleep of 3000us
stress: dbug: [2684] --> hogvm worker 1 [2685] forked
stress: dbug: [2685] allocating 500000000 bytes ...
stress: dbug: [2685] touching bytes in strides of 4096 bytes ...
stress: FAIL: [2684] (416) <-- worker 2685 got signal 9
stress: WARN: [2684] (418) now reaping child worker processes
stress: FAIL: [2684] (452) failed run completed in 0s

附录

子资源系统(Resource Classes or SubSystem)

目前有下面这些资源子系统:

  • Block IO(blkio):限制块设备(磁盘、SSD、USB 等)的 IO 速率
  • CPU Set(cpuset):限制任务能运行在哪些 CPU 核上
  • CPU Accounting(cpuacct):生成 cgroup 中任务使用 CPU 的报告
  • CPU (CPU):限制调度器分配的 CPU 时间
  • Devices (devices):允许或者拒绝 cgroup 中任务对设备的访问
  • Freezer (freezer):挂起或者重启 cgroup 中的任务
  • Memory (memory):限制 cgroup 中任务使用内存的量,并生成任务当前内存的使用情况报告
  • Network Classifier(net_cls):为 cgroup 中的报文设置上特定的 classid 标志,这样 tc 等工具就能根据标记对网络进行配置
  • Network Priority (net_prio):对每个网络接口设置报文的优先级
  • perf_event:识别任务的 cgroup 成员,可以用来做性能分析

参考资料

  1. https://adtxl.com/index.php/archives/179.html
  2. https://zorrozou.github.io/docs/详解Cgroup V2.html
  3. cgroup-tools 支持 cgroup v2 https://github.com/libcgroup/libcgroup/issues/12
  4. https://askubuntu.com/questions/1406329/how-to-run-cgexec-without-sudo-as-current-user-on-ubuntu-22-04-with-cgroups-v2
  5. https://www.cnblogs.com/sparkdev/p/8296063.html
  6. https://cizixs.com/2017/08/25/linux-cgroup/