Skip to content

Latest commit

 

History

History
165 lines (121 loc) · 12.3 KB

cgroups的原理与应用.md

File metadata and controls

165 lines (121 loc) · 12.3 KB

疫情以来一直是居家办公,确实是很久都没有好好研究东西了,cgroups作为容器侧的两大基石本该是很早以前就被研究的东西,但是因为种种原因吧就一直拖延到了22年才加入到笔记里面

CgroupsContainer

在现在看来,CgroupsContainer的基石之一,但是在2007年那个容器技术还不是很成熟的时代Cgroup却就是Container本身,因为其最初被设计出来的用法就是被用作容器化进程的,甚至是连名字都是Process containers

管中窥豹,可见一斑

可以从最初的Porcess container上来探究未来的Cgroups的设计模式,在内核中引入了一些新的概念,其中比较重要的一个叫做子系统(subsystem),比如内核中原本存在的cpusets这种用于绑定进程的机制就被变成了一种子系统,而其余的一些子系统也都是类似的原本关注于资源管理的一些机制,然后还有一个container的概念代表的是一组使用了相同子系统配置的进程,再就是container是分层的且配置是可继承的,因为它用了一个group的形式来管理进程所以在最初的patch提交的时候设计者还在其中探讨了是否需要把这个系统重新命名一下比如ProcessSets? ResourceGroups? TaskGroups?

simple hierarchy

[容器层次结构]

在如上的图中,GuestsSys tasks就是两个容器,用到了不同的设置的进程会运行在其中,倘若有些Guests下的一些进程想要更进一步地进行配置,那就会在Guests的基础上再创建出具有特定策略的新容器出来,例如G1,G2,G3

伴随着子系统container中应用相应的还有随之配套的一些基础规则产生:

  1. 一个层级结构可以关联上一个或者多个子系统
  2. 一个子系统只能关联到一个层结构上
  3. 在有多个层次结构时进程就会同时处于多个容器中,至少每个层级结构中有一个 ,但是相同的层级中一个进程只能出现在一个容器里

在设计上container的使用并不是通过提供内核ABI或是Library的形式方便代码直接调用,而是选用了VFS来作为用户接口,用户在挂载对应的文件系统后通过对文件的操作来实现container的创建,这种做法极大的降低了操作的难度并且使得配置可视化方便了使用者

那么重新以现在的视角来审视最初的设计模式,Process container的容器概念不像是如今的container而更像是k8spod的概念,其在设计上注重的就并非是隔离(isloation)而是资源管理(resource management),原本的container变成了现在的cgroup,不同的cgroup有着不同的子系统配置并且其中跑着task,而这些cgroup组成了树状结构就是hierarchy,而一个系统中可以有多个hierarchy,这些就是Cgroups整个概念的抽象表现

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

这是现代操作系统中cgroups v1的一个最典型的使用,从上述的挂载信息中就可以看出来系统在启动的时候就初始化了11个hierarchy并且分别关联了不同的子系统,这样当你需要去针对不同的资源进行控制的时候就只需要到对应的目录中创建新的cgroup,实际也就是一个目录然后再把指定的进程放进去就可以了

从进程中去看

当然上面的都是从用户态去观察cgroup,然而实质运用上来说最小的单位依旧是一个进程也就是一个task,那么在内核中是怎么知道一个task被怎么限制的资源呢?像cgroups这种内核中的运用非常广泛的大机制一般都会有一个专门的结构体来负责然后再作为进程task_struct的一个基础成员

struct task_struct {
······
#ifdef CONFIG_CGROUPS
    /* Control Group info protected by css_set_lock */
    struct css_set __rcu *cgroups;
    /* cg_list protected by css_set_lock and tsk->alloc_lock */
    struct list_head cg_list;
#endif
······
}

一般一个进程没有经过特殊处理的话默认都会处于系统的初始cgroups之中

crash> task -R cgroups 3555
PID: 3555   TASK: ffff880207fb4380  CPU: 1   COMMAND: "zsh"
  cgroups = 0xffff880223a0ce00

cgroups由内核初始化时候创建

/**
 * cgroup_init - cgroup initialization
 *
 * Register cgroup filesystem and /proc file, and initialize
 * any subsystems that didn't request early init.
 */
int __init cgroup_init(void)

该函数会初始化填充subsys整个数组,为了便于管理一般来说初始化的cgroups都是和全部的子系统都有关联

crash> css_set.subsys 0xffff880223a0ce00
  subsys = {
                    0xffffffff81c7f640 <top_cpuset>, 
                    0xffffffff81ff4d40 <root_task_group>, 
                    0xffffffff81c535a0 <root_cpuacct>, 
                    0xffffffff822472a0 <blkcg_root>, 
                    0xffff88017fc1c000, 
                    0xffff88023128e000, 
                    0xffff88017fc3fbc0, 
                    0xffff88017fc3fc80, 
                    0xffff88017fc3fd40, 
                    0xffff88017fc0d000, 
                    0xffff88023128c900
}

