cgroup的原理其实并不复杂,用法也比较简单。但是涉及的内核数据结构真的复杂,错综复杂的数据结构感觉才是cgroup真正的难点。本文结合个人学习cgroup源码的心得,尽可能以举例的形式,总结cgroup整体框架和核心源码实现,尽可能少贴源码。本次是在centos 7.6测试的cgroup,源码注释基于3.10.96。更详细的源码注释见https://github.com/dongzhiyan-stack/kernel-code-comment。

这里先把cgroup涉及的各个数据结构的关系图发下,后边需要多次用到这幅图。

1 cgroup的创建

centos 7.6系统启动后,默认systemd就已经挂载好了cgroup文件系统

[root@localhost ~]# mount | grep cgroup
tmpfs on /sys/fs/cgroup type tmpfs (ro,nosuid,nodev,noexec,seclabel,mode=755)
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/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,hugetlb)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,cpuacct,cpu)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,blkio)
cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,perf_event)
cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,pids)
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,cpuset)
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/memory type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,memory)
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,freezer)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,devices)

进入cgroup挂载目录看一下cgroup各个子系统,如下:

[root@localhost ~]# cd /sys/fs/cgroup/
[root@localhost cgroup]# ls
blkio  cpu  cpuacct  cpu,cpuacct  cpuset  devices  freezer  hugetlb  memory  net_cls  net_cls,net_prio  net_prio  perf_event  pids  systemd

cpu、blkio、memory是比较常见的cgroup子系统,分别用来限制进程CPU使用率、IO传输吞吐量IOPS、内存分配。每个cgroup子系统都有特定的功能,这个比较好理解。这里需要提一下另一个概念:cgroup层级,如下是从https://tech.meituan.com/2015/03/31/cgroups.html 直接贴过来的演示cgroup层级的示意图:

个人认为一个cgroup层级更像是一个包含不同功能的cgroup子系统集合。比如示意图的cgroup层级A集成了cpu、cpuacct两个cgroup子系统,cgroup层级B集成了memory这个cgroup子系统。上边介绍centos 7.6的cgroup子系统时,ls  /sys/fs/cgroup时看到了很多个cgroup子系统,这种情况应该只有一个cgroup层级,当然你也可以再创建一个cgroup层级。网上关于cgroup层级的介绍有点抽象,当然我的理解也不一定到位。

回到cgroup子系统,如果我们想限制进程1的CPU使用率不能超过50%,可以执行如下命令:

[root@localhost cgroup]# cd /sys/fs/cgroup/cpu
[root@localhost cpu]# mkdir test
[root@localhost cpu]# cd test
[root@localhost test]# ls
cgroup.clone_children  cgroup.procs  cpuacct.usage         cpu.cfs_period_us  cpu.rt_period_us   cpu.shares  notify_on_release
cgroup.event_control   cpuacct.stat  cpuacct.usage_percpu  cpu.cfs_quota_us   cpu.rt_runtime_us  cpu.stat    tasks
[root@localhost test]# cat cpu.cfs_quota_us
-1
[root@localhost test]# cat cpu.cfs_period_us
100000
[root@localhost test]# echo 50000 > cpu.cfs_quota_us
[root@localhost test]# echo 进程1PID > tasks

之后top进程1的CPU使用率最大50%,即便进程1是陷入while(1);死循环。我们赋予cpu.cfs_quota_us的是50000,cpu.cfs_period_us是100000。cpu.cfs_period_us表示一个调度周期,cpu.cfs_quota_us表示一个调度周期进程可以使用的最大配额,显然是一半,即50%。关于cgroup的使用,这里不再介绍,我们重点介绍以上这些命令设置的cgroup内核源码实现。

首先” mkdir test”命令,内核流程是sys_mkdir-> SyS_mkdirat-> vfs_mkdir-> cgroup_mkdir-> cgroup_create

  1. static long cgroup_create(struct cgroup *parent, struct dentry *dentry,//dentry是新创建的目录的dentry
  2. umode_t mode)
  3. {
  4. struct cgroup *cgrp;
  5. struct cgroupfs_root *root = parent->root;
  6. //分配本次的cgroup结构体,一个目录对应一个struct cgroup
  7. cgrp = kzalloc(sizeof(*cgrp), GFP_KERNEL);
  8. //指向本cgroup对应目录的dentry
  9. cgrp->dentry = dentry;
  10. //cgroup的parent
  11. cgrp->parent = parent;
  12. //指向根cgroupfs_root
  13. cgrp->root = parent->root;
  14. //遍历cgroupfs_root->subsys_list链表,找到该链表上的cgroup_subsys,即cgroup子系统,然后执行该cgroup子系统的css_alloc()函数
  15. for_each_subsys(root, ss) {
  16. //分配cgroup子系统的控制结构(如cpu的是task_group),返回task_group结构的第一个成员struct cgroup_subsys_state css的地址赋于css
  17. css = ss->css_alloc(cgrp);//cpu子系统是cpu_cgroup_css_alloc()
  18. //初始化struct cgroup_subsys_state css,并赋值cgrp->subsys[ss->subsys_id]=css
  19. init_cgroup_css(css, ss, cgrp);
  20. }
  21. //创建该目录的inode,建立dentry和inode的关系
  22. err = cgroup_create_file(dentry, S_IFDIR | mode, sb);
  23. //生成该cgroup目录下相关子系统的控制文件,如"tasks"、"notify_on_release"等
  24. err = cgroup_populate_dir(cgrp, true, root->subsys_mask);
  25. }

这个函数一下引出了很多数据结构,struct cgroup、struct cgroupfs_root、struct cgroup_subsys、struct cgroup_subsys_state。

1  struct  cgroup:当我们mkdir  test创建一个cgroup目录时,首先会在vfs层创建一个该目录的dentry结构,然后执行cgroup文件系统的cgroup_mkdir()->cgroup_create()函数,该函数首先分配一个struct cgroup结构。每创建一个cgroup目录,都要分配一个struct cgroup结构与之对应。

2   struct cgroupfs_root和struct cgroup_subsys:当cgroup文件系统mount挂载时(比如cpu cgroup挂载时执行的mount -t cgroup -ocpu cpu /sys/fs/cgroup/cpu,cpuacct命令),内核里最后执行cgroup_mount(),分配struct cgroupfs_root和super_block结构,二者一一对应。然后执行cgroup_mount()->rebind_subsystems()中,按照该cgroup子系统编号从subsys[i]全局数组取出struct cgroup_subsys结构(cpu cgroup子系统的是struct cgroup_subsys  cpuset_subsys,blkio的是struct cgroup_subsys blkio_subsys),然后把struct cgroup_subsys移动到struct cgroupfs_root的subsys_list链表。

3  struct cgroup_subsys_state:以cpu 子系统为例,其他cgroup子系统类似。每次创建cgroup目录分配struct  cgroup后,都会执行cpu_cgroup_css_alloc()分配cgroup控制结构struct task_group,而struct task_group的第一个成员就是struct cgroup_subsys_state。再令struct  cgroup结构的成员struct cgroup_subsys_state *subsys[cpu子系统ID]指向刚才分配的struct cgroup_subsys_state结构。后续container_of(cgroup_subsys_state指针)就指向刚才分配的struct task_group。如此就可以通过struct  cgroup找到对应的struct  task_group,这是cgroup_subsys_state存在的意义,牵线搭桥。

struct group代表的是cgroup目录,struct  task_group代表的是该cgroup目录对应的cpu cgroup子系统的控制结构,还有struct  task_group的第一个成员struct cgroup_subsys_state   css,3者 一一对应。将来正是用struct  task_group限制进程的CPU使用率。

为了便于理解,把这些数据结构的关系图单独截个大图,关系标的还算明确。

cpu、memory、blkio等cgroup子系统用struct cgroup_subsys表示,struct cgroupfs_root是该cgroup子系统mount挂载时分配的,与super_block一一对应。所以cgroup子系统cgroup_subsys和它的cgroupfs_root一一对应,可以看些示意图。

结合这个示意图,再啰嗦一下彼此的关系。struct  cgroup的成员struct  cgroup_subsys_state *subsys[]保存的是struct  cgroup_subsys_state指针。比如,当该cgroup是属于cpu cgroup子系统,subsys[cpuset_subsys_id]   ( cpuset_subsys_id是cpu cgroup子系统ID,这个数组只有一个成员指向的cgroup_subsys_state指针有效)指向的cgroup_subsys_state是struct  task_group结构的成员struct  cgroup_subsys_state css。这样知道了cgroup_subsys_state的地址,container_of(cgroup_subsys_state)就是struct  task_group的地址。

