《重识云原生系列》专题索引:
从进程的角度出发来剖析 cgroups 相关数据结构之间的关系。在 Linux 中管理进程的数据结构是 task_struct。cgroup表示进程的行为控制,因为子系统必须要知道进程是位于哪一个cgroup,所以在struct task_struct和cgroup中存在一种映射。
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 的进程组织成一个链表。
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;
};
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。
struct cg_cgroup_link {
struct list_head cgrp_link_list;
struct cgroup *cgrp;
struct list_head cg_link_list;
struct css_set *cg;
};
前面介绍过, cgroup 是用来控制进程组对各种资源的使用,而在内核中, cgroup 是通过 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 结构体各个字段的用途:
我们通过下面图片来描述 层级 中各个 cgroup 组成的树状关系:
cgroup链表结构示意图
每个子系统 都有属于自己的资源控制统计信息结构,而且每个 cgroup 都绑定一个这样的结构,这种资源控制统计信息结构就是通过 cgroup_subsys_state 结构体实现的,其定义如下:
struct cgroup_subsys_state{
struct cgroup* cgroup;
atomic_t refcnt;
unsigned long flags;
};
下面介绍一下 cgroup_subsys_state 结构各个字段的作用:
从 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 结构示意图
由于一个进程可以同时添加到不同的 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 结构体各个字段的作用:
进程描述符 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关系示意图
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 结构还包含了很多字段,下面说明一下各个字段的作用:
内存子系统定义了一个名为 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
};
前面介绍了 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 结构的各个字段含义:
其中最重要的是 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 文件)。
通过向 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