cgroups文件系统
  Linux 使用了多种数据结构在内核中实现了 cgroups 的配置,关联了进程和 cgroups 节点,那么 Linux 又是如何让用户态的进程使用到 cgroups 的功能呢? Linux内核有一个很强大的模块叫 VFS (Virtual File System)。 VFS 能够把具体文件系统的细节隐藏起来,给用户态进程提供一个统一的文件系统 API 接口。 cgroups 也是通过 VFS 把功能暴露给用户态的,cgroups 与 VFS 之间的衔接部分称之为 cgroups 文件系统。下面先介绍一下 VFS 的基础知识,然后再介绍下 cgroups 文件系统的实现。
  VFS
  VFS 是一个内核抽象层,能够隐藏具体文件系统的实现细节,从而给用户态进程提供一套统一的 API 接口。VFS 使用了一种通用文件系统的设计,具体的文件系统只要实现了 VFS 的设计接口,能够注册到 VFS 中,从而使内核可以读写这种文件系统。 这很像面向对象设计中的抽象类与子类之间的关系,抽象类负责对外接口的设计,子类负责具体的实现。其实,VFS本身是用 c 语言实现的一套面向对象的接口。
  通用文件模型
  VFS 通用文件模型中包含以下四种元数据结构:
  超级块对象(superblock object),用于存放已经注册的文件系统的信息。比如ext2,ext3等这些基础的磁盘文件系统,还有用于读写socket的socket文件系统,以及当前的用于读写cgroups配置信息的 cgroups 文件系统等。
  索引节点对象(inode object),用于存放具体文件的信息。对于一般的磁盘文件系统而言,inode 节点中一般会存放文件在硬盘中的存储块等信息;对于socket文件系统,inode会存放socket的相关属性,而对于cgroups这样的特殊文件系统,inode会存放与 cgroup 节点相关的属性信息。这里面比较重要的一个部分是一个叫做 inode_operations 的结构体,这个结构体定义了在具体文件系统中创建文件,删除文件等的具体实现。
  文件对象(file object),一个文件对象表示进程内打开的一个文件,文件对象是存放在进程的文件描述符表里面的。同样这个文件中比较重要的部分是一个叫 file_operations 的结构体,这个结构体描述了具体的文件系统的读写实现。当进程在某一个文件描述符上调用读写操作时,实际调用的是 file_operations 中定义的方法。 对于普通的磁盘文件系统,file_operations 中定义的是普通的块设备读写操作;对于socket文件系统,file_operations 中定义的是 socket 对应的 send/recv 等操作;而对于cgroups这样的特殊文件系统,file_operations 中定义的是操作 cgroup 结构体等具体的实现。
  目录项对象(dentry object),在每个文件系统中,内核在查找某一个路径中的文件时,会为内核路径上的每一个分量都生成一个目录项对象,通过目录项对象能够找到对应的 inode 对象,目录项对象一般会被缓存,从而提高内核查找速度。
  cgroups文件系统的实现
  基于 VFS 实现的文件系统,都必须实现 VFS 通用文件模型定义的这些对象,并实现这些对象中定义的部分函数。cgroup 文件系统也不例外,下面来看一下 cgroups 中这些对象的定义。
  首先看一下 cgroups 文件系统类型的结构体:
  static struct file_system_type cgroup_fs_type = {
  .name = "cgroup",
  .mount = cgroup_mount,
  .kill_sb = cgroup_kill_sb,
  };
  这里面两个函数分别代表安装和卸载某一个 cgroup 文件系统所需要执行的函数。每次把某一个 cgroups 子系统安装到某一个装载点的时候,cgroup_mount 方法会被调用,这个方法会生成一个 cgroups_root(cgroups层级结构的根)并封装成超级快对象。
  然后看一下 cgroups 超级块对象定义的操作:
  static const struct super_operations cgroup_ops = {
  .statfs = simple_statfs,
  .drop_inode = generic_delete_inode,
  .show_options = cgroup_show_options,
  .remount_fs = cgroup_remount,
  };
  这里只有部分函数的实现,这是因为对于特定的文件系统而言,所支持的操作可能仅是 super_operations 中所定义操作的一个子集,比如说对于块设备上的文件对象,肯定是支持类似 fseek 的查找某个位置的操作,但是对于 socket 或者 cgroups 这样特殊的文件系统,不支持这样的操作。
  同样简单看下 cgroups 文件系统对 inode 对象和 file 对象定义的特殊实现函数:
static const struct inode_operations cgroup_dir_inode_operations = {
.lookup = cgroup_lookup,
.mkdir = cgroup_mkdir,
.rmdir = cgroup_rmdir,
.rename = cgroup_rename,
};
static const struct file_operations cgroup_file_operations = {
.read = cgroup_file_read,
.write = cgroup_file_write,
.llseek = generic_file_llseek,
.open = cgroup_file_open,
.release = cgroup_file_release,
};
  本文并不去研究这些函数的代码实现是什么样的,但是从这些代码可以推断出,cgroups 通过实现 VFS 的通用文件系统模型,把维护 cgroups 层级结构的细节,隐藏在 cgroups 文件系统的这些实现函数中。
  从另一个方面说,用户在用户态对 cgroups 文件系统的操作,通过 VFS 转化为对 cgroups 层级结构的维护。通过这样的方式,内核把 cgroups 的功能暴露给了用户态的进程。
  cgroups使用方法
  cgroups文件系统挂载

  Linux中,用户可以使用mount命令挂载 cgroups 文件系统,格式为: mount -t cgroup -o subsystems name /cgroup/name,其中 subsystems 表示需要挂载的 cgroups 子系统, /cgroup/name 表示挂载点,如上文所提,这条命令同时在内核中创建了一个cgroups 层级结构。
  比如挂载 cpuset, cpu, cpuacct, memory 4个subsystem到/cgroup/cpu_and_mem 目录下,可以使用 mount -t cgroup -o remount,cpu,cpuset,memory cpu_and_mem /cgroup/cpu_and_mem
  在centos下面,在使用yum install libcgroup安装了cgroups模块之后,在 /etc/cgconfig.conf 文件中会自动生成 cgroups 子系统的挂载点:
  mount {
  cpuset = /cgroup/cpuset;
  cpu = /cgroup/cpu;
  cpuacct = /cgroup/cpuacct;
  memory = /cgroup/memory;
  devices = /cgroup/devices;
  freezer = /cgroup/freezer;
  net_cls = /cgroup/net_cls;
  blkio = /cgroup/blkio;
  }
  上面的每一条配置都等价于展开的 mount 命令,例如mount -t cgroup -o cpuset cpuset /cgroup/cpuset。这样系统启动之后会自动把这些子系统挂载到相应的挂载点上。
  子节点和进程
  挂载某一个 cgroups 子系统到挂载点之后,可以通过在挂载点下面建立文件夹或者使用cgcreate命令的方法创建 cgroups 层级结构中的节点。比如通过命令cgcreate -t sankuai:sankuai -g cpu:test可以在 cpu 子系统下建立一个名为 test 的节点。结果如下所示:
  [root@idx cpu]
  # ls
  cgroup.event_control cgroup.procs cpu.cfs_period_us cpu.cfs_quota_us cpu.rt_period_us cpu.rt_runtime_us cpu.shares cpu.stat lxc notify_on_release release_agent tasks test
  然后可以通过写入需要的值到 test 下面的不同文件,来配置需要限制的资源。每个子系统下面都可以进行多种不同的配置,需要配置的参数各不相同,详细的参数设置需要参考 cgroups 手册。使用 cgset 命令也可以设置 cgroups 子系统的参数,格式为 cgset -r
  parameter=value path_to_cgroup。
  当需要删除某一个 cgroups 节点的时候,可以使用 cgdelete 命令,比如要删除上述的 test 节点,可以使用 cgdelete -r cpu:test命令进行删除
  把进程加入到 cgroups 子节点也有多种方法,可以直接把 pid 写入到子节点下面的 task 文件中。也可以通过 cgclassify 添加进程,格式为 cgclassify -g subsystems:path_to_cgroup pidlist,也可以直接使用 cgexec 在某一个 cgroups 下启动进程,格式为gexec -g subsystems:path_to_cgroup command arguments.
  实践中的例子
  相信大多数人都没有读过 Docker 的源代码,但是通过这篇文章,可以估计 Docker 在实现不同的 Container 之间资源隔离和控制的时候,是可以创建比较复杂的 cgroups 节点和配置文件来完成的。然后对于同一个 Container 中的进程,可以把这些进程 PID 添加到同一组 cgroups 子节点中已达到对这些进程进行同样的资源限制。
  通过各大互联网公司在网上的技术文章,也可以看到很多公司的云平台都是基于 cgroups 技术搭建的,其实也都是把进程分组,然后把整个进程组添加到同一组 cgroups 节点中,受到同样的资源限制。
  笔者所在的广告组,有一部分任务是给合作的广告投放网站生成“商品信息”,广告投放网站使用这些信息,把广告投放在他们各自的网站上。但是有时候会有恶意的爬虫过来爬取商品信息,所以我们生成了另外“一小份”数据供优先级较低的用户下载,这时候基本能够区分开大部分恶意爬虫。对于这样的“一小份”数据,对及时更新的要求不高,生成商品信息又是一个比较费资源的任务,所以我们把这个任务的cpu资源使用率限制在了50%。
  首先在 cpu 子系统下面创建了一个 halfapi 的子节点:cgcreate abc:abc -g cpu:halfapi。
  然后在配置文件中写入配置数据:echo 50000 > /cgroup/cpu/halfapi/cpu.cfs_quota_us。cpu.cfs_quota_us中的默认值是100000,写入50000表示只能使用50%的 cpu 运行时间。
  后在这个cgroups中启动这个任务:cgexec -g “cpu:/halfapi” php halfapi.php half >/dev/null 2>&1
  在 cgroups 引入内核之前,想要完成上述的对某一个进程的 cpu 使用率进行限制,只能通过 nice 命令调整进程的优先级,或者 cpulimit 命令限制进程使用进程的 cpu 使用率。但是这些命令的缺点是无法限制一个进程组的资源使用限制,也无法完成 Docker 或者其他云平台所需要的这一类轻型容器的资源限制要求。
  同样,在 cgroups 之前,想要完成对某一个或者某一组进程的物理内存使用率的限制,几乎是不可能完成的。使用 cgroups 提供的功能,可以轻易的限制系统内某一组服务的物理内存占用率。 对于网络包,设备访问或者io资源的控制,cgroups 同样提供了之前所无法完成的精细化控制。
  结束语
  本文首先介绍了 cgroups 在内核中的实现方式,然后介绍了 cgroups 如何通过 VFS 把相关的功能暴露给用户,然后简单介绍了 cgroups 的使用方法,后通过分析了几个 cgroups 在实践中的例子,进一步展示了 cgroups 的强大的精细化控制能力。
  笔者希望通过整篇文章的介绍,读者能够了解到 cgroups 能够完成什么样的功能,并且希望读者在使用 cgroups 的功能的时候,能够大体知道内核通过一种什么样的方式来实现这种功能。