如下是常见的cpu、memory、blkio这3个cgroup子系统的struct cgroup_subsys结构。

  1. struct cgroup_subsys cpu_cgroup_subsys = {
  2. .name       = "cpu",
  3. .css_alloc  = cpu_cgroup_css_alloc,//创建cpu cgroup具体的控制单元task_group
  4. .css_free   = cpu_cgroup_css_free,
  5. .css_online = cpu_cgroup_css_online,
  6. .css_offline    = cpu_cgroup_css_offline,
  7. .can_attach = cpu_cgroup_can_attach,
  8. .attach     = cpu_cgroup_attach,
  9. .exit       = cpu_cgroup_exit,
  10. .subsys_id  = cpu_cgroup_subsys_id,//cpu cgroup子系统的id是cpu_cgroup_subsys_id
  11. .base_cftypes   = cpu_files, //cpu cgroup子系统独有的控制文件
  12. .early_init = 1,
  13. }
  14. struct cgroup_subsys mem_cgroup_subsys = {
  15. .name = "memory",
  16. .subsys_id = mem_cgroup_subsys_id,
  17. .css_alloc = mem_cgroup_css_alloc,
  18. .css_online = mem_cgroup_css_online,
  19. .css_offline = mem_cgroup_css_offline,
  20. .css_free = mem_cgroup_css_free,
  21. .can_attach = mem_cgroup_can_attach,
  22. .cancel_attach = mem_cgroup_cancel_attach,
  23. .attach = mem_cgroup_move_task,
  24. .bind = mem_cgroup_bind,
  25. .base_cftypes = mem_cgroup_files,//memory cgroup子系统独有的控制文件
  26. .early_init = 0,
  27. .use_id = 1,
  28. };
  29. struct cgroup_subsys blkio_subsys = {
  30. .name = "blkio",
  31. .css_alloc = blkcg_css_alloc,
  32. .css_offline = blkcg_css_offline,
  33. .css_free = blkcg_css_free,
  34. .can_attach = blkcg_can_attach,
  35. .subsys_id = blkio_subsys_id,
  36. .base_cftypes = blkcg_files,
  37. .module = THIS_MODULE,
  38. .broken_hierarchy = true,
  39. };

每个cgroup子系统的struct cgroup_subsys结构最终是保存在struct cgroup_subsys *subsys[CGROUP_SUBSYS_COUNT]全局结构体指针数组中,如下:

#define SUBSYS(_x) [_x ## _subsys_id] = &_x ## _subsys,

  1. static struct cgroup_subsys *subsys[CGROUP_SUBSYS_COUNT] = {
  2. #include <linux/cgroup_subsys.h>
  3. SUBSYS(cpuset)
  4. SUBSYS(debug)
  5. SUBSYS(cpu_cgroup)//限制CPU
  6. SUBSYS(cpuacct)
  7. SUBSYS(mem_cgroup)//限制mem
  8. SUBSYS(devices)
  9. SUBSYS(freezer)
  10. SUBSYS(net_cls)
  11. SUBSYS(blkio)//限制block
  12. SUBSYS(net_prio)
  13. SUBSYS(hugetlb)
  14. };

把这些结构体拆开简化后如下:

  1. static struct cgroup_subsys *subsys[CGROUP_SUBSYS_COUNT] = {
  2. ………
  3. [cpuset_subsys_id] = cpu_cgroup_subsys,
  4. ………
  5. [mem_cgroup_subsys_id] = mem_cgroupt_subsys,
  6. [blkio_subsys_id] = blkio_subsys,
  7. ………
  8. };

终于看到cpu、memory、blkio等cgroup子系统的struct cgroup_subsys结构cpu_cgroup_subsys、mem_cgroupt_subsys、blkio_subsys了。cpuset_subsys_id是cpu cgroup子系统的ID,其他类似。

回到cgroup_create()函数,该函数最后执行cgroup_populate_dir()创建该cgroup目录下相关子系统的控制文件。什么意思?前文执行mkdir test创建”test”这个cgroup目录后,ls test便会看到” tasks”、” cpu.cfs_period_us”、” cpu.cfs_quota_us”等文件。我们并没有在”test” cgroup目录下创建这些文件,从何而来?正是sys_mkdir-> SyS_mkdirat-> vfs_mkdir-> cgroup_mkdir-> cgroup_create-> cgroup_populate_dir()中创建的,下一节讲解。

2 cgruop目录下文件的创建与读写

2.1 cgruop目录下文件的创建

首先说明一下内核struct cftype表示一个cgroup文件。看下cgroup_populate_dir()函数的关键源码。

  1. static int cgroup_populate_dir(struct cgroup *cgrp, bool base_files,
  2. unsigned long subsys_mask)
  3. {
  4. struct cgroup_subsys *ss;
  5. //从struct cftype files[]全局结构体获取cgroup基本的文件cftype,比如"task"、"release_agent"等基本cgroup文件,然后在cgroup子系统的目录下创建这些基本文件*/
  6. if (base_files) {
  7. err = cgroup_addrm_files(cgrp, NULL, files, true);
  8. }
  9. /*cgrp->root指向根cgroupfs_root,这是遍历根cgroupfs_root的subsys_list链表上的cpu、memory、blkio等cgroup_subsys,再通过cgroup_subsys的成员sibling container_of找到cftype_set,再找到cftype_set的成员cfts指向的cftype。这个cftype是每个cgroup_subsys特有的cftype。上边的是创建cgroup基本文件,这里的cftype是cpu、memory、blkio特有的cgroup文件。*/
  10. for_each_subsys(cgrp->root, ss) {
  11. struct cftype_set *set;
  12. /*这是限定只有与subsys_mask指定的cgroup子系统匹配才能执行下边的cgroup_addrm_files()创建特定cgroup子系统的cgroup文件。比如在cpu cgroup目录下创建cgroup文件,该if只有是ss是cpu的cgroup_subsys才不成立*/
  13. if (!test_bit(ss->subsys_id, &subsys_mask))
  14. continue;
  15. /*遍历cgroup_subsys该cgroup子系统cftsets链表上的cftype_set,再通过cftype_set的cfts找到cgroup子系统特有的struct cftype[],创建cftype指定的该cgroup子系统特有的cgroup文件。cgroup子系统特有的struct cftype[]有哪些呢?如struct cftype cpu_files[],struct cftype blkcg_files[],struct cftype throtl_files[].更详细见struct cftype结构体描述。*/
  16. list_for_each_entry(set, &ss->cftsets, node)
  17. cgroup_addrm_files(cgrp, ss, set->cfts, true);
  18. }
  19. }
  20. static int cgroup_addrm_files(struct cgroup *cgrp, struct cgroup_subsys *subsys,
  21. struct cftype cfts[], bool is_add)
  22. {
  23. struct cftype *cft;
  24. //依次取出struct cftype files定义的文件
  25. for (cft = cfts; cft->name[0] != '\0'; cft++) {
  26. //cft指向struct cftype files[]定义的每个基本文件结构。以cft指向成员内容,创建cgroup下的文件,常见的"tasks"等等,
  27. cgroup_add_file(cgrp, subsys, cft);
  28. }
  29. }
  30. //以cft指向成员内容,创建cgroup下的文件,常见的"tasks"等等
  31. static int cgroup_add_file(struct cgroup *cgrp, struct cgroup_subsys *subsys,
  32. struct cftype *cft)//创建cgroup基本文件时,cft指向struct cftype files[]定义的每个基本文件结构
  33. {
  34. //cgroup子系统父目录的dentry,下边就是在这个dentry下创建cgroup子文件"tasks"等等
  35. struct dentry *dir = cgrp->dentry;
  36. struct cgroup *parent = __d_cgrp(dir);
  37. struct dentry *dentry;
  38. struct cfent *cfe;
  39. //文件名字,比如"tasks"
  40. strcat(name, cft->name);
  41. //分配cfe,cfe代表了一个cgroup目录下的的cgroup文件
  42. cfe = kzalloc(sizeof(*cfe), GFP_KERNEL);
  43. //在父目录base下,查找或者创建name指定的文件dentry
  44. dentry = lookup_one_len(name, dir, strlen(name));
  45. //cfe保存这个cgroup文件的基本信息
  46. cfe->type = (void *)cft;
  47. cfe->dentry = dentry;
  48. dentry->d_fsdata = cfe;
  49. //分配文件的inode,建立inode和dentry联系,成功返回0
  50. error = cgroup_create_file(dentry, mode | S_IFREG, cgrp->root->sb);
  51. //把新分配的cfe添加到cgroup父目录的struct cgroup的files链表
  52. list_add_tail(&cfe->node, &parent->files);
  53. }

一下又冒出了几个结构体struct cftype、struct cfent。struct cftype包含原始的cgroup文件信息,它来自两部分。一部分是cgroup基本文件,如”tasks”、” notify_on_release”等等,cpu、blkio、memory等cgroup子系统都有。这些文件信息定义在kernel/cgroup.c文件的struct cftype files[]这个结构体数组,如下所示:

// cgroup子系统base控制文件结构struct cftype files[]

  1. static struct cftype files[] = { //这个是 cgroup子系统的基本文件
  2. {
  3. .name = "tasks",
  4. .open = cgroup_tasks_open,
  5. .write_u64 = cgroup_tasks_write,//绑定每个进程
  6. .release = cgroup_pidlist_release,
  7. .mode = S_IRUGO | S_IWUSR,
  8. },
  9. {
  10. .name = CGROUP_FILE_GENERIC_PREFIX "procs",
  11. .open = cgroup_procs_open,
  12. .write_u64 = cgroup_procs_write,
  13. .release = cgroup_pidlist_release,
  14. .mode = S_IRUGO | S_IWUSR,
  15. },
  16. {
  17. .name = "notify_on_release",
  18. .read_u64 = cgroup_read_notify_on_release,
  19. .write_u64 = cgroup_write_notify_on_release,
  20. },
  21. .............
  22. };