如果把进程加入到某个新的cgroup当中,那么进程的cgroups就会变掉

~ /sys/fs/cgroup/devices/test  sudo sh -c 'echo 3555 > cgroup.procs'


crash> task -R cgroups 3555
PID: 3555   TASK: ffff880207fb4380  CPU: 3   COMMAND: "zsh"
  cgroups = 0xffff880207f09600, 


crash> css_set.subsys 0xffff880207f09600
  subsys = {0xffff880033380800, 0xffffffff81ff4d40 <root_task_group>, 0xffffffff81c535a0 <root_cpuacct>, 0xffffffff822472a0 <blkcg_root>, 0xffff88017fc1c000, 0xffff8800bb333600, 0xffff88017fc3fbc0, 0xffff88017fc3fc80, 0xffff88017fc3fd40, 0xffff88017fc0d000, 0xffff88023128c900}

而后这些限制如何被内核所落实那就是组调度来负责实现的,这是另一个知识点暂且按下不表

v1 VS v2

Kernel 4.5以后Cgroups v2被正式加入到了内核,它与v1相比有了极大的变化,最典型的一个就是针对hierarchy的简化,v1为了灵活性不同的子系统分成了不同的hierarchy,需要控制进程的某个资源就到相应的hierarchy中创建子节点然后将进程添加进去,这是没有任何限制的因此当控制需求逐渐增多就会变得愈发混乱,第二点就是v1允许了线程划分到不同的cgroup中,但是这些线程都是共享的相同的进程资源,这使得memory的资源控制变得没有意义

v2的改进:

  1. Cgroups v2 中所有的 controller 都会被挂载到一个 unified hierarchy 下,不在存在像 v1 中允许不同的 controller 挂载到不同的 hierarchy 的情况
  2. Proess 只能绑定到 cgroup 的根(“/“)目录和 cgroup 目录树中的叶子节点
  3. 通过 cgroup.controllers 和 cgroup.subtree_control 指定哪些 controller 可以被使用
  4. v1 版本中的 task 文件和 cpuset controller 中的 cgroup.clone_children 文件被移除
  5. 当 cgroup 为空时的通知机制得到改进,通过 cgroup.events 文件通知

安全问题

cgroups上出现的安全问题基本都围绕着如下两个方面:

  1. devices
  2. release_agent

前者主要用于控制task能够访问到的设备资源,而后者则是cgroups本身的一个清理机制,先说说前者

  • v1之中存在一个devices.allow,其中决定了task能够访问到设备,在容器这种隔离的环境下,如果该文件被重写使得容器中的进程能够访问到整个主机磁盘设备的话,就可以通过新建设备文件然后重挂载或者debugfs的方式进行磁盘读写造成容器逃逸

该技术只需要解决以下几个难点即可:

  1. devices.allow的内容格式是type major:minor access只需要写入a *:* rwm即可,但是mknod是需要知道明确的major:minor的,这点在真实环境中需要自己去找
  2. 在正常情况下cgroup的目录都是只读挂载的,因此需要有写入权限才能被利用

但是很可惜的是,到了v2当中device controllers被移除了,在·unified hierarchy·中不再提供接口文件而是在cgroup bpf基础上实现访问控制,管理者需要通过编写BPF_PROG_TYPE_CGROUP_DEVICE类型的BPF程序然后attach到指定的cgroup且类型指定为BPF_CGROUP_DEVICE,进程访问设备触发BPF程序来决定是否允许访问,这种方式已经使用于runc当中了

再说说release_agent,这本身是cgroup的一个清理机制,主要用途就是当cgroup中不再有进程的时候就可以触发执行用于清除整个cgroup目录或是做其余一些收尾的事情,当然如果想要触发的话就需要在notify_on_release中设置为1,但是这个机制很容易被滥用,因为其底层原始实际上是在内核中调用了call_usermodehelper()也就是在内核态执行用户态的程序权限是十分高的,当release_agent的内容可以随意修改的话就很容易遭到利用

在早期的版本中release_agent的写入检查就仅仅是root即可而没有正确检查写入进程是否具有CAP_SYS_ADMIN权限,直接导致了当cgroup被以读写形式挂载到容器当中时可以轻而易举的形成逃逸,当然这个问题在CVE-2022-0492被提交以后就被修补了,但是release_agent依然还是在提权思路中一个经久不衰的tips,但是很可惜的是v2中因为通信机制的问题导致该文件被移除取而代之的是cgroup.events文件,而收尾清理的工作则需要管理者主动去监控该文件来实现

结语

今年因为种种特殊原因很久没有接触技术类的东西了,导致刚开始几天看起文章和资料来相当的乏力,因此这篇关于cgroups的内容还是存在着种种的不足,比如调度问题上都被一带而过,比如v2上其实都没有做任何的实际阐述,更比如安全性上仅仅是举了cgroups两个人尽皆知的例子而没有加入任何自己的思考,这种归纳状态无疑是不正确的

牢骚无需多言,如果有什么需要探讨的东西,可以随时联系我 : )

参考资料