Linux 基础:cgroup 原理与实现

WBOY
发布: 2024-02-10 08:15:21
转载
492 人浏览过

本文将通过研究源代码(此处使用Linux 2.6.25版本)来详细解析CGroup的实现原理。在深入源代码之前,我们先来了解几个关键的数据结构,因为CGroup是通过这些数据结构来管理进程组对各种资源的使用。

cgroup结构体

前面已经提到,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结构体各个字段的用途:

  1. flags: 用于标识当前cgroup的状态。
  2. count: 引用计数器,表示有多少个进程在使用这个cgroup
  3. sibling、children、parent: 由于cgroup是通过层级来进行管理的,这三个字段就把同一个层级的所有cgroup连接成一棵树。parent指向当前cgroup的父节点,sibling连接着所有兄弟节点,而children连接着当前cgroup的所有子节点。
  4. dentry: 由于cgroup是通过虚拟文件系统来进行管理的,在介绍cgroup使用时说过,可以把cgroup当成是层级中的一个目录,所以dentry字段就是用来描述这个目录的。
  5. subsys: 前面说过,子系统能够附加到层级,而附加到层级子系统都有其限制进程组使用资源的算法和统计数据。所以subsys字段就是提供给各个子系统存放其限制进程组使用资源的统计数据。我们可以看到subsys字段是一个数组,而数组中的每一个元素都代表了一个子系统相关的统计数据。从实现来看,cgroup只是把多个进程组织成控制进程组,而真正限制资源使用的是各个子系统
  6. root: 用于保存层级的一些数据,比如:层级的根节点,附加到层级子系统列表(因为一个层级可以附加多个子系统),还有这个层级有多少个cgroup节点等。
  7. top_cgroup:层级的根节点(根cgroup)。

我们通过下面图片来描述层级中各个cgroup组成的树状关系:

Linux 基础:cgroup 原理与实现cgroup-links

cgroup_subsys_state结构体

每个子系统都有属于自己的资源控制统计信息结构,而且每个cgroup都绑定一个这样的结构,这种资源控制统计信息结构就是通过cgroup_subsys_state结构体实现的,其定义如下:

struct cgroup_subsys_state { struct cgroup *cgroup; atomic_t refcnt; unsigned long flags; };
登录后复制

下面介绍一下cgroup_subsys_state结构各个字段的作用:

  1. cgroup: 指向了这个资源控制统计信息所属的cgroup
  2. refcnt: 引用计数器。
  3. flags: 标志位,如果这个资源控制统计信息所属的cgroup层级的根节点,那么就会将这个标志位设置为CSS_ROOT表示属于根节点。

cgroup_subsys_state结构的定义看不到各个子系统相关的资源控制统计信息,这是因为cgroup_subsys_state结构并不是真实的资源控制统计信息结构,比如内存子系统真正的资源控制统计信息结构是mem_cgroup,那么怎样通过这个cgroup_subsys_state结构去找到对应的mem_cgroup结构呢?我们来看看mem_cgroup结构的定义:

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结构的定义可以发现,mem_cgroup结构的第一个字段就是一个cgroup_subsys_state结构。下面的图片展示了他们之间的关系:

Linux 基础:cgroup 原理与实现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结构之间的关系如下图:

Linux 基础:cgroup 原理与实现cgroup-subsys-state

css_set结构体

由于一个进程可以同时添加到不同的cgroup中(前提是这些cgroup属于不同的层级)进行资源控制,而这些cgroup附加了不同的资源控制子系统。所以需要使用一个结构把这些子系统的资源控制统计信息收集起来,方便进程通过子系统ID快速查找到对应的子系统资源控制统计信息,而css_set结构体就是用来做这件事情。css_set结构体定义如下:

struct css_set { struct kref ref; struct list_head list; struct list_head tasks; struct list_head cg_links; struct cgroup_subsys_state *subsys[CGROUP_SUBSYS_COUNT]; };
登录后复制

下面介绍一下css_set结构体各个字段的作用:

  1. ref: 引用计数器,用于计算有多少个进程在使用此css_set
  2. list: 用于连接所有css_set
  3. tasks: 由于可能存在多个进程同时受到相同的cgroup控制,所以用此字段把所有使用此css_set的进程连接起来。
  4. 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结构的关系如下图:

Linux 基础:cgroup 原理与实现

cgroup-task-cssset

cgroup_subsys结构

CGroup通过cgroup_subsys结构操作各个子系统,每个子系统都要实现一个这样的结构,其定义如下:

struct cgroup_subsys { struct cgroup_subsys_state *(*create)(struct cgroup_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_head sibling; void *private; };
登录后复制

cgroup_subsys结构包含了很多函数指针,通过这些函数指针,CGroup可以对子系统进行一些操作。比如向CGrouptasks文件添加要控制的进程PID时,就会调用cgroup_subsys结构的attach()函数。当在层级中创建新目录时,就会调用create()函数创建一个子系统的资源控制统计信息对象cgroup_subsys_state,并且调用populate()函数创建子系统相关的资源控制信息文件。

除了函数指针外,cgroup_subsys结构还包含了很多字段,下面说明一下各个字段的作用:

