Redian新闻
>
图解|cgroup 设计分析(Docker底层技术)

图解|cgroup 设计分析(Docker底层技术)

科技

cgroup 可能很多人都不了解,但提起 Docker 估计每个后端程序员都了解过。是的,Docker 已经成为程序员必须掌握的技术之一了。Docker 主要解决了传统虚拟机启动慢、占用大量资源的缺点。

当然,本文的重点并不是 Docker,而是 Docker 的底层支撑技术 cgroup。可以这样说,没有 cgroup 就没有 Docker

什么是 cgroup

cgroup 的全称为 control group,中文翻译为 控制组。主要用于控制进程组对某种资源的使用,这些资源包括但不限于:内存CPUI/O 和 网络 等。

如下图所示,使用 cgroup 来限制进程组对内存的使用:

(图1)

在上图中,我们创建了 2 个 cgroup(每个 cgroup 有 4 个进程),并且限制它们各自最多只能使用 2GB 的内存。如果使用超过 2GB 的内存,那么将会触发 OOM(Out Of Memory) 错误。

cgroup 通过把进程划分成控制组(一个控制组包含一个或多个进程),并且可以对控制组进行资源使用的控制,也就是说 cgroup 作用对象是控制组。

cgroup 提供了将进程组织成控制组的能力,然后通过使用 资源控制子系统(cgroup_subsys) 来对控制组进行资源使用的控制,cgroup 支持的 资源控制子系统 有以下几种:

  • cpu子系统:限制 CPU 的使用。
  • memory子系统:限制内存使用。
  • cpuset子系统:可以为进程组分配单独的 CPU 或者内存节点。
  • cpuacct子系统:统计CPU group的使用情况。
  • blkio子系统:限制I/O,一般用于磁盘。
  • devices子系统:限制进程使用的设备。
  • freezer子系统:可以挂起和恢复进程组。
  • net_cls子系统:可以标记进程组的网络数据包,使用 tc 模块(traffic control)对数据包进行控制。

也就是说,cgroup 通过把进程组织成 控制组,然后通过 资源控制子系统 来对 控制组 进行资源使用的限制,所以 cgroup 的分析可以分成两部分:cgroup框架 和 资源控制子系统

cgroup 源码分析

cgroup 的设计还是比较复杂的,主要是因为 cgroup 涉及多种资源的控制,并且 cgroup 通过虚拟文件系统来组织进程控制组,所以导致 cgroup 的实现变得复杂难懂。

cgroup 的概念和使用可以参考这篇文章:《cgroup介绍》。

为了不会让大家陷入枯燥的概念和源码之中,本文主要通过以设计者的角度来分析 cgroup 的设计与实现。

OK,Let's go!

1. 设计一个简单的 cgroup

如果让你来设计一个限制进程组对内存使用的方案,你会怎么设计呢?

最简单的方法就是,创建一个内存使用的计数器,然后将进程组中所有的进程都指向这个计数器。当进程组的进程申请内存时,就增加计数器的值,如果计数器超过限制就触发错误。如下图所示:

(图2)

上图中计数器的 limit 字段表示限制进程中使用的最大内存数,而 count 字段表示当前进程使用的内存数。每当进程组中的进程申请内存时,都需要增加计数器的 count 字段,并且比较 count 是否已经超出 limit 的限制。

数据结构可以这样设计:

(图3)

我们通过链表来将进程组织成进程组,并且在计数器中增加一个 task_group 字段,让其指向进程组。当进程组中的进程申请内存时,可以通过指针来找到对应的计数器,并且增加计数器的 count 字段。

就这样,我们设计了一个简单的 cgroup 功能。如果系统只有内存这种资源的话,的确可以这样设计。但是系统除了内存,还有CPU、硬盘和网络这些资源,所以 Linux 创建了一种比较通用的方式来组织进程组。

2. 控制组

有了上面的雏形,cgroup 的很多概念就比较容易理解了,下面主要介绍一下 控制组 这个概念。

控制组 说白了就是一组进程(进程组),cgroup 就是用来限制 控制组 的资源使用。为了能够方便地向一个 控制组 添加或者移除进程(在命令行也能操作),内核使用了 虚拟文件系统 来进行管理 控制组

