《重识云原生系列》专题索引:
- 第一章——不谋全局不足以谋一域
- 第二章计算第1节——计算虚拟化技术总述
- 第二章计算第2节——主流虚拟化技术之VMare ESXi
- 第二章计算第3节——主流虚拟化技术之Xen
- 第二章计算第4节——主流虚拟化技术之KVM
- 第二章计算第5节——商用云主机方案
- 第二章计算第6节——裸金属方案
- 第三章云存储第1节——分布式云存储总述
- 第三章云存储第2节——SPDK方案综述
- 第三章云存储第3节——Ceph统一存储方案
- 第三章云存储第4节——OpenStack Swift 对象存储方案
- 第三章云存储第5节——商用分布式云存储方案
- 第四章云网络第一节——云网络技术发展简述
- 第四章云网络4.2节——相关基础知识准备
- 第四章云网络4.3节——重要网络协议
- 第四章云网络4.3.1节——路由技术简述
- 第四章云网络4.3.2节——VLAN技术
- 第四章云网络4.3.3节——RIP协议
- 第四章云网络4.3.4节——OSPF协议
- 第四章云网络4.3.5节——EIGRP协议
- 第四章云网络4.3.6节——IS-IS协议
- 第四章云网络4.3.7节——BGP协议
- 第四章云网络4.3.7.2节——BGP协议概述
- 第四章云网络4.3.7.3节——BGP协议实现原理
- 第四章云网络4.3.7.4节——高级特性
- 第四章云网络4.3.7.5节——实操
- 第四章云网络4.3.7.6节——MP-BGP协议
- 第四章云网络4.3.8节——策略路由
- 第四章云网络4.3.9节——Graceful Restart(平滑重启)技术
- 第四章云网络4.3.10节——VXLAN技术
- 第四章云网络4.3.10.2节——VXLAN Overlay网络方案设计
- 第四章云网络4.3.10.3节——VXLAN隧道机制
- 第四章云网络4.3.10.4节——VXLAN报文转发过程
- 第四章云网络4.3.10.5节——VXlan组网架构
- 第四章云网络4.3.10.6节——VXLAN应用部署方案
- 第四章云网络4.4节——Spine-Leaf网络架构
- 第四章云网络4.5节——大二层网络
- 第四章云网络4.6节——Underlay 和 Overlay概念
- 第四章云网络4.7.1节——网络虚拟化与卸载加速技术的演进简述
- 第四章云网络4.7.2节——virtio网络半虚拟化简介
- 第四章云网络4.7.3节——Vhost-net方案
- 第四章云网络4.7.4节vhost-user方案——virtio的DPDK卸载方案
- 第四章云网络4.7.5节vDPA方案——virtio的半硬件虚拟化实现
- 第四章云网络4.7.6节——virtio-blk存储虚拟化方案
- 第四章云网络4.7.8节——SR-IOV方案
- 第四章云网络4.7.9节——NFV
- 第四章云网络4.8.1节——SDN总述
- 第四章云网络4.8.2.1节——OpenFlow概述
- 第四章云网络4.8.2.2节——OpenFlow协议详解
- 第四章云网络4.8.2.3节——OpenFlow运行机制
- 第四章云网络4.8.3.1节——Open vSwitch简介
- 第四章云网络4.8.3.2节——Open vSwitch工作原理详解
- 第四章云网络4.8.4节——OpenStack与SDN的集成
- 第四章云网络4.8.5节——OpenDayLight
- 第四章云网络4.8.6节——Dragonflow
1 cgroups数据结构解析
从进程的角度出发来剖析 cgroups 相关数据结构之间的关系。在 Linux 中管理进程的数据结构是 task_struct。cgroup表示进程的行为控制,因为子系统必须要知道进程是位于哪一个cgroup,所以在struct task_struct和cgroup中存在一种映射。
1.1 task_struct和cgroup的映射链路
1.1.1 task_struct 结构体
struct task_struct {
……
……
#ifdef CONFIG_CGROUPS
/* Control Group info protected by css_set_lock */
struct css_set cgroups; / cg_list protected by css_set_lock and tsk->alloc_lock */
struct list_head cg_list;
#endif
……
……
}
cgroups 指针指向了一个 css_set 结构,而css_set 存储了与进程有关的 cgroups 信息。cg_list 是一个嵌入的 list_head 结构,用于将连到同一个 css_set 的进程组织成一个链表。
1.1.2 css_set结构体
struct css_set {
atomic_t refcount;
struct hlist_node hlist;
struct list_head tasks;
struct list_head cg_links;
struct cgroup_subsys_state *subsys[CGROUP_SUBSYS_COUNT];
struct rcu_head rcu_head;
};
- refcount 是该 css_set 的引用数,因为一个 css_set 可以被多个进程公用,只要这些进程的 cgroups 信息相同,比如:在所有已创建的层级里面都在同一个 cgroup 里的进程;
- hlist 是嵌入的 hlist_node,用于把所有 css_set 组织成一个 hash 表,这样内核可以快速查找特定的 css_set;
- tasks 指向所有连到此 css_set 的进程连成的链表;
- cg_links 指向一个由 struct_cg_cgroup_link 连成的链表;
- Subsys 是一个指针数组,存储一组指向 cgroup_subsys_state 的指针;一个cgroup_subsys_state 就是进程与一个特定子系统相关的信息;通过这个指针数组,进程就可以获得相应的 cgroups 控制信息了。
1.1.3 cgroup_subsys_state 代码
struct cgroup_subsys_state {
struct cgroup *cgroup;
atomic_t refcnt;
unsigned long flags;
struct css_id *id;
};
cgroup 指针指向了一个 cgroup 结构,也就是进程属于的 cgroup。进程受到子系统的控制,实际上是通过加入到特定的 cgroup 实现的,因为 cgroup 在特定的层级上,而子系统又是附和到上面的。通过以上三个结构,进程就可以和 cgroup 连接起来了:task_struct->css_set->cgroup_subsys_state->cgroup。
1.1.4 cg_cgroup_link结构体
struct cg_cgroup_link {
struct list_head cgrp_link_list;
struct cgroup *cgrp;
struct list_head cg_link_list;
struct css_set *cg;
};
- cgrp_link_list 连入到 cgrouo->css_set 指向的链表,cgrp 指向此 cg_cgroup_link 相关的 cgroup。
- cg_link_list 连入到 css_set->cg_links 指向的链表,cg 指向此 cg_cgroup_link 相关的 css_set。
- cgroup 和 css_set 是多对多的关系,须添加一个中间结构来将两者联系起来,这就是 cg_cgroup_link 的作
- cg_cgroup_link 中的 cgrp 和 cg 就是此结构提的联合主键,而 cgrp_link_list 和 cg_link_list 分别连入到 cgroup 和 css_set 相应的链表,使得能从 cgroup 或 css_set 都可以进行遍历查询。
1.2 cgroup相关数据结构剖析
前面介绍过, cgroup 是用来控制进程组对各种资源的使用,而在内核中, cgroup 是通过 cgroup 结构体来描述的,我们来看看其定义。
1.2.1 cgroup结构体
struct cgroup{
unsigned long flags; /* "unsigned long" so bitops work */
atomic_t count;
struct list_head sibling; /* my parent's children */
struct list_head children; /* my children */
struct cgroup* parent; /* my parent */
struct dentry* dentry; /* cgroup fs entry */
struct cgroup_subsys_state* subsys[ CGROUP_SUBSYS_COUNT];
struct cgroupfs_root* root;
struct cgroup* top_cgroup;
struct list_head css_sets;
struct list_head release_list;
};
下面我们来介绍一下 cgroup 结构体各个字段的用途:
- flags : 用于标识当前 cgroup 的状态。
- count : 引用计数器,表示有多少个进程在使用这个 cgroup 。
- sibling、children、parent : 由于 cgroup 是通过 层级 来进行管理的,这三个字段就把同一个 层级 的所有 cgroup 连接成一棵树。 parent 指向当前 cgroup 的父节点, sibling 连接着所有兄弟节点,而 children 连接着当前 cgroup 的所有子节点。
- dentry : 由于 cgroup 是通过 虚拟文件系统 来进行管理的,在介绍 cgroup 使用时说过,可以把 cgroup 当成是 层级 中的一个目录,所以 dentry 字段就是用来描述这个目录的。
- subsys : 前面说过, 子系统 能够附加到 层级 ,而附加到 层级 的 子系统 都有其限制进程组使用资源的算法和统计数据。所以 subsys 字段就是提供给各个 子系统 存放其限制进程组使用资源的统计数据。我们可以看到 subsys 字段是一个数组,而数组中的每一个元素都代表了一个 子系统 相关的统计数据。从实现来看, cgroup 只是把多个进程组织成控制进程组,而真正限制资源使用的是各个 子系统 。
- root : 用于保存 层级 的一些数据,比如: 层级 的根节点,附加到 层级 的 子系统 列表(因为一个 层级 可以附加多个 子系统 ),还有这个 层级 有多少个 cgroup 节点等。
- top_cgroup : 层级 的根节点(根cgroup)。
我们通过下面图片来描述 层级 中各个 cgroup 组成的树状关系:
cgroup链表结构示意图
1.2.2 cgroup_subsys_state 结构体
每个子系统 都有属于自己的资源控制统计信息结构,而且每个 cgroup 都绑定一个这样的结构,这种资源控制统计信息结构就是通过 cgroup_subsys_state 结构体实现的,其定义如下:
struct cgroup_subsys_state{
struct cgroup* cgroup;
atomic_t refcnt;
unsigned long flags;
};
下面介绍一下 cgroup_subsys_state 结构各个字段的作用:
- cgroup : 指向了这个资源控制统计信息所属的 cgroup 。
- refcnt : 引用计数器。
- flags : 标志位,如果这个资源控制统计信息所属的 cgroup 是 层级 的根节点,那么就会将这个标志位设置为 CSS_ROOT 表示属于根节点。
1.2.3 mem_cgroup 结构体
从 cgroup_subsys_state 结构的定义看不到各个子系统相关的资源控制统计信息,这是因为 cgroup_subsys_state 结构并不是真实的资源控制统计信息结构,比如内存子系统真正的资源控制统计信息结构是mem_cgroup ,那么怎样通过这个cgroup_subsys_state 结构去找到对应的 mem_cgroup 结构呢?我们来看看 mem_cgroup 结构的定义:
struct mem_cgroup{
struct cgroup_subsys_statecss; // 注意这里
struct res_counterres;
struct mem_cgroup_lru_infoinfo;
int prev_priority;
struct mem_cgroup_statstat;
};
从 mem_cgroup 结构的定义可以发现, mem_cgroup 结构的第一个字段就是一个 cgroup_subsys_state 结构。下面的图片展示了他们之间的关系:
cgroup-state-memory结构示意图
从上图可以看出, mem_cgroup 结构包含了 cgroup_subsys_state 结构, 内存子系统 对外暴露出 mem_cgroup 结构的 cgroup_subsys_state 部分(即返回 cgroup_subsys_state 结构的指针),而其余部分由 内存子系统 自己维护和使用。
由于 cgroup_subsys_state 部分在 mem_cgroup 结构的首部,所以要将 cgroup_subsys_state 结构转换成 mem_cgroup 结构,只需要通过指针类型转换即可。
cgroup 结构与 cgroup_subsys_state 结构之间的关系如下图:
cgroup-subsys-state 结构示意图
1.2.4 css_set 结构体
由于一个进程可以同时添加到不同的 cgroup 中(前提是这些 cgroup 属于不同的层级 )进行资源控制,而这些 cgroup 附加了不同的资源控制子系统 。所以需要使用一个结构把这些子系统的资源控制统计信息收集起来,方便进程通过子系统ID快速查找到对应的子系统资源控制统计信息,而 css_set 结构体就是用来做这件事情。 css_set 结构体定义如下:
struct css_set{
struct krefref;
struct list_headlist;
struct list_headtasks;
struct list_headcg_links;
struct cgroup_subsys_state* subsys[ CGROUP_SUBSYS_COUNT];
};
下面介绍一下 css_set 结构体各个字段的作用:
- ref : 引用计数器,用于计算有多少个进程在使用此 css_set 。
- list : 用于连接所有 css_set 。
- tasks : 由于可能存在多个进程同时受到相同的 cgroup 控制,所以用此字段把所有使用此 css_set 的进程连接起来。
- subsys : 用于收集各种 子系统 的统计信息结构。
进程描述符 task_struct 有两个字段与此相关,如下:
struct task_struct{
...
struct css_set* cgroups;
struct list_head cg_list;
...
}
可以看出,task_struct 结构的 cgroups 字段就是指向 css_set 结构的指针,而 cg_list 字段用于连接所有使用此 css_set 结构的进程列表。
task_struct 结构与 css_set 结构的关系如下图:
cgroup-task与css-set关系示意图
1.2.4 cgroup_subsys 结构体
cgroup_subsys 定义了一组操作,让各个子系统根据各自的需要去实现。这个相当于 C++中抽象基类,然后各个特定的子系统对应 cgroup_subsys 则是实现了相应操作的子类。类似的思想还被用在了 cgroup_subsys_state 中,cgroup_subsys_state 并未定义控制信息,而只是定义了各个子系统都需要的共同信息,比如该 cgroup_subsys_state 从属的 cgroup。然后各个子系统再根据各自的需要去定义自己的进程控制信息结构体,最后在各自的结构体中将 cgroup_subsys_state 包含进去,这样通过 Linux 内核的 container_of 等宏就可以通过 cgroup_subsys_state 来获取相应的结构体。
struct cgroup_subsys{
struct cgroup_subsys_state*(* create)( structcgroup_subsys* ss, struct cgroup* cgrp);
void (*pre_destroy)(struct cgroup_subsys *ss, struct cgroup *cgrp);
void (*destroy)(struct cgroup_subsys *ss, struct cgroup *cgrp);
int (*can_attach)(struct cgroup_subsys *ss, struct cgroup *cgrp, struct task_struct *tsk);
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 (*populate)(struct cgroup_subsys *ss, struct cgroup *cgrp);
void (*post_clone)(struct cgroup_subsys *ss, struct cgroup *cgrp);
void (*bind)(struct cgroup_subsys *ss, struct cgroup *root);
int subsys_id;
int active;
int disabled;
int early_init;
const char* name;
struct cgroupfs_root* root;
struct list_headsibling;
void* private;
};
cgroup_subsys 结构包含了很多函数指针,通过这些函数指针,CGroup可以对子系统进行一些操作。比如向CGroup的tasks文件添加要控制的进程PID时,就会调用 cgroup_subsys 结构的 attach 函数。当在层级中创建新目录时,就会调用 create 函数创建一个子系统的资源控制统计信息对象 cgroup_subsys_state ,并且调用 populate 函数创建子系统相关的资源控制信息文件。
除了函数指针外, cgroup_subsys 结构还包含了很多字段,下面说明一下各个字段的作用:
- subsys_id : 表示了子系统的ID。
- active : 表示子系统是否被激活。
- disabled : 子系统是否被禁止。
- name : 子系统名称。
- root : 被附加到的层级挂载点。
- sibling : 用于连接被附加到同一个层级的所有子系统。
- private : 私有数据。
内存子系统定义了一个名为 mem_cgroup_subsys 的 cgroup_subsys 结构,如下:
struct cgroup_sub sysmem_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,
};
另外 Linux 内核还定义了一个 cgroup_subsys 结构的数组 subsys ,用于保存所有 子系统 的 cgroup_subsys 结构,如下:
static struct cgroup_subsys* subsys[] = {
cpuset_subsys,
debug_subsys,
ns_subsys,
cpu_cgroup_subsys,
cpuacct_subsys,
mem_cgroup_subsys
};
2 部分重要实现
2.1 CGroup 的挂载实现
前面介绍了 CGroup 相关的几个结构体,接下来我们分析一下 CGroup 的实现。
要使用 CGroup 功能首先必须先进行挂载操作,比如使用下面命令挂载一个 CGroup :
$ mount -t cgroup -o memory memory /sys/fs/cgroup/memory
在上面的命令中, -t 参数指定了要挂载的文件系统类型为 cgroup ,而 -o 参数表示要附加到此 层级 的子系统,上面表示附加了 内存子系统 ,当然可以附加多个 子系统 。而紧随 -o 参数后的 memory 指定了此 CGroup 的名字,最后一个参数表示要挂载的目录路径。
挂载过程最终会调用内核函数 cgroup_get_sb 完成,由于 cgroup_get_sb 函数比较长,所以我们只分析重要部分:
static int cgroup_get_sb(struct file_system_type *fs_type, int flags, const char *unused_dev_name, void* data, struct vfsmount *mnt)
{
...
struct cgroupfs_root* root;
...
root = kzalloc( sizeof(*root), GFP_KERNEL);
...
ret = rebind_subsystems(root, root->subsys_bits);
...
struct cgroup* cgrp= & root-> top_cgroup;
cgroup_populate_dir(cgrp);
...
}
cgroup_get_sb 函数会调用 kzalloc 函数创建一个 cgroupfs_root 结构。 cgroupfs_root 结构主要用于描述这个挂载点的信息,其定义如下:
structcgroupfs_root{
struct super_block* sb;
unsigned long subsys_bits;
unsigned long actual_subsys_bits;
struct list_headsub sys_list;
struct cgroup top_cgroup;
int number_of_cgroups;
struct list_head root_list;
unsigned long flags;
char release_agent_path[PATH_MAX];
};
下面介绍一下 cgroupfs_root 结构的各个字段含义:
- sb : 挂载的文件系统超级块。
- subsys_bits/actual_subsys_bits : 附加到此层级的子系统标志。
- subsys_list : 附加到此层级的子系统(cgroup_subsys)列表。
- top_cgroup : 此层级的根cgroup。
- number_of_cgroups : 层级中有多少个cgroup。
- root_list : 连接系统中所有的cgroupfs_root。
- flags : 标志位。
其中最重要的是 subsys_list 和 top_cgroup 字段, subsys_list 表示了附加到此 层级 的所有 子系统 ,而 top_cgroup 表示此 层级 的根 cgroup 。
接着调用 rebind_subsystems 函数把挂载时指定要附加的 子系统 添加到 cgroupfs_root 结构的 subsys_list 链表中,并且为根 cgroup 的 subsys 字段设置各个 子系统 的资源控制统计信息对象,最后调用 cgroup_populate_dir 函数向挂载目录创建 cgroup 的管理文件(如 tasks 文件)和各个 子系统 的管理文件(如 memory.limit_in_bytes 文件)。
2.2 向 CGroup 添加要进行资源控制的进程
通过向 CGroup 的 tasks 文件写入要进行资源控制的进程PID,即可以对进程进行资源控制。例如下面命令:
$ echo 123012 > /sys/fs/cgroup/memory/ test/tasks
向 tasks 文件写入进程PID是通过 attach_task_by_pid 函数实现的,代码如下:
static int attach_task_by_pid(struct cgroup *cgrp, char*pidbuf)
{
pid_t pid;
struct task_struct* tsk;
int ret;
if( sscanf(pidbuf, "%d", &pid) != 1) // 读取进程pid
return -EIO;
if(pid) { // 如果有指定进程pid
...
tsk = find_task_by_vpid(pid); // 通过pid查找对应进程的进程描述符
if(!tsk || tsk->flags & PF_EXITING) {
rcu_read_unlock;
return -ESRCH;
}
...
} else{
tsk = current; // 如果没有指定进程pid, 就使用当前进程
...
}
ret = cgroup_attach_task(cgrp, tsk); // 调用 cgroup_attach_task 把进程添加到cgroup中
...
return ret;
}
attach_task_by_pid 函数首先会判断是否指定了进程pid,如果指定了就通过进程pid查找到进程描述符,如果没指定就使用当前进程,然后通过调用 cgroup_attach_task 函数把进程添加到 cgroup 中。
我们接着看看 cgroup_attach_task 函数的实现:
int cgroup_attach_task(struct cgroup *cgrp, struct task_struct *tsk)
{
int retval = 0;
struct cgroup_subsys* ss;
struct cgroup* oldcgrp;
struct css_set* cg= tsk-> cgroups;
struct css_set* newcg;
struct cgroupfs_root* root= cgrp-> root;
...
newcg = find_css_set(cg, cgrp); // 根据新的cgroup查找css_set对象
...
rcu_assign_pointer(tsk->cgroups, newcg); // 把进程的cgroups字段设置为新的css_set对象
...
// 把进程添加到css_set对象的tasks列表中
write_lock(&css_set_lock);
if(!list_empty(&tsk->cg_list)) {
list_del(&tsk->cg_list);
list_add(&tsk->cg_list, &newcg->tasks);
}
write_unlock(&css_set_lock);
// 调用各个子系统的attach函数
for_each_subsys(root, ss) {
if(ss->attach)
ss->attach(ss, cgrp, oldcgrp, tsk);
}
...
return 0;
}
cgroup_attach_task 函数首先会调用 find_css_set 函数查找或者创建一个 css_set 对象。前面说过 css_set 对象用于收集不同 cgroup 上附加的 子系统 资源统计信息对象。
因为一个进程能够被加入到不同的 cgroup 进行资源控制,所以 find_css_set 函数就是收集进程所在的所有 cgroup 上附加的 子系统 资源统计信息对象,并返回一个 css_set 对象。接着把进程描述符的 cgroups 字段设置为这个 css_set 对象,并且把进程添加到这个 css_set 对象的 tasks 链表中。
最后, cgroup_attach_task 函数会调用附加在 层级 上的所有 子系统 的 attach 函数对新增进程进行一些其他的操作(这些操作由各自 子系统 去实现)。
参考链接
linux 容器(LXC) 第4章 cgroups_caoshuming_500的博客-CSDN博客
Linux 基础:cgroup 原理与实现_CGroup_层级_控制
【docker 底层知识】cgroup 原理分析_张忠琳的博客-CSDN博客_cgroup
CGroup的原理和使用_书笑生的博客-CSDN博客_cgroup原理
Linux Cgroups详解(二) - lisperl - 博客园
Linux Cgroup系列(04):限制cgroup的内存使用(subsystem之memory)
Linux Cgroup系列(04):限制cgroup的内存使用(subsystem之memory) - SegmentFault 思否
Linux Cgroup系列(01):Cgroup概述 - SegmentFault 思否
深入理解 Linux Cgroup 系列(一):基本概念 - SegmentFault 思否
深入理解 Linux Cgroup 系列(二):玩转 CPU