  1. subsys_id: 表示了子系统的ID。
  2. active: 表示子系统是否被激活。
  3. disabled: 子系统是否被禁止。
  4. name: 子系统名称。
  5. root: 被附加到的层级挂载点。
  6. sibling: 用于连接被附加到同一个层级的所有子系统。
  7. private: 私有数据。

内存子系统定义了一个名为mem_cgroup_subsyscgroup_subsys结构,如下:

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, };
登录后复制

另外 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功能首先必须先进行挂载操作,比如使用下面命令挂载一个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结构主要用于描述这个挂载点的信息,其定义如下:

struct cgroupfs_root { struct super_block *sb; unsigned long subsys_bits; unsigned long actual_subsys_bits; struct list_head subsys_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结构的各个字段含义:

  1. sb: 挂载的文件系统超级块。
  2. subsys_bits/actual_subsys_bits: 附加到此层级的子系统标志。
  3. subsys_list: 附加到此层级的子系统(cgroup_subsys)列表。
  4. top_cgroup: 此层级的根cgroup。
  5. number_of_cgroups: 层级中有多少个cgroup。
  6. root_list: 连接系统中所有的cgroupfs_root。
  7. flags: 标志位。

其中最重要的是subsys_listtop_cgroup字段,subsys_list表示了附加到此层级的所有子系统,而top_cgroup表示此层级的根cgroup

字段, 表示了附加到此 层级的所有 子系统,而 表示此 层级的根 cgroup

接着调用rebind_subsystems()函数把挂载时指定要附加的子系统添加到cgroupfs_root结构的subsys_list链表中,并且为根cgroupsubsys字段设置各个子系统的资源控制统计信息对象,最后调用cgroup_populate_dir()函数向挂载目录创建cgroup的管理文件(如tasks文件)和各个子系统的管理文件(如memory.limit_in_bytes文件)。

CGroup添加要进行资源控制的进程

通过向CGrouptasks文件写入要进行资源控制的进程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()函数对新增进程进行一些其他的操作(这些操作由各自子系统去实现)。

限制CGroup的资源使用

本文主要是使用内存子系统作为例子,所以这里分析内存限制的原理。

可以向cgroupmemory.limit_in_bytes文件写入要限制使用的内存大小(单位为字节),如下面命令限制了这个cgroup只能使用 1MB 的内存:

$ echo 1048576 > /sys/fs/cgroup/memory/test/memory.limit_in_bytes
登录后复制

memory.limit_in_bytes写入数据主要通过mem_cgroup_write()函数实现的,其实现如下:

static ssize_t mem_cgroup_write(struct cgroup *cont, struct cftype *cft, struct file *file, const char __user *userbuf, size_t nbytes, loff_t *ppos) { return res_counter_write(&mem_cgroup_from_cont(cont)->res, cft->private, userbuf, nbytes, ppos, mem_cgroup_write_strategy); }
登录后复制

其主要工作就是把内存子系统的资源控制对象mem_cgroupres.limit字段设置为指定的数值。

限制进程使用资源

当设置好cgroup的资源使用限制信息,并且把进程添加到这个cgrouptasks列表后,进程的资源使用就会受到这个cgroup的限制。这里使用内存子系统作为例子,来分析一下内核是怎么通过cgroup来限制进程对资源的使用的。

当进程要使用内存时,会调用do_anonymous_page()来申请一些内存页,而do_anonymous_page()函数会调用mem_cgroup_charge()函数来检测进程是否超过了cgroup设置的资源限制。而mem_cgroup_charge()最终会调用mem_cgroup_charge_common()函数进行检测,mem_cgroup_charge_common()函数实现如下:

static int mem_cgroup_charge_common(struct page *page, struct mm_struct *mm, gfp_t gfp_mask, enum charge_type ctype) { struct mem_cgroup *mem; ... mem = rcu_dereference(mm->mem_cgroup); // 获取进程对应的内存限制对象 ... while (res_counter_charge(&mem->res, PAGE_SIZE)) { // 判断进程使用内存是否超出限制 if (!(gfp_mask & __GFP_WAIT)) goto out; if (try_to_free_mem_cgroup_pages(mem, gfp_mask)) // 如果超出限制, 就释放一些不用的内存 continue; if (res_counter_check_under_limit(&mem->res)) continue; if (!nr_retries--) { mem_cgroup_out_of_memory(mem, gfp_mask); // 如果尝试过5次后还是超出限制, 那么发出oom信号 goto out; } ... } ... }
登录后复制

mem_cgroup_charge_common()函数会对进程内存使用情况进行检测,如果进程已经超过了cgroup设置的限制,那么就会尝试进行释放一些不用的内存,如果还是超过限制,那么就会发出OOM (out of memory)的信号。

以上是Linux 基础:cgroup 原理与实现的详细内容。更多信息请关注PHP中文网其他相关文章!

来源:lxlinux.net
本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责声明 Sitemap
PHP中文网:公益在线PHP培训,帮助PHP学习者快速成长!