我们可以把一个 控制组 当成是一个目录,由于目录有层级关系,所以 控制组 也有层级关系,如下图所示:

(图4)

如上图所示,控制组 是以目录树来组织的,每一个目录代表一个 控制组。在内核中,一个由 控制组 组成的目录树被称为 层级(hierarchy)

每个控制组目录中,都有一个名为 tasks 的文件,用于保存当前 控制组 包含的进程列表。如果我们想向某个 控制组 添加一个进程时,可以把进程的 PID 写入到 tasks 文件中。例如:

$ cd /sys/fs/cgroup/memory/cgrp1    # 进入控制组cgrp1
$ echo 1029 > tasks                 # 将PID为1029的进程添加到cgrp1控制组中

我们也可以通过读取 tasks 文件来查看某个 控制组 中的进程列表,例如:

$ cat tasks
1
2
3
5
6
7
...

在内核中,控制组使用 cgroup 结构来表示,其定义如下:

struct cgroup {
    ...
    // 下面3个字段把控制组连接成一个树结构
    struct list_head sibling;   // 兄弟节点
    struct list_head children;  // 子节点
    struct cgroup *parent;      // 父节点

    struct dentry *dentry;      // 当前控制组对应的目录对象

    // 当前控制组关联的子系统资源统计对象
    struct cgroup_subsys_state *subsys[CGROUP_SUBSYS_COUNT]; 
    ...
};

内核通过 cgroup 结构的 siblingchildren 和 parent 这3个字段来将 控制组 组织成一棵树状结构。如下图所示:

(图5)

另外,cgroup 结构的 subsys 字段表示当前控制组关联的子系统状态对象,下面介绍 资源控制子系统 时将会详细介绍。

在 Linux 内核中,可以存在多个 层级(控制组树),每个层级可以关联一个或多个 资源控制子系统,但同一个 资源控制子系统 不能关联到多个层级中(也就是说,同一种 资源控制子系统 只能关联到一个层级)。如下图所示:

(图6)

在内核中,层级 的根结点使用 cgroupfs_root 结构来表示, 我们来看看其定义:

struct cgroupfs_root {
    struct super_block *sb;            // 挂载点超级块对象(虚拟文件系统使用)
    unsigned long subsys_bits;         // 当前层级绑定的资源子系统位图(1表示已经绑定到当前层级)
    ...
    struct list_head subsys_list;      // 绑定到当前层级的资源子系统列表
    struct cgroup top_cgroup;          // 当前层级的根控制组
    int number_of_cgroups;             // 当前层级拥有的控制组数量
    ...
};

在 Linux 内核中,有个名为 rootnode 的根层级,在系统启动后,由内核自动创建并且初始化的层级。系统启动后,所有的资源控制子系统都关联到此层级。rootnode 的定义如下:

// 定义在文件 ./kernel/cgroup.c 中

static struct cgroupfs_root rootnode;

如果用户想把资源控制子系统关联到其他层级,那么可以使用 mount 命令来进行挂载,如下命令所示:

$ mount -t cgroup -o memory memory /sys/fs/cgroup/memory

上面的命令用于将内存子系统重新关联到 /sys/fs/cgroup/memory 这个层级。

3. 资源控制子系统

我们继续来介绍 资源控制子系统 (下面简称子系统) 这个重要的概念。

在 设计一个简单的 cgroup 例子中,主要以内存资源作为分析对象。但我们知道,计算机不单止只有内存资源,还有譬如 CPU、硬盘和网络等资源。所以,cgroup 不单止要控制内存资源的使用,还要控制 CPU、硬盘和网络等资源的使用。如下图所示:

(图7)

在上面的实例中,我们使用一个计数器来统计进程组对内存资源的使用情况,每个 控制组 都需要一个这样的计数器来统计和限制进程组对内存资源的使用。

在 Linux 内核中也有类似的 “计数器“,使用 cgroup_subsys_state 结构来表示(我们称它为 资源统计对象),其定义如下:

struct cgroup_subsys_state {
    struct cgroup *cgroup; // 指向控制组对象
    atomic_t refcnt;       // 引用计数器
    unsigned long flags;   // 标志位
};