但是每个cgroup子系统也有特有的cgroup控制文件,如cpu 子系统的” cpu.cfs_period_us”、” cpu.cfs_quota_us”等cgroup文件,memory子系统的” memory.limit_in_bytes”、” memory.usage_in_bytes”等cgroup文件,blkio子系统的” blkio.throttle.read_iops_device”、” blkio.throttle.write_iops_device”等cgroup文件。我们看下cpu cgroup子系统特有的控制文件结构体,如下:

  1. static struct cftype cpu_files[] = {
  2. {
  3. .name = "cfs_quota_us",//cfs进程运行时间配额
  4. .read_s64 = cpu_cfs_quota_read_s64,
  5. .write_s64 = cpu_cfs_quota_write_s64,
  6. },
  7. {
  8. .name = "cfs_period_us",//cfs进程运行周期
  9. .read_u64 = cpu_cfs_period_read_u64,
  10. .write_u64 = cpu_cfs_period_write_u64,
  11. },
  12. {
  13. .name = "stat",
  14. .read_map = cpu_stats_show,
  15. }
  16. }

blkio cgroup子系统独有的控制文件结构体数组struct cftype throtl_files[]

  1. static struct cftype throtl_files[] = {
  2. ..........
  3. {
  4. .name = "throttle.read_iops_device",
  5. .private = offsetof(struct throtl_grp, iops[READ]),
  6. .read_seq_string = tg_print_conf_uint,
  7. .write_string = tg_set_conf_uint,
  8. .max_write_len = 256,
  9. },
  10. {
  11. .name = "throttle.write_iops_device",
  12. .private = offsetof(struct throtl_grp, iops[WRITE]),
  13. .read_seq_string = tg_print_conf_uint,
  14. .write_string = tg_set_conf_uint,
  15. .max_write_len = 256,
  16. },
  17. ..........
  18. }

memory cgroup子系统控制文件结构体数组struct cftype mem_cgroup_files []

  1. static struct cftype mem_cgroup_files[] = {
  2. {
  3. .name = "usage_in_bytes",
  4. .private = MEMFILE_PRIVATE(_MEM, RES_USAGE),
  5. .read = mem_cgroup_read,
  6. .register_event = mem_cgroup_usage_register_event,
  7. .unregister_event = mem_cgroup_usage_unregister_event,
  8. },
  9. {
  10. .name = "max_usage_in_bytes",
  11. .private = MEMFILE_PRIVATE(_MEM, RES_MAX_USAGE),
  12. .trigger = mem_cgroup_reset,
  13. .read = mem_cgroup_read,
  14. },
  15. {
  16. .name = "limit_in_bytes",//设置进程内存使用
  17. .private = MEMFILE_PRIVATE(_MEM, RES_LIMIT),
  18. .write_string = mem_cgroup_write,
  19. .read = mem_cgroup_read,
  20. },
  21. .........
  22. }

以上提到的这些cgroup文件结构体struct cftype是怎么添加到系统的呢?又是怎么被使用而最终在cgroup目录创建这些文件呢?我们一一道来:

struct cftype主要有3类

1  第一类,cgroup子系统基本的struct cftype数组,对应"tasks"、"release_agent"等cgroup文件,保存在struct cftype files[]全局结构体数组,这些cgroup文件的创建在cgroup_create->cgroup_populate_dir->cgroup_addrm_files(cgrp, NULL, files, true)( if (base_files)那个分支),前文提过。

接着

2  第二类,每个cgroup子系统struct cgroup_subsys结构体base_cftypes成员指向的struct cftype结构体数组。如cpu cgroup子系统struct cgroup_subsys cpu_cgroup_subsys[]的struct cftype cpu_files[],如下:

  1. struct cgroup_subsys cpu_cgroup_subsys = {
  2. .name       = "cpu",
  3. .css_alloc  = cpu_cgroup_css_alloc,
  4. .css_free   = cpu_cgroup_css_free,
  5. .css_online = cpu_cgroup_css_online,
  6. .css_offline    = cpu_cgroup_css_offline,
  7. .can_attach = cpu_cgroup_can_attach,
  8. .attach     = cpu_cgroup_attach,
  9. .exit       = cpu_cgroup_exit,
  10. .subsys_id  = cpu_cgroup_subsys_id,
  11. .base_cftypes   = cpu_files,
  12. .early_init = 1,
  13. };

cpu cgroup子系统struct cgroup_subsys的base_cftypes成员指向的struct cftype cpu_files[]是怎么添加到cgroup相关的数据结构呢?这是在cgroup子系统模块初始化cgroup_init->cgroup_init_subsys()->cgroup_init_cftsets,源码如下:

  1. int __init cgroup_init(void)
  2. {
  3. for (i = 0; i < CGROUP_SUBSYS_COUNT; i++) {
  4. //从subsys[]按照cgroup子系统编号依次取出 cpuset_subsys 、 blkio_subsys等这些cgroup子系统结构体struct cgroup_subsys
  5. struct cgroup_subsys *ss = subsys[i];
  6. cgroup_init_subsys(ss);//cpu、mem、cgroup等cgroup_subsys结构初始化赋值
  7. }
  8. }
  9. static void __init cgroup_init_subsys(struct cgroup_subsys *ss)
  10. {
  11. cgroup_init_cftsets(ss);//初始化cpu、memcory、block 的cgroup文件
  12. list_add(&ss->sibling, &rootnode.subsys_list);//cgroup_subsys添加到subsys_list链表
  13. ss->root = &rootnode;//cgroup_subsys->root 指向rootnode
  14. css = ss->css_alloc(dummytop);//分配顶层cgroup的struct cgroup_subsys_state
  15. init_cgroup_css(css, ss, dummytop);//初始化顶层cgorup的struct cgroup_subsys_state
  16. }
  17. static void __init_or_module cgroup_init_cftsets(struct cgroup_subsys *ss)
  18. {
  19. INIT_LIST_HEAD(&ss->cftsets);
  20. if (ss->base_cftypes) {
  21. //cgroup_subsys的cftype_set的cfts指向cgroup子系统cgroup_subsys的成员base_cftypes指向的struct cftype数组
  22. ss->base_cftset.cfts = ss->base_cftypes;
  23. //把cgroup子系统struct cgroup_subsys的成员base_cftypes指向的struct cftype数组添加到cgroup子系统struct cgroup_subsys的cftsets链表
  24. list_add_tail(&ss->base_cftset.node, &ss->cftsets);
  25. }
  26. }

除了cpu的struct cftype cpu_files[]数组,memory子系统的struct cftype mem_cgroup_files[]数组,blkio子系统的struct cftype blkcg_files[]数组,都是以这种形式添加到各个cgroup子系统struct cgroup_subsys的cftsets链表。

这些cgroup文件的创建在:cgroup_create->cgroup_populate_dir->cgroup_addrm_files(cgrp, ss, set->cfts, true)( for_each_subsys(cgrp->root, ss)那个分支)

3  第三类,每个cgroup子系统动态添加的struct cftype数组。比如blkio  cgroup子系统blkcg_policy_register->cgroup_add_cftypes(),把block层流控的struct cftype throtl_files[]数组动态添加到blkio子系统struct cgroup_subsys的cftsets链表,源码如下:

int blkcg_policy_register(struct blkcg_policy *pol)

{

//这个pol->cftypes就是blkio 流控的cftype数组struct cftype throtl_files[]

if (pol->cftypes)//把blkio流控的cftype数组struct cftype throtl_files[]添加到blkio cgorup子系统struct blkio_subsys的cftsets链表

WARN_ON(cgroup_add_cftypes(&blkio_subsys, pol->cftypes)

}

//把cgroup子系统特有的cgroup控制文件cftype数组添加到cgorup子系统struct cgroup_subsys的cftsets链表

  1. int cgroup_add_cftypes(struct cgroup_subsys *ss, struct cftype *cfts)
  2. {
  3. struct cftype_set *set;
  4. //分配cftype_set
  5. set = kzalloc(sizeof(*set), GFP_KERNEL);
  6. if (!set)
  7. return -ENOMEM;
  8. cgroup_cfts_prepare();
  9. //cftype_set的cfts指向cftype数组
  10. set->cfts = cfts;
  11. //cftype_set靠其node成员添加到cgroup_subsys的cftsets链表
  12. list_add_tail(&set->node, &ss->cftsets);
  13. cgroup_cfts_commit(ss, cfts, true);
  14. return 0;
  15. }

这些cgroup文件的创建也是在cgroup_create->cgroup_populate_dir->cgroup_addrm_files(cgrp, ss, set->cfts, true)(for_each_subsys(cgrp->root, ss)那个分支)。

对于第2、3点,需要特别说明,cgroup_populate_dir()函数for_each_subsys(cgrp->root, ss)那个分支,是遍历该cgroup子系统struct cgroup_subsys的成员cftsets链表上的cftype_set,再通过cftype_set的成员cfts找到cgroup子系统特有的struct cftype[]结构体数组。然后根据这个数组创建指定的cgroup文件。这些cgroup子系统特有的控制文件cftype数组前文已经介绍过怎么添加到cgroup_subsys该cgroup子系统cftsets链表上。

需要说明,struct cftype_set存在的意义就是为了把每个cgroup子系统特有的cgroup文件数组struct cftype[]添加到cgroup子系统struct cgroup_subsys的成员cftsets链表上。为什么要这样呢?因为每个cgroup子系统可能有多个特有的struct cftype[]数组,比如blkio cgroup子系统的struct cftype blkcg_files[]和 struct cftype throtl_files[],每来一个struct cftype[]数组都添加到struct cgroup_subsys的成员cftsets链表上即可。struct cftype_set的成员struct cftype *cfts成员指向具体的struct cftype[]数组。下图更详细的描述了cgroup控制文件cftype错综复杂的关系。

1  struct cftype和cfent一一对应,表示cgroup控制文件:"tasks"、release_agent、cfs_quota_us等,struct cfent靠其成员node添加到struct cgroup结构的files链表。

2  每个cgroup子系统cgroup_subsys都有一个基础cftype_set,就在cgroup_subsys结构体里。在cgroup_add_cftypes()还会再分配新的cftype_set。每个cftype_set都靠其成员node添加到cgroup_subsys的cftsets链表。

3  struct cftype_set的成员struct cftype *cfts指向cgroup子系统特有的cftype数组。主要有两类,正是前文提到的“struct cftype主要有3类”的2、3两类。

3.2 cgruop目录下文件的读写

前一节讲解了cgroup目录和文件的创建过程,那这些文件的读写是怎么进行的呢?比如前文介绍的这些命令

  1. [root@localhost cpu]# mkdir test
  2. [root@localhost test]# cat cpu.cfs_quota_us
  3. -1
  4. [root@localhost test]# cat cpu.cfs_period_us
  5. 100000
  6. [root@localhost test]# echo 50000 > cpu.cfs_quota_us

cpu.cfs_quota_us这个文件是怎么读写的?

  1. static struct cftype cpu_files[] = {
  2. {
  3. .name = "cfs_quota_us",//cfs进程配额
  4. .read_s64 = cpu_cfs_quota_read_s64,
  5. .write_s64 = cpu_cfs_quota_write_s64,
  6. },
  7. {
  8. .name = "cfs_period_us",//cfs进程运行周期
  9. .read_u64 = cpu_cfs_period_read_u64,
  10. .write_u64 = cpu_cfs_period_write_u64,
  11. },
  12. }

echo 10000 > cpu.cfs_period_us的内核过程是sys_write()->vfs_write()->cgroup_file_write()-> cpu_cfs_period_write_u64()。可以发现先由vfs层调到cgroup文件系统的cgroup_file_write()函数,

  1. static ssize_t cgroup_file_write(struct file *file, const char __user *buf,
  2. size_t nbytes, loff_t *ppos)
  3. {
  4. //通过文件dentry得到struct cfent,再由struct cfent的type得到文件的struct cftype。struct cftype包含了该cgroup文件的读写控制函数,如echo设置进程运行时间要调用的write函数
  5. struct cftype *cft = __d_cft(file->f_dentry);
  6. //通过父目录的dentry获取父目录对应的struct cgroup结构体,这个struct cgroup就代表了这个目录
  7. struct cgroup *cgrp = __d_cgrp(file->f_dentry->d_parent);
  8. //根据情况调用struct cftype *cft的write函数。echo 50000 > cpu.cfs_quota_us设置进程运行时间配额最终调用的write函数是 cpu_cfs_quota_write_s64()
  9. if (cft->write)
  10. return cft->write(cgrp, cft, file, buf, nbytes, ppos);
  11. if (cft->write_u64 || cft->write_s64)
  12. return cgroup_write_X64(cgrp, cft, file, buf, nbytes, ppos);
  13. if (cft->write_string)
  14. return cgroup_write_string(cgrp, cft, file, buf, nbytes, ppos);
  15. ......
  16. }

cpu_cfs_quota_write_s64函数源码如下:

  1. static int cpu_cfs_quota_write_s64(struct cgroup *cgrp, struct cftype *cftype,
  2. s64 cfs_quota_us)
  3. {
  4. return tg_set_cfs_quota(cgroup_tg(cgrp), cfs_quota_us);
  5. }
  6. //设置进程组struct task_group的运行时间配额
  7. int tg_set_cfs_quota(struct task_group *tg, long cfs_quota_us)
  8. {
  9. u64 quota, period;
  10. //进程组的运行时间
  11. period = ktime_to_ns(tg->cfs_bandwidth.period);
  12. if (cfs_quota_us < 0)
  13. quota = RUNTIME_INF;
  14. else
  15. quota = (u64)cfs_quota_us * NSEC_PER_USEC;
  16. return tg_set_cfs_bandwidth(tg, period, quota);
  17. }
  18. static int tg_set_cfs_bandwidth(struct task_group *tg, u64 period, u64 quota)
  19. {
  20. struct cfs_bandwidth *cfs_b = &tg->cfs_bandwidth;
  21. //进程组的运行周期和时间配额设置到进程组的struct task_group的cfs_bandwidth
  22. cfs_b->period = ns_to_ktime(period);
  23. cfs_b->quota = quota;
  24. }

可以发现,根据传入的struct cgroup结构,找到进程绑定cpu cgroup 子系统实际的struct task_group控制结构,struct  task_group保存了进程运行的时间周期、时间配额等数据。在进程调度时,如果进程运行的时间超出了struct  task_group配置的时间配额,就被迫让出CPU使用权,内核选择新的进程运行。

每个cgroup子系统都有自己的控制结构,实现特定的功能限制。比如,进程绑定到memory cgorup子系统,对应struct mem_cgroup控制结构,保存了进程的内存分配上限,当进程分配的内存超出限制,内核会oom kill该进程;如果进程绑定到了blkio cgroup子系统,对应struct blkcg控制结构,用于限制进程的内核block层的IOPS。

cat cpu.cfs_period_us读取的内核过程是:sys_read()->vfs_read()->cgroup_file_read()->cpu_cfs_period_read_u64(), 源码不再列了。

3 进程绑定到cgroup及css_set详解

3.1 css_set与各个数据结构的关系

比如前文介绍的执行如下命令,将进程1绑定到cpu cgroup子系统,之后进程1的CPU使用率被限制最大50%。

  1. [root@localhost cpu]# mkdir test//创建cgroup目录
  2. [root@localhost test]# cat cpu.cfs_quota_us
  3. -1
  4. [root@localhost test]# cat cpu.cfs_period_us
  5. 100000
  6. [root@localhost test]# echo 50000 > cpu.cfs_quota_us
  7. [root@localhost test]# echo 进程1的ID > tasks

上一节介绍过,echo 50000 > cpu.cfs_quota_us,实际是把50000“这个进程运行时间配额值”设置到“test”这个cgroup目录对应的struct cgroup的struct  task_group中。“echo 进程1的ID > tasks”命令把进程1绑定到“test”这个cgroup目录中,之后进程1的运行时间便受该cgroup的限制。什么意思?比如进程1的函数代码是while(1);死循环,正常情况CPU使用率是100%,现在只能到50%。因为进程1绑定到“test”这个cgroup目录,该cgroup限制进程的运行时间是周期的一半,即50000/100000,100000代表进程运行周期,50000是进程在该时间内的运行时间配额。

再啰嗦一点,centos 7.6系统,每个进程默认都绑定了每个cgroup子系统的顶层目录cgroup。什么意思?举个例子,如下:

  1. [root@localhost cgroup]# sleep 999999&
  2. [1] 64056 //新创建的进程PID是64056
  3. [root@localhost cgroup]# cat cpu/tasks | grep 64056
  4. 64056
  5. [root@localhost cgroup]# cat memory/tasks | grep 64056
  6. 64056
  7. [root@localhost cgroup]# cat blkio/tasks | grep 64056
  8. 64056

所以,当我们再执行“echo 64056 > cpu/test/tasks”把进程绑定“test”这个新的cgroup目录时,只是从cpu cgroup顶层目录转移到“test”这个cgroup目录而已。

既然每个进程都绑定了一个默认的cgroup子系统的某个层级的cgroup目录,并且每个进程可以绑定到每个cgroup子系统的每个cgroup目录。比如你可以创建N个进程都绑定到cpu cgroup“test”目录,也可以保持默认的cgroup绑定关系。当系统又上万个进程,每个进程按照自己的需求,随意绑定到cpu、memory、blkio等cgorup子系统任一个目录,这错综复杂的关系该怎么描述呢?准确说,怎么建立进程、进程绑定的所有cgroup子系统的cgroup目录的关系呢?这里引入了一个新的数据结构,struct css_set。为了理解方便,这里把前文截图有关css_set结构关系图单独贴下:

首先,进程唯一的struct task_struct结构,这个没啥说的;代表cgroup子系统的某层cgroup目录的struct cgroup结构;剩下的是两个中间牵线搭桥结构struct cg_cgroup_link和struct css_set。

1 struct task_struct通过其成员struct list_head cg_list添加到struct css_set的成员struct list_head tasks这个链表

2 struct cg_cgroup_link通过其成员struct cg_cgroup_link添加到struct css_set的成员struct list_head cg_links这个链表

3 struct cg_cgroup_link通过其成员struct list_head cgrp_link_list添加到struct cgroup的成员struct list_head css_sets这个链表

4 struct cg_cgroup_link的成员struct css_set *cg指向对应的struct css_set结构

5 struct cg_cgroup_link的成员struct cgroup *cgrp指向对应的struct cgroup结构

显然通过这些链接关系,任一个进程,知道了struct task_struct,就可以直到它都绑定了哪些cgroup子系统的哪些cgroup目录(准确说是知道了struct cgroup结构);同理,知道了一个cgroup目录的struct  cgroup结构,就可以找到都哪些进程(准确说是知道了struct  task_struct结构)绑定到了这个cgroup目录。

显然,struct task_struct可以通过struct cg_cgroup_link和struct css_set找到struct cgroup。反过来struct  cgroup也可以通过struct cg_cgroup_link和struct css_set找到struct task_struct。这些错综复杂的关系是怎么建立的呢?是在进程” echo 64056 > cpu/test/tasks”绑定到cgroup目录完成的,内核过程是:sys_write->vfs_write->cgroup_file_write->cgroup_tasks_write->attach_task_by_pid->cgroup_attach_task,重点正是在cgroup_attach_task函数。

3.2  cgroup_attach_task函数讲解

先把重点源码列下

//进程绑定到cgroup,一个进程可以绑定多个cgroup,比如cpu、memcory、blkio

  1. static int cgroup_attach_task(struct cgroup *cgrp, struct task_struct *tsk,
  2. bool threadgroup)
  3. {
  4. int retval, i, group_size;
  5. struct cgroup_subsys *ss, *failed_ss = NULL;
  6. struct cgroupfs_root *root = cgrp->root;//代表cgroup子系统对应的文件系统
  7. struct task_struct *leader = tsk;
  8. struct task_and_cgroup *tc;
  9. struct flex_array *group;
  10. struct cgroup_taskset tset = { };
  11. /*分配struct flex_array,顺带着分配内存,下边执行flex_array_put()向group指向的struct flex_array填充数据,然后在下边通过flex_array_get()从group指向的struct flex_array取出数据*/
  12. group = flex_array_alloc(sizeof(*tc), group_size, GFP_KERNEL);
  13. retval = flex_array_prealloc(group, 0, group_size, GFP_KERNEL);
  14. do {
  15. struct task_and_cgroup ent;
  16. //ent.task保存待绑定进程的task_struct结构
  17. ent.task = tsk;
  18. /*找到要绑定的进程task_struct的struct css_set,依次找到css_set的cg_links链表上挂着的struct cg_cgroup_link,看哪个struct cg_cgroup_link对应的struct cgroup与本次绑定的struct cgroup属于同一个cgroup子系统。函数返回的就是从cg_cgroup_link找到的struct cgroup,其实就是找到返回该进程之前绑定的struct cgroup吧*/
  19. ent.cgrp = task_cgroup_from_root(tsk, root);
  20. /*把struct task_and_cgroup ent保存到group指向的struct flex_array,ent包含了本次待绑定的进程task_struct和该进程之前绑定的struct cgroup*/
  21. retval = flex_array_put(group, i, &ent, GFP_ATOMIC);
  22. }while_each_thread(leader, tsk);
  23. /*tset.tc_array指向新分配的struct flex_array *group,group指向struct flex_array,flex_array里保存了struct task_and_cgroup ent,ent包含了本次待绑定的进程task_struct和该进程之前绑定的struct cgroup*/
  24. tset.tc_array = group;
  25. tset.tc_array_len = group_size;//group_size是本次要绑定进程的线程数
  26. //group_size是本次要绑定进程的线程数,这是对本次绑定进程的每个线程依次处理
  27. for (i = 0; i < group_size; i++) {
  28. /*从group指向的struct flex_array,找到并返回前边保存的struct task_and_cgroup给tc。之后tc包含了本次待绑定的进程/线程的task_struct和该进程/线程之前绑定的struct cgroup。说准确点,tc->cgrp是该进程/线程之前绑定的struct cgroup,tc->task是本次要绑定的进程/线程task结构*/
  29. tc = flex_array_get(group, i);
  30. /*找到已经存在的struct css_set直接返回。否则,分配新的struct css_set *res和struct cg_cgroup_link。然后建立新分配的struct css_set *res、新分配的struct cg_cgroup_link、tc->task->cgroups链表上原有的struct cgroup或者本次要建立绑定的struct cgroup *cgrp,三者相互的联系。就是struct css_set、struct cg_cgroup_link、struct cgroup三者关系的建立*/
  31. tc->cg = find_css_set(tc->task->cgroups, cgrp);
  32. }
  33. for (i = 0; i < group_size; i++) {
  34. /*从group指向的struct flex_array,找到并返回前边保存的struct task_and_cgroup给tc。tc->cgrp是该进程之前绑定的struct cgroup,tc->task是本次要绑定的进程task结构,tc->cg是本次绑定操作新分配的struct css_set*/
  35. tc = flex_array_get(group, i);
  36. /*前边建立了struct css_set、struct cg_cgroup_link、struct cgroup的关系,这是建立待绑定进程的struct task_struct结构与绑定的struct css_set的相互联系*/
  37. cgroup_task_migrate(tc->cgrp, tc->task, tc->cg);
  38. }
  39. for_each_subsys(root, ss) {
  40. /*调用该cgroup子系统struct cgroup_subsys的atach函数,cpu 子系统时是cpu_cgroup_attach()。进程绑定了新的cpu cgroup时,要从之前cpu cgroup清理干净,然后设置进程在新的进程组的关系*/
  41. if (ss->attach)
  42. ss->attach(cgrp, &tset);
  43. }
  44. }

cgroup_attach_task函数的处理未免太过啰嗦,核心点只有几个

  1. 分配struct task_and_cgroup *tc结构,执行下边的do...while_each_thread(leader, tsk)把进程及其线程的task_struct、struct old css_set、struct old cgroup信息保存到struct task_and_cgroup。该函数经常从struct task_and_cgroup取出这些信息。
  2. 循环执行find_css_set(),按照进程或者线程之前绑定的old css_set、本次要绑定的进程的struct cgroup *cgrp,是否有匹配的css_set,有的话直接返回这个css_set。没有找到匹配的css_set,则分配新的css_set,分配12新的struct cg_cgroup_link,把old css_set上之前进程绑定的cgroup目录struct cgroup和本次进程要绑定的cgroup目录struct cgroup *cgrp(一共12个),按照他们所属于的cgorup子系统编号先添加到struct cg_cgroup_link,再把struct cg_cgroup_link添加到新的css_set的cg_links链表。总之就建立了新的css_set和新的cgorup的关系。
  3. 循环执行执行cgroup_task_migrate(tc->cgrp, tc->task, tc->cg),建立进程或者线程task_struct与css_set的关系

比较重要的是find_css_set()函数:找到已经存在的struct css_set直接返回。否则,分配新的struct css_set *res和root_count个struct cg_cgroup_link建立新分配的struct css_set *res、新分配的struct cg_cgroup_link、struct css_set *oldcg链表上原有的struct cgroup或者本次建立绑定的struct cgroup *cgrp,三者相互的联系。

  1. static struct css_set *find_css_set(struct css_set *oldcg, struct cgroup *cgrp)
  2. {
  3. struct css_set *res;
  4. struct cgroup_subsys_state *template[CGROUP_SUBSYS_COUNT];//template
  5. /*如果找到已经存在的struct css_set,我觉得struct css_set *oldcg是存在的,这是试图找到与本次绑定的struct cgroup *cgrp的匹配的struct css_set,新创建的应该都不会成立吧*/
  6. res = find_existing_css_set(oldcg, cgrp, template);
  7. if (res)
  8. return res;
  9. //分配struct css_set
  10. res = kmalloc(sizeof(*res), GFP_KERNEL);
  11. //分配root_count(应该是12)个struct cg_cgroup_link并添加到struct list_head tmp_cg_links链表,
  12. allocate_cg_links(root_count, &tmp_cg_links) < 0);
  13. //把struct cgroup_subsys_state *template[CGROUP_SUBSYS_COUNT]里所有的cgroup_subsys_state指针都复制到res->subsys[]数组
  14. memcpy(res->subsys, template, sizeof(res->subsys));
  15. /*遍历struct css_set *oldcg的cg_links链表上的已经存在的struct cg_cgroup_link, 找到其指向的struct cgroup(即link->cgrp),然后struct cgroup *c = link->cgrp,接着把tmp_cg_links链表上的struct cg_cgroup_link移动到struct cgroup c的css_sets链表和struct css_set res的cg_links链表,还令cg_cgroup_link的成员cg和cgrp分别指向struct css_set res和struct cgroup c*/
  16. list_for_each_entry(link, &oldcg->cg_links, cg_link_list) {
  17. struct cgroup *c = link->cgrp;
  18. /*如果遍历到的老的css_set的cgroup与本次进程要绑定的cgorup属于同一个cgorup子系统,要替换成本次进程要绑定cgroup,即c = cgrp。然后下边建立本次进程要绑定cgroup和css_set的关系*/
  19. if (c->root == cgrp->root)
  20. c = cgrp;
  21. /*对tmp_cg_links上的cg_cgroup_link关键成员赋值,cg_cgroup_link的cg指向struct css_set res,cg_cgroup_link的cgrp指向struct cgroup c,后把cg_cgroup_link移动到struct cgroup的css_sets链表,把cg_cgroup_link添加到struct css_set res的cg_links链表。这样就建立了css_set、cgroup、cg_cgroup_link三者联系*/
  22. link_css_set(&tmp_cg_links, res, c);
  23. }
  24. //以struct css_set *res所有struct cgroup_subsys_state *css[]所有的cgroup子系统cgroup_subsys_state指针累加值作为hash key
  25. key = css_set_hash(res->subsys);
  26. /*通过struct css_set *res的成员hlist把css_set添加到css_set_table这个hash树,key是css_set的struct cgroup_subsys_state *css[]所有cgroup_subsys_state指针累加值*/
  27. hash_add(css_set_table, &res->hlist, key);
  28. }