cgroup_subsys_state 结构看起来非常简单,这只是表面现象。内核为了将所有的 资源统计对象 抽象化(也就是都能用 cgroup_subsys_state 指针来指向所有类型的 资源统计对象),才定义出这个通用的部分,实际上的 资源统计对象 是比较复杂的。

例如内存的 资源统计对象 定义如下:

struct mem_cgroup {
    // 资源统计对象通用部分
    struct cgroup_subsys_state css;

    // 资源统计对象私有部分
    struct res_counter res;  // 用于统计进程组的内存使用情况
    struct mem_cgroup_lru_info info;
    int prev_priority;
    struct mem_cgroup_stat stat;
};

mem_cgroup 结构与 cgroup_subsys_state 结构的关系如下图所示:

(图8)

资源统计对象 必须与 控制组 绑定,才能实现限制 控制组 对资源的使用。前面我们了解到 cgroup 结构中有个名为 subsys 的字段,如下代码所示:

struct cgroup {
    ...
    // 当前控制组关联的资源统计对象
    struct cgroup_subsys_state *subsys[CGROUP_SUBSYS_COUNT]; 
    ...
};

可以看出,subsys 字段是一个 cgroup_subsys_state 结构的数组,数组的大小为系统支持的 资源控制子系统 数(也就是说,数组上的每个槽位对应着一个子系统资统计对象)。如下图所示:

(图9)

在 Linux 内核中,一个进程可以属于多个 控制组,而每个 控制组 又关联着一个或多个 资源统计对象。所以,一个进程所关联的 资源统计对象 是其所在 控制组 关联的 资源统计对象 的集合。这句话有点难懂,我们用一幅图来说明:

(图10)

如上图所示:

  • 进程A 属于控制组 /sys/fs/cgroup/memory/cgrp1/cgrp3 和控制组 /sys/fs/cgroup/cpu/cgrp2/cgrp3,所以 进程A 就关联了 mem_group A 和 task_group A 这两个资源统计对象。

  • 进程B 属于控制组 /sys/fs/cgroup/memory/cgrp1/cgrp4 和控制组 /sys/fs/cgroup/cpu/cgrp2/cgrp3,所以 进程B 就关联了 mem_group B 和 task_group A 这两个资源统计对象。

进程通过 css_set 结构来收集不同控制组的 资源统计对象,其定义如下:

struct css_set {
    ...
    // 用于收集不同控制组的资源统计对象
    struct cgroup_subsys_state *subsys[CGROUP_SUBSYS_COUNT];
};

在 进程描述符结构(task_struct) 中有个指向 css_set 结构的指针,如下所示:

struct task_struct {
    ...
    struct css_set *cgroups;
    ...
};

所以,当把一个进程添加到一个 控制组 时,将会把 控制组 关联的 资源统计对象 添加到进程的 cgroups 字段中,从而使进程受到这些 资源统计对象 的限制,结合图10就比较容易理解了。

另外,资源子系统必须关联到某个层级才能起到限制 控制组 使用的目的。每种资源子系统都由一个名为 cgroup_subsys 的结构来描述,其定义如下:

struct cgroup_subsys {
    struct cgroup_subsys_state *(*create)(struct cgroup_subsys *ss
                                          struct cgroup *cgrp);

    ...
    void (*attach)(struct cgroup_subsys *ss, struct cgroup *cgrp,
                   struct cgroup *old_cgrp, struct task_struct *tsk);
    void (*fork)(struct cgroup_subsys *ss, struct task_struct *task);
    void (*exit)(struct cgroup_subsys *ss, struct task_struct *task);
    ...
    int subsys_id;
    int active;
    int disabled;
    int early_init;
    const char *name;            // 子系统名字
    struct cgroupfs_root *root;  // 关联的层级根节点
    struct list_head sibling;
    void *private;
};

从 cgroup_subsys 结构的定义可以看出,其主要定义了一些方法和关联的层级。比如:create 方法主要用于当新建一个 控制组 时,创建一个新的 资源统计对象 与其关联;而 root 字段指向关联的层级根节点。

如内存子系统的定义如下:

// 定义在文件:./mm/memcontrol.c

struct cgroup_subsys mem_cgroup_subsys = {
    .name        = "memory",
    .subsys_id   = mem_cgroup_subsys_id,
    .create      = mem_cgroup_create,
    .pre_destroy = mem_cgroup_pre_destroy,
    .destroy     = mem_cgroup_destroy,
    .populate    = mem_cgroup_populate,
    .attach      = mem_cgroup_move_task,
    .early_init  = 0,
};

总结

本文主要分析了 cgroup 的设计与源码实现,不过聪明的读者可能发现本文并没有分析 cgroup 的逻辑代码。

是的,本文并没有分析具体的逻辑代码实现。不过按照本文的设计分析,相信读者能够很容易看到 cgroup 的逻辑代码实现,有兴趣的读者可以自行阅读源代码。

微信扫码关注该文公众号作者

戳这里提交新闻线索和高质量文章给我们。
相关阅读
精选Data岗位 | Expedia Group、EA、Zscaler等公司发布海量岗位!揭秘XR底层供应链系列研讨会之光学材料专场闭门分享会圆满结束元气可乐开启预售;Tims推中式新品;Groupon&Warby Parker裁员... | 刀法品牌热讯《2022 元宇宙行业人才发展报告》出炉:C 语言、Java 等底层技术是硬通货,相关岗位平均月薪 1.8 万元Docker 容器网络及其配置详解Docker 之父:Go、Rust 为什么会成为云原生的主导语言?Choice 集团收购 Radisson Hotel Group AmericasShanghai Rental Market Sees ‘Abrupt’ Post-Lockdown Growth陌上花开875 Be Your Mirror, Your Sword, Your Shield, and Your Love南街知名餐馆Jim's Steaks被烧毁! 费城社区大学开设卡车司机培训班!廉价超市Grocery Outlet在北费城开新店短波笔记(13)-川普、国会听证会如何使用 Dockerfile 创建自定义 Docker 镜像 | Linux 中国Shanghai’s Foreign Companies Strive to Bounce Back Post LockdownLinux 下的 Docker 入门教程 | Linux 中国HTC推出Vive Flow商用版本;Khronos Group成立新组织以统一元宇宙标准,微软、高通、索尼等公司加入仅30多万起!新西敏First & Roya,限量版低价上车盘,车位Locker全包!我如何从 Docker Desktop 切换到 Colima | Linux 中国如何在 Ubuntu 22.04 LTS 中安装 Docker 和 Docker Compose | Linux 中国PPIO边缘云聚焦音视频底层技术,探索元宇宙“登月工程”特别有才的人往往是在为人处世方面有些缺陷的,比如陈景润,那绝对在现在拿不到TENURE。。盲人摸象?单一统计分析难以回避的脆弱性学做Tapas中的一道素菜:芦笋蘑菇总选再次破亿,SNH48 GROUP如何持续吸金十年?Update:恭喜老查留学5位同学成功入围John Locke Shortlist名单!平淡忙碌中寻找快乐的我(企业分析)如何下手开始企业分析?--- 从底层逻辑向上做企业分析(第一课)Cities Offer Huge ‘Group Buying’ Discounts on Unsold Properties图解 Rocky Linux 9 安装步骤 | Linux 中国Shanghai’s 30-Year-Old Bakery Struggles to Beat Lockdown Woes说三道四(109) 见贤思齐 取法乎上Boast About Big Bucks Busts Brokers’ BankrollsOnce a Lifeline, Group-Buying in China Gasps for SurvivalVideo of Women Brutally Attacked in Restaurant Shocks China【闭门会议】9月29日——揭秘XR底层供应链系列研讨会之Pancake光学技术专场从ICML 2022看域泛化(Domain Generalization)最新进展
logo
联系我们隐私协议©2024 redian.news
Redian新闻
Redian.news刊载任何文章,不代表同意其说法或描述,仅为提供更多信息,也不构成任何建议。文章信息的合法性及真实性由其作者负责,与Redian.news及其运营公司无关。欢迎投稿,如发现稿件侵权,或作者不愿在本网发表文章,请版权拥有者通知本网处理。