里边重点执行的是find_existing_css_set函数,

  1. static struct css_set *find_existing_css_set(
  2. struct css_set *oldcg,
  3. struct cgroup *cgrp,
  4. struct cgroup_subsys_state *template[])//template[]在这里
  5. {
  6. int i;
  7. struct cgroupfs_root *root = cgrp->root;
  8. struct css_set *cg;
  9. unsigned long key;
  10. /*for循环次数等于cgroup子系统个数(12个)。每次循环取出一个cgroup子系统的cgroup_subsys_state赋值于template[]。大部分都是从oldcg->subsys[i]取cgroup_subsys_state赋于template[i]。只有if (root->subsys_mask & (1UL << i))成立,说明本次循环遍历到了本次进程要绑定的struct cgroup *cgrp对应的cgroup子系统,则从cgrp->subsys[i]取出cgroup_subsys_state赋于template[i]*/
  11. for (i = 0; i < CGROUP_SUBSYS_COUNT; i++) {
  12. /*这轮循环是本次进程要绑定的struct cgroup *cgrp对应的cgroup子系统,从本次进程要绑定的cgroup获取对应的cgroup_subsys_state
  13. if (root->subsys_mask & (1UL << i)) {
  14. template[i] = cgrp->subsys[i];
  15. } else {
  16. //从old cgroup获取对应的cgroup_subsys_state
  17. template[i] = oldcg->subsys[i];
  18. }
  19. }
  20. /*hash key是template[]里所有的cgroup_subsys_state指针。此时template[]里的cgroup_subsys_state结合了老的oldcg和新的cgrp这两个cgroup的struct cgroup->subsys[]里所有的cgroup_subsys_state*/
  21. key = css_set_hash(template);
  22. //遍历所有的css_set_table hash链表上的css_set,
  23. hash_for_each_possible(css_set_table, cg, hlist, key) {
  24. if (!compare_css_sets(cg, oldcg, cgrp, template))
  25. continue;
  26. return cg;
  27. }
  28. return NULL;
  29. }

find_existing_css_set()是结合进程之前绑定的oldcg和本次绑定的cgroup目录cgrp,在css_set_table链表找到一个匹配的css_set,找到则返回css_set,否则返回NULL。查找规则是什么呢?我们把find_existing_css_set函数拆解成3步分详细说说。

1在for (i = 0; i < CGROUP_SUBSYS_COUNT; i++)循环那里使用cgrp->subsys[i]和oldcg->subsys[i]的cgroup_subsys_state填充template[CGROUP_SUBSYS_COUNT]数组。除了for循环遍历到了本次进程要绑定的struct cgroup *cgrp对应的cgroup子系统,是从cgrp->subsys[i]取出cgroup_subsys_state赋于template[i],即template[i] = cgrp->subsys[i]。其他都是template[i] = oldcg->subsys[i]

2  之后template[i]的cgroup_subsys_state就结合了老css_set和本次要绑定进程的cgroup的cgroup_subsys_state,以template[i]为key在css_set_table链表查找匹配的css_set,这里称为css_set_new。css_set_new->subsys[i]和template[i]的cgroup_subsys_state应该完全一样,毕竟是以template[i]的cgroup_subsys_state为key在css_set_table链表找到的css_set_new。

3  接着执行compare_css_sets()循环遍历css_set_new和struct css_set *oldcg的cg_links链表上的cg_cgroup_link指向的cgroup,简单说就是css_set绑定的struct cgroup而已。然后结合本次进程要绑定的struct cgroup *cgrp,判断3者是否相等。具体规则是:每次循环,遍历到css_set_new和struct css_set *oldcg上的cgroup如果不相等,直接返回false ; 如果本次循环从css_set_new遍历到的cgroup与本次进程要绑定的struct cgroup *cgrp都属于同一个cgroup子系统(cgroup->cgroupfs_root相等),但是两个cgroup不相等,说明不是同一个cgroup目录,返回false。如果经过前边的判断全都不成立,则返回true。这说明css_set_new就是本次进程绑定的css_set。

compare_css_sets函数源码如下:

  1. static bool compare_css_sets(struct css_set *cg,
  2. struct css_set *old_cg,
  3. struct cgroup *new_cgrp,
  4. struct cgroup_subsys_state *template[]
  5. {
  6. //l1和l2指向css_set的 struct list_head cg_links
  7. l1 = &cg->cg_links;
  8. l2 = &old_cg->cg_links;
  9. while (1) {
  10. struct cg_cgroup_link *cgl1, *cgl2;
  11. struct cgroup *cg1, *cg2;
  12. //l1 = l1->next赋值后,l1才指向struct cg_cgroup_link的cg_link_list成员,然后list_entry()再找到struct cg_cgroup_link。l2同理
  13. l1 = l1->next;//下边list_entry()由l1指向的cg_link_list得到cg_cgroup_link
  14. l2 = l2->next;//下边list_entry()由l2指向的cg_link_list得到cg_cgroup_link
  15. BUG_ON(cg1->root != cg2->root);
  16. //由l1和l2指向的cg_link_list分别得到各自的cg_cgroup_link
  17. cgl1 = list_entry(l1, struct cg_cgroup_link, cg_link_list);
  18. cgl2 = list_entry(l2, struct cg_cgroup_link, cg_link_list);
  19. //再由cg_cgroup_link得到cgroup
  20. cg1 = cgl1->cgrp;
  21. cg2 = cgl2->cgrp;
  22. /*cg1->root == new_cgrp->root,说明本次循环遍历到的css_set_table链表上的css_set的cg_links链表上的cgroup和本次的new_cgrp属于同一个cgroup子系统.那就再看看这两个cgroup是否是cgroup目录(即if(cg1 != new_cgrp)),如果不是同一个cgroup,返回flase*/
  23. if (cg1->root == new_cgrp->root) {
  24. if (cg1 != new_cgrp)
  25. return false;
  26. } else {
  27. //其他情况,css_set_table链表上的css_set的cg_links链表上的cgroup和old_cg的cg_links链表上的cgroup不相等,直接返回false
  28. if (cg1 != cg2)
  29. return false;
  30. }
  31. }
  32. return true;
  33. }

compare_css_sets()循环遍历struct css_set *cg和struct css_set *oldcg的cg_links链表上的cg_cgroup_link指向的cgroup,简单说就是css_set绑定的struct cgroup而已。然后结合本次进程要绑定的struct cgroup *cgrp,判断3者是否相等。具体规则是:每次循环,遍历到struct css_set *cg和struct css_set *oldcg上的cgroup如果不相等,直接返回false ; 如果本次循环从struct css_set *cg遍历到的cgroup与本次进程要绑定的struct cgroup *cgrp都属于同一个cgroup子系统(cgroup->cgroupfs_root相等),但是两个cgroup不相等,说明不是同一个cgroup目录,返回false。如果经过前边的判断全都不成立,则返回true。这说明struct css_set *cg就是本次进程要绑定的css_set。

每一个进程绑定的css_set的 cg_links链表上,一定有12个struct cg_cgroup_link,对应12个cgroup子系统的cgroup目录。这些12个cgroup子系统的cgroup目录是按照cgroup子系统的编号顺序排列在css_set的 cg_links链表上。compare_css_sets函数的for循环就是取出struct css_set *cg和struct css_set *old_cg这两个css_set的cg_links链表的struct cg_cgroup_link对应的cgorup目录结构的struct cgroup,由于cgroup是按照cgroup子系统编号顺序排列在css_set的 cg_links链表上,所以每轮循环从struct css_set *cg和struct css_set *old_cg取出的struct cgroup一定属于同一个cgroup子系统,所以BUG_ON(cg1->root != cg2->root)一定不成立。并且,这两个css_set肯定都只有12个cgroup,所以BUG_ON(l2 != &old_cg->cg_links)也不成立。但是每轮循环从struct css_set *cg和struct css_set *old_cg取出的struct cgroup cgroup目录结构不一定一样,因为对应的两个进程绑定的cgroup目录不一定一样,这样就匹配失败。

是不是感觉css_set很复杂,我一次看的时候相当迷茫,其实这个问题从全局反而不容易陷入代码泥潭。

3.3 css_set更深层次的解释

进程"echo 进程ID >cpu/tasks"绑定cgroup目录,执行函数cgroup_attach_task->find_css_set->find_existing_css_set->compare_css_sets(),如果找到匹配的css_set,直接把进程task_struct与css_set建立关系即可。如果找不到就要分配新的css_set、struct cg_cgroup_link,然后用进程之前绑定的old css_set的cg_links链表上的cg_cgroup_link对应的cgroup结构以及本次绑定的cgorup目录结构,建立3者的关系。最后,建立进程task_stuct与新的css_set的关系,步骤如下:

1    首先是find_existing_css_set函数里,向css_set的subsys[i]数组保存cgroup_subsys_state:进程绑定cgroup目录时执行到cgroup_attach_task->find_css_set->find_existing_css_set()函数,该函数里执行template[i] = cgrp->subsys[i]。cgrp是本次进程要绑定的cgroup目录结构,i是本次进程要绑定的cgroup目录对应的cgroup子系统编号,cgrp->subsys[i]就是cgroup目录对应的cgroup_subsys_state。后边在find_css_set()里会把template[i]的所有cgroup_subsys_state复制到css_set的subsys[i]数组,下边有讲。所以css_set->subsys[]里的cgroup_subsys_state来自进程要绑定的cgroup目录对应的cgroup控制结构(比如cpu cgroup子系统的struct task_group)的cgroup_subsys_state成员。如果一个进程没有绑定cgroup目录,那对应css_set->subsys[i]里的cgroup_subsys_state都是从父进程继承的默认的cgroup_subsys_state。之后进程每绑定一个cgroup目录,就要把这个cgroup目录对应的cgroup控制结构(比如cpu cgroup子系统的struct task_group)的成员cgroup_subsys_state按照该cgroup子系统编号保存到css_set->subsys[i],i是cgroup子系统编号。

2  如果在find_css_set->find_existing_css_set()中找到了进程要绑定css_set则直接返回该css_set,然后在cgroup_attach_task()将进程的task_struct结构绑定到返回的css_set即可(见2.6)。如果没有找到要绑定的css_set,则find_css_set->find_existing_css_set()返回NULL,这种情况很复杂,需要分配新的css_set。并且要把进程之前绑定的old css_set的成员cg_links链表上的cg_cgroup_link指向的所有cgroup结构迁移到新的css_set,说到底就是要把进程之前绑定的所有其他cgroup子系统的cgroup目录结构转移到新的css_set。这些是在find_css_set()函数后期执行的,步骤是:

2.1 执行 struct css_set *res = kmalloc(sizeof(*res), GFP_KERNEL)分配新的css_set
    2.2 执行 allocate_cg_links(root_count, &tmp_cg_links) 为新的css_set分配新的struct cg_cgroup_link
   2.3 执行 memcpy(res->subsys, template, sizeof(res->subsys))把template[]所有的cgroup_subsys_state复制到struct css_set *res的subsys[]数组。
  2.4 执行 link_css_set(&tmp_cg_links, res, c) 建立新的css_set、新的struct cg_cgroup_link、进程之前绑定的old css_set的cg_links链表上的cg_cgroup_link对应的cgroup结构以及本次绑定的cgorup目录结构,三者的关系。
   2.5 执行 key = css_set_hash(res->subsys) 和 hash_add(css_set_table, &res->hlist, key),以新的css_set的subsys[]保存的cgroup_subsys_state为key,把新的css_set加入css_set_table链表。每个css_set都是以这种形式加入到css_set_table链表,将来也是按照同样方法计算css_set的key,然后从css_set_table链表链表找到对应的css_set。
   2.6 接着从cgroup_attach_task->find_css_set()返回到cgroup_attach_task函数,执行cgroup_task_migrate(tc->cgrp, tc->task, tc->cg)建立本次要绑定cgroup目录的进程的task_struct结构与新的css_set的关系。

css_set 的存在意义到底是什么?

css_set的存在就是为了记录进程绑定的所有的cgroup目录结构,一个进程可以绑定到cpu、blkio、memory等12个cgroup子系统。准确说,一个新创建的进程默认就绑定了12个cgroup子系统,对应1个struct css_set,12个struct cg_cgroup_link,12个struct cgroup,这12个struct cgroup按照他们的cgroup子系统编号顺序链入struct cg_cgroup_link,struct cg_cgroup_link再链入struct css_set的cg_links成员。所以说,css_set的cg_links的链表上的cg_cgroup_link对应的sturct cgroup,第一个的cgroup子系统编号是0,第2个cgroup子系统编号是1,其他类推。总之这些struct cgroup就是按照cgroup子系统编号排列的。

之后进程1绑定cpu、blkio、memory等新的cgroup目录时(比如cpu/test/tasks),需要分配新一个css_set,12个struct cg_cgroup_link。然后把进程之前绑定old css_set的cg_links的链表上的cg_cgroup_link对应的sturct cgroup和本次进程要绑定的新cgroup目录的struct cgroup (碰到同一个cgroup子系统的struct cgroup,要踢掉old css_set的这个struct croup,而使用本次要绑定的新cgroup目录的struct cgroup),按照cgroup子系统编号依次转移到新分配的12个struct cg_cgroup_link上,这12个struct cg_cgroup_link再按照顺序链入新分配的css_set的cg_links链表。这个过程就对应find_css_set->find_existing_css_set()没有找到匹配css_set的情况。

然后再有创建的进程2,把它绑定也绑定到"cpu/test/tasks"。此时进程1和进程2绑定的cgroup子系统和cgroup目录完全一样,直接找到了上一次进程绑定"cpu/test/tasks"时分配css_set直接返回即可。这个过程就对应find_css_set->find_existing_css_set()找到匹配css_set的情况。

进程绑定的css_set有什么规律?

  1. 1 进程绑定cgroup时,一定是从一个老的cgroup转移到新的cgroup,进程的cgroup信息继承父进程的,原始父进程绑定的有默认的cgroup。
  2. 2 两个进程绑定的cgroup层级、cgroup子系统、cgroup目录完全一致时,二者绑定的css_set是同一个。进程1绑定cpu/tasks和memory/tasks,进程2绑定cpu/tasks和memory/test/tasks,二者绑定的css_set不相同。
  3. 3 一个进程只会绑定一个see_set,不管绑定多少个cgroup子系统、绑定到哪个cgroup目录,无非绑定的css_set变来变去。并且进程绑定一个cgroup目录后,它绑定的css_set的subsys[]数组要保存该cgroup目录对应的cgroup控制结构(比如cpu cgroup子系统的struct task_group)的成员cgroup_subsys_state地址,保存在subsys[]数组什么位置呢?由该cgroup目录对应的cgroup子系统编号决定。

为了能彻底的解释清楚,举个例子,重点来了。

  • 进程1绑定"cpu/tasks"(cgroup目录是cgroup1)和"memory/tasks"(cgroup目录是cgroup2),绑定的css_set是css_set1。
  • 进程2绑定"cpu/tasks"(cgroup目录是cgroup1)和"memory/test/tasks"(cgroup目录是cgroup3),绑定的css_set是css_set2。
  • css_set1->subsys[12]={... ,cgroup1对应的cgroup_subsys_state, ... ,cgroup2对应的cgroup_subsys_state,}
  • css_set2->subsys[12]={... ,cgroup1对应的cgroup_subsys_state, ... , cgroup3对应的cgroup_subsys_state,}

这两个css_set都以css_set->subsys[]的cgroup_subsys_state指针为key加入到css_set_table链表。

继续,进程2改为绑定"memory/tasks",执行到cgroup_attach_task->find_css_set->find_existing_css_set()函数,

for (i = 0; i < CGROUP_SUBSYS_COUNT; i++)中对template[i]赋值,赋值后是

  • template[12]={...,cgroup1对应的cgroup_subsys_state,...,cgroup2对应的cgroup_subsys_state,}

然后执行key = css_set_hash(template),以template[12]里边12个cgroup子系统的cgroup_subsys_state指针为key,在css_set_table链表找到css_set1。然后执行compare_css_sets(css_set1, css_set2, cgroup2, template)函数,进行匹配校验。主要匹配两点:

1  循环从css_set_table上找到css_set,找到css_set1时,从css_set1和进程2之前绑定的css_set2的cg_links的链表上的取出cg_cgroup_link,再得到cg_cgroup_link对应的struct cgroup,比较两个struct cgroup是否相等(实际是按照cgroup子系统的编号成对比较),比较11次(一共12 cgroup子系统),有一对不相等返回false。

2 在第一步的基础上,还有1次就是从css_set1取出的struct cgroup与进程2本次要绑定的"memory/tasks"的cgroup2属于同一个cgroup子系统,则要判断两个struct cgroup是否相等,不相等返回false。这些判断都通过,说明从css_set_table上找到的css_set1,就是进程绑定"memory/tasks"cgroup目录要绑定的css_set,css_set匹配成功。然后回到cgroup_attach_task函数,执行cgroup_task_migrate()把进程2的task_struct绑定到css_set1,暂时完工。

如果进程2改为绑定"memory/test2/tasks"(cgroup目录是cgroup5),执行到cgroup_attach_task->find_css_set->find_existing_css_set()函数,同样执行到for (i = 0; i < CGROUP_SUBSYS_COUNT; i++)中对template[i]赋值,赋值后是

template[i]={...,cgroup1对应的cgroup_subsys_state,...,cgroup5对应的cgroup_subsys_state,}

显然find_existing_css_set()中找不到匹配的css_set则返回NULL。然后回到find_css_set(),分配新的css_set3,分配12个struct cg_cgroup_link,再取出进程2之前绑定的css_set2的成员cg_links的链表上的cg_cgroup_link对应的struct cgroup(一共12个),依次把这12个struct cgroup按照cgroup子系统编号先加入cg_cgroup_link,再把cg_cgroup_link加入到css_set3的cg_links链表。

添加过程如果碰到struct cgroup与进程2要绑定"memory/test2/tasks" (cgroup目录是cgroup5)一致,是要把cgroup5添加到cg_cgroup_link链表,再把cg_cgroup_link加入到css_set3的cg_links链表。这个过程在find_css_set()函数的如下代码完成

  1. list_for_each_entry(link, &oldcg->cg_links, cg_link_list)
  2. {
  3. //如果遍历到的老的css_set的cgroup与本次进程要绑定的cgorup属于同一个cgorup子系统,要替换成本次进程要绑定cgroup,然后下边建立本次进程要绑定cgroup5、css_set3的关系
  4. if (c->root == cgrp->root)
  5. c = cgrp;
  6. //设置进程task_struct结构与css_set的关系
  7. link_css_set(&tmp_cg_links, res, c);
  8. }

最后回到cgroup_attach_task函数,执行cgroup_task_migrate()把进程2的task_struct绑定到css_set3,完工。

     如图,进程1、进程2、进程3都绑定到了cpu子系统的/sys/fs/cgroup/cpu/test1目录和memory子系统的/sys/fs/cgroup/memory/test1目录,3者绑定的其他的cgroup子系统的cgroup目录都是默认的的根目录,总计12个cgroup子系统。3个进程绑定的css_set都是css_set1。正如前文所说,centos 7.6系统,每个新创建的进程默认都绑定到了12个cgroup子系统的根目录,之后进程绑定到某个新的cgroup目录,无非是从老的cgroup目录移动到新的,比如进程1默认绑定cpu子系统的/sys/fs/cgroup/cpu这个cgroup目录,现在绑定到了/sys/fs/cgroup/cpu/test1这个cgroup目录。

现在进程1改变cpu子系统的绑定目录,由” /sys/fs/cgroup/cpu/test1”改为” /sys/fs/cgroup/cpu/test2”,它绑定的css_set变为css_set2。进程2和进程3绑定的cgroup目录不变,二者绑定的css_set还是css_set1。前文也提过,一个进程绑定的cgroup目录只要有一个发生变化,它绑定的css_set就要变。

两个进程绑定的cgroup子系统和cgroup目录只有完全一样,它们绑定的css_set才是同一个。struct css_set结构的成员struct cgroup_subsys_state *subsys[]保存的它对应的12个cgroup子系统的cgroup目录对应的具体控制结构(cpu子系统的是struct  task_group)的成员struct cgroup_subsys_state css的地址。struct  task_group、struct cgroup_subsys_state css、cgroup目录对应的struct cgroup  三者一一对应,在创建目录时分配。所以说,css_set的struct cgroup_subsys_state *subsys[]保存的其实是每个cgroup目录的唯一信息,进程绑定的cgroup目录完全一样,绑定的css_set就是同一个。

css_set以其成员struct cgroup_subsys_state *subsys[]保存的所有cgroup_subsys_state指针累计为hash key,保存在css_set_table链表。一个进程绑定一个新的cgroup目录时,以绑定的cgroup目录对应的cgroup_subsys_state指针加上绑定的其他cgroup子系统cgroup目录对应的cgroup_subsys_state指针,在css_set_table链表查找匹配css_set。如果之前已经有进程绑定的cgroup目录与这个进程本次绑定的完全一致,则直接返回css_set即可,否则就要分配新的css_set。好了,感觉说的太啰嗦了,上边的示意图说明的比较充分。

参考

https://www.cnblogs.com/acool/p/6852250.html

http://blog.chinaunix.net/uid-20543183-id-1930840.html

https://www.cnblogs.com/lisperl/archive/2012/04/18/2455027.html

https://tech.meituan.com/2015/03/31/cgroups.html

https://www.cnblogs.com/acool/p/6882644.html

cgroup使用举例和linux内核源码详解相关推荐

  1. page_to_pfn 、virt_to_page、 virt_to_phys、page、页帧pfn、内核虚拟地址、物理内存地址linux内核源码详解

    首先说说内核态虚拟地址和物理内存地址转换关系 #define PAGE_OFFSET     UL(0xffffffc000000000) /* PHYS_OFFSET - the physical ...

  2. ext4 extent详解2之内核源码详解

    在查看本文前,希望先查看<ext4 extent详解1之示意图演示>这篇文章,有助于理解本文.本文内核源码版本3.10.96,详细内核详细源码注释见https://github.com/d ...

  3. Redhat7.2上编译Linux内核源码

    下载linux源码包:https://git.kernel.org/pub/scm/virt/kvm/kvm.git/snapshot/kvm-4.17-1.tar.gz (这是包含kvm开发版本的l ...

  4. 深入分析Linux内核源码oss.org.cn/kernel-book/

    本html页面地址:http://oss.org.cn/kernel-book/ 深入分析Linux内核源码 前言         第一章 走进linux 1.1 GNU与Linux的成长 1.2 L ...

  5. linux的进程/线程/协程系列3:查看linux内核源码——vim+ctags/find+grep

    linux的进程/线程/协程系列3:查看linux内核源码--vim+ctags/find+grep 前言 摘要: 1. 下载linux内核源码 2. 打标签方法:vim+ctags 2.1 安装vi ...

  6. iostat IO统计原理linux内核源码分析----基于单通道SATA盘

    iostat IO统计原理linux内核源码分析----基于单通道SATA盘 先上一个IO发送submit_bio流程图,本文基本就是围绕该流程讲解. 内核版本 3.10.96 详细的源码注释:htt ...

  7. Linux内核源码分析--内核启动之(3)Image内核启动(C语言部分)(Linux-3.0 ARMv7) 【转】...

    原文地址:Linux内核源码分析--内核启动之(3)Image内核启动(C语言部分)(Linux-3.0 ARMv7) 作者:tekkamanninja 转自:http://blog.chinauni ...

  8. Linux内核源码中使用宏定义的若干技巧

    在C中,宏定义的概念虽然简单,但是真要用好却并不那么容易,下面从Linux源码中抽取一些宏定义的使用方法,希望能从中得到点启发: 1. 类型检查 比如module_init的宏定义: 点击(此处)折叠 ...

  9. 【Linux 内核】Linux 内核源码目录说明 ① ( arch 目录 | block 目录 | certs 目录 | crypto 目录 | Documentation 目录 )

    文章目录 一.arch 目录 二.block 目录 三.certs 目录 四.crypto 目录 五.Documentation 目录 在上一篇博客 [Linux 内核]Linux 内核源码结构 ( ...

最新文章

  1. eclipse java8报错_eclipse4.3安装支持Java8插件,之后就报错无法打开eclipse,求解?
  2. 运行Qt release版本时出现“丢失QtCore4.dll”错误
  3. (转载)Linux多线程实现
  4. 对象的序列化流_ObjectOutputStream
  5. 【机器学习】传统目标检测算法之HOG
  6. MySQL主从虚IP_Mysql主从同步时Slave_IO_Running:Connecting ; Slave_SQL_Running:Yes的情况故障排除...
  7. java 分布式同步_Java Web分布式集群搭建(三)——Session同步
  8. MySQL关闭Enterprise Server源码
  9. 大量删除的表、查询卡顿的表,重建索引
  10. 小学生python游戏编程_适合刚入门Python小白的趣味游戏编程
  11. 酒浓码浓 - js / 前端 / 支付宝,微信合并二维码功能
  12. Android基于Ymodem协议升级嵌入式MCU主控
  13. 什么是在网上常说的“丢包”?
  14. 全排列算法(字典序法、SJT Algorithm 、Heap‘s Algorithm)
  15. 学python需要什么软件,python软件有哪些图标
  16. tornado完成一个简单的登录界面/图片的上传
  17. AdGuard免费的电脑手机广告拦截程序
  18. tomcat重启警告:Abandoned connection cleanup thread 服务器宕机解决方案
  19. 数据字典的作用/用途:是数据流程图的补充!4个主要条目:数据项,数据处理(逻辑功能及其算法),数据存储,数据流条目
  20. CIKM 2022 AnalytiCup Competition: 联邦异质任务学习

热门文章

  1. Windows 10/11 官方下载工具 镜像制作U盘启动盘 快速安装
  2. 极速office怎么才能自定义PPT幻灯片大小
  3. java经典题之冒泡排序
  4. c++读取wav音频方法
  5. linux 内核usleep,linux 内核 usleep
  6. tensorflow之constant()函数
  7. com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException异常产生的原因及解决办法
  8. 字符串 转16进制 sscanf
  9. Spring源码研读
  10. java实现如何定时给微信群中发送消息