注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

Tsecer的回音岛

Tsecer的博客

 
 
 

日志

 
 

cgroup实现分析(2)--进程附加与参数调整  

2012-04-21 22:30:55|  分类: Linux内核 |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |
一、cfile
1、cfile的作用和意义
这个结构是为了适配文件系统而引入的概念。大致来收,本真“以文件为本”的思想,cgroup(也包括许多其它的机制,例如伪终端、套接口等)是以文件之名,行组控之实。为了让用户使用最为常见和顺手的文件来调整一个控制组中的参数,每个子系统可以定义自己子系统在一个控制组中可调/可读的参数文件,这些文件就是cfile类型文件。
当一个cgroup被创建之后,cgroup会调用本次挂载中不同子系统的populate接口,这个接口就是让子系统有机会向一个cgroup中移民。关于这个populate,是一个文件系统中比较通用的概念,那就是当文件夹创建之后,邀请不同的系统来这里落户,用现代的说法是“招商引资”,用古代的说法就是“徕民屯田”。在根文件系统创建之后,我们也可以看到这个模式,有兴趣的同学看一下内核中populate_rootfs的实现(其实看一下函数名就可以了)。
从本质上说,这些文件代表了一个子系统特有的控制参数,以文件的形式呈现给用户,从而可以通过通用的文件操作工具(最为原始的echo和cat都能胜任)来完成对参数的调整,这样便于系统管理员来进行动态操作。
2、为什么把cfile放在这里说明
这一节完全是抽风式吐槽,比较赶时间的同学可以自动跳过。这个结构其实应该是放在上一篇作为基础结构来说明的,但是上一篇已经很长了,为了避免读者出现“太长没看完”的症状,我就把它放在这里;另一方面也是因为我写的比较累了,明天还要上班。那在这里再展开一下代码的布局问题。函数和文件都尽量不要太长,因为比较短的静态函数编译器会自动进行内联,你单独提取个函数,以后这个地方就可以方便的调整,扩展性也有了,而且函数名本身也是一个极好的注释,它也是实现代码“自注释”的一个好习惯。
文件也是这样,在glibc中,glibc-2.7\nptl\pthread_create.c文件会包含另外两个源文件文件
/* Code to allocate and deallocate a stack.  */
#include "allocatestack.c"

/* Code to create the thread.  */
#include <createthread.c>
内核中,调度文件同样可以包含其它源文件文件linux-2.6.37.1\kernel\sched.c
#include "sched_idletask.c"
#include "sched_fair.c"
#include "sched_rt.c"
#include "sched_stoptask.c"
这里都是货真价实的源文件包含,也就是说他们在物理上是在同一文件中,这样便于编译器进行优化,事实上,规模越大,就越容易体现编译器的优化能力。就好像活字印刷一样:“若止印三二本,未为简易;若印数十百千本,则极为神速”(这里也顺便体现了软件的“复用”思想)。但是从逻辑上说,把不同逻辑功能的代码独立分开,是比较便于理解和维护的,所以这种源代码的包含是一种折中选择,当然这种实现对Makefile系统的要求是比较高的,对于wildcard形式的粗放式管理,这里会出现大量链接时重定义。
3、cfile和cgroup的交互
在cgroup系统中,一个cgroup是以一个文件夹的形式出现,而它管理的各个子系统的参数则是通过文件(cfile)来体现的,所以“文件夹+文件”结构的一对多关系就是“cgroup+cgroup_subsys”的一对多关系的自然模拟和映射。
cftype文件需要提供不同的字符型文件名,这些是以文件的形式呈现给用户的参数,用户可以通过文件操作工具直接读取和写入,也便于维护。然后不同的cfile实例要可以提供自己的read/write接口,而这些接口的不同实现需要读取和写入子系统特有参数,而具体是什么值和什么意义由子系统而不是cgroup决定。
①、populate一个文件夹
在一个cgroup文件夹被创建出来之后,cgroup会调用自己管理的各个子系统的populate接口,通过该接口不同子模块可以在文件夹中创建自己的参数文件。通常的流程为
在cgroup_add_file函数中,其中两个比较关心的操作:
cpuset_populate--->>cgroup_add_file
    if (subsys && !test_bit(ROOT_NOPREFIX, &cgrp->root->flags)) {
        strcpy(name, subsys->name);
        strcat(name, ".");这里是把子系统的名称和cfile中的名称拼接在一起,这样同一个cgroup中不同的子系统可以起相同的名字,避免“好ID都被狗起”了的悲剧,这也是我们看到一个cgroup文件夹下文件名都有子模块前缀的原因
    }
        error = cgroup_create_file(dentry, mode | S_IFREG,
                        cgrp->root->sb);
        if (!error)
            dentry->d_fsdata = (void *)cft;大家注意这个cfile实例是如何和文件系统粘合起来的,这一点和struct file中的private_data指针的作用是一样的,虽然是文件系统的正式编制,但是由不同的应用定义、解释和使用,相当于“特务机构”
真正的创建在接下来接口中
cgroup_create_file(struct dentry *dentry, mode_t mode, struct super_block *sb)
    if (S_ISDIR(mode)) {
        inode->i_op = &cgroup_dir_inode_operations;
        inode->i_fop = &simple_dir_operations;

        /* start off with i_nlink == 2 (for "." entry) */
        inc_nlink(inode);

        /* start with the directory inode held, so that we can
         * populate it without racing with another mkdir */
        mutex_lock_nested(&inode->i_mutex, I_MUTEX_CHILD);
    } else if (S_ISREG(mode)) {
        inode->i_size = 0;
        inode->i_fop = &cgroup_file_operations;
    }
作为对比,我们看一下一个cgroup是如何和文件系统扯上关系的
cgroup_mkdir--->>>cgroup_create--->>>cgroup_create_dir
    if (!error) {
        dentry->d_fsdata = cgrp;可以看到,cgroup同样是附着在了dentry结构的d_fsdata指针上。这里顺便说一下,dentry是一个内存结构和inode一样,并不和具体文件系统直接对应,它是一个文件系统抽象的通用目录项表示,文件和文件都有唯一对应项,inode也是如此
        inc_nlink(parent->d_inode);
        rcu_assign_pointer(cgrp->dentry, dentry);
        dget(dentry);
    }
②、cgroup如何找到自己包含的所有文件
这个其实是使用文件系统通用功能实现的,在前面看到的
cgroup_mkdir--->>>cgroup_create--->>>cgroup_create_dir--->>cgroup_create_file
    if (S_ISDIR(mode)) {
        inode->i_op = &cgroup_dir_inode_operations;
        inode->i_fop = &simple_dir_operations;
……
}
在simple_dir_operations函数中,对于文件夹内容的读取使用的是通用文件操作接口,而这个接口需要的数据在cgroup_add_file--->>>lookup_one_len会创建一个挂载在cgroup文件夹下的文件,进而在cgroup_create_file函数中通过cgroup_new_inode和d_instantiate接口进行新分配dentry的初始化、实例化和合法化。
当我们执行ls一个文件夹的时候,系统调用对应的操作就是simple_dir_operations中注册的dcache_readdir接口,这个就是简单的dentry结构展示,所以和VFS的主流框架实现了汇合。
③、cgroup如何知道自己管理的子系统
同样是在cgroup_mkdir--->>>cgroup_create
for_each_subsys(root, ss) {遍历自己管理的每个子系统,调用它们的ceate接口返回各自的cgroup_subssys_state结构
        struct cgroup_subsys_state *css = ss->create(ss, cgrp);

        if (IS_ERR(css)) {
            err = PTR_ERR(css);
            goto err_destroy;
        }
        init_cgroup_css(css, ss, cgrp);该函数将会执行cgrp->subsys[ss->subsys_id] = css命令,将子系统创建的cgroup_subsys_state结构安装到cgroup实例的内置数组中,这也就意味着每个子系统以自己编译时确定的子系统号来到cgroup中查找自己的cgroup_subsys_state地址
        if (ss->use_id) {
            err = alloc_css_id(ss, parent, cgrp);
            if (err)
                goto err_destroy;
        }
        /* At error, ->destroy() callback has to free assigned ID. */
        if (clone_children(parent) && ss->post_clone)
            ss->post_clone(ss, cgrp);
    }
④、子系统如何知道自己cgroup_subsys_state实例
每个子系统可以管理任意多的cgroup_subsys_state实例,本质上看,它们都是一些C++中的接口,它们需要知道每次操作的this指针。例如文件操作接口如何知道此次操作的是哪个cgroup_subsys_state实例?
我们一cpuset子系统实现为例,它在cfile中注册的cpu文件操作接口为
static int cpuset_write_resmask(struct cgroup *cgrp, struct cftype *cft, const char *buf)
可以看到,它的第一个参数是一个cgroup实例,如果知道了这个结构,可以通过自己编译时确定的子系统编号从cgroup实例中的struct cgroup_subsys_state *subsys[CGROUP_SUBSYS_COUNT];
数组方便准确的找到此次的操作数,因为当cgroup确定之后,它的各个子系统实例就已经确定。而该函数中查找此次实例的位置为

static inline struct cgroup_subsys_state *cgroup_subsys_state(
    struct cgroup *cgrp, int subsys_id)
{
    return cgrp->subsys[subsys_id];
}

/* Retrieve the cpuset for a cgroup */
static inline struct cpuset *cgroup_cs(struct cgroup *cont)
{
    return container_of(cgroup_subsys_state(cont, cpuset_subsys_id),该 cpuset_subsys_id作为cgroup中数组下标索引cpuset_subsys_state
                struct cpuset, css);
}
struct cpuset *cs = cgroup_cs(cgrp);
那么这个cgroup实例是从哪里来的呢?我们在前面的cgroup_mkdir-->>cgroup_create--->>cgroup_create_dir函数中,它将文件夹对应的cgroup实例寄生在了dentry结构的d_fsdata指针中。然后在文件的操作中,直接承接VFS read/write操作的为cgroup_file_operations结构中接口,以写操作为例
static inline struct cgroup *__d_cgrp(struct dentry *dentry)
{
    return dentry->d_fsdata;
}

static inline struct cftype *__d_cft(struct dentry *dentry)
{
    return dentry->d_fsdata;
}
static ssize_t cgroup_file_write(struct file *file, const char __user *buf,
                        size_t nbytes, loff_t *ppos)
{
    struct cftype *cft = __d_cft(file->f_dentry);文件目录项的d_fsdata为cfile实例地址,其中包含了不同子系统参数的真正写入操作,相当于重载的虚函数
    struct cgroup *cgrp = __d_cgrp(file->f_dentry->d_parent);父目录项中的d_fsdata为cgroup实例地址,此处从父目录项的d_fsdata中读到cgroup实例地址

    if (cgroup_is_removed(cgrp))
        return -ENODEV;
    if (cft->write)
        return cft->write(cgrp, cft, file, buf, nbytes, ppos);
    if (cft->write_u64 || cft->write_s64)
        return cgroup_write_X64(cgrp, cft, file, buf, nbytes, ppos);接下来两个还可能进行再次转发
    if (cft->write_string)
        return cgroup_write_string(cgrp, cft, file, buf, nbytes, ppos);
    if (cft->trigger) {
        int ret = cft->trigger(cgrp, (unsigned int)cft->private);
        return ret ? ret : nbytes;
    }
    return -EINVAL;
}
二、进程附加
当一个cgroup创建之后,它就相当于搭建了一个蜘蛛网,不同的进程就可以自投罗网了,加入这个cgroup管理中。这里的进程附加就相当于投奔梁山,自己主动单个要求加入(当然,事实上都是被逼的,通常是系统管理员让他附加的)这个cgroup。当有新的进程附加的时候,不同的子系统要根据当前子系统配置参数对进程进行适当的调整,从而让它成为一个驯服的进程。
还是以cpuset为例(为什么一cpuset威力呢?因为其他的要么太简单,例如cpuacct,要么太复杂,例如meme,要么太不规范,例如freezer)。假设说一个cpuset配置只能在2核和3核上运行(配置为4核SMP系统),而新附加的线程正在1核上运行,那么cpuset系统就需要此时马上纠正这种错误行为(这种纠正比外交部的抗议要实在的多,因为这里是动真格的了,而且周末不休息),把它的运行限制在2核和3核上。
1、基础框架内文件创建
在cgroup文件中,有一些是cgroup提供的基础设施文件,它们在所有的文件夹中都是存在的,通过这些文件可以控制一些通用性能。例如,文件的添加功能就是通过cgroup提供的tasks文件来实现的:
static struct cftype files[] = {
    {
        .name = "tasks",
        .open = cgroup_tasks_open,
        .write_u64 = cgroup_tasks_write,
        .release = cgroup_pidlist_release,
        .mode = S_IRUGO | S_IWUSR,
    },
当用户向文件夹的tasks文件中写入数字的时候,就相当于让一个文件加入了这个cgroup,所以cgroup就会把这个新加入的任务通知到各个子系统,从而让各个子系统对任务进行修剪和加工,以适应该组的特征,或者说是“同化”的过程。这些基础设施文件在创建文件夹时创建:cgroup_mkdir--->>cgroup_create--->>>cgroup_populate_dir
err = cgroup_add_files(cgrp, NULL, files, ARRAY_SIZE(files));
……
    for_each_subsys(cgrp->root, ss) {
        if (ss->populate && (err = ss->populate(ss, cgrp)) < 0)
            return err;
    }
可以看到在调用所有子系统的populate接口之前,人家框架自己先加入了一些文件。
2、如何通知子系统新任务附加
在前面 cgroup_file_write函数中可以看到,它会一次判断cfile的write、write_u64、write_s64、write_string接口是否定义,然后调用,由于tasks文件定义了
.write_u64 = cgroup_tasks_write,
接口,所以调用cgroup_tasks_write,接下来的操作流程为
cgroup_tasks_write--->>>attach_task_by_pid--->>>cgroup_attach_task
for_each_subsys(root, ss) {
        if (ss->can_attach) {
            retval = ss->can_attach(ss, cgrp, tsk, false);
            if (retval) {
                /*
                 * Remember on which subsystem the can_attach()
                 * failed, so that we only call cancel_attach()
                 * against the subsystems whose can_attach()
                 * succeeded. (See below)
                 */
                failed_ss = ss;
                goto out;任何一个子系统不能附加,则此次附加失败,相当于联合国五大常委的一票否决制
            }
        }
    }
……
    for_each_subsys(root, ss) {遍历所有子系统
        if (ss->attach)
            ss->attach(ss, cgrp, oldcgrp, tsk, false);此处通知特定子系统有新的进程加入控制系统
    }
3、cpuset的attach实现
cpuset_attach--->>>cpuset_attach_task--->>>set_cpus_allowed_ptr---->>>stop_one_cpu(cpu_of(rq), migration_cpu_stop, &arg)--->>>>migration_cpu_stop--->>>__migrate_task---->>>activate_task(rq_dest, p, 0)
这里大家对这个cpuset的设置流程的逻辑可以不理解,因为这个一般是多核中才有的一个特征,这里列出调用链的原因只是想说明一个文件:当有新进程附加时,新进程会被马上进行裁剪,当
echo $pid>tasks
调用正确返回之后,pid对应的任务已经被重新绑定到了指定范围的CPU中。
三、参数调整
当进程附加的时候,它是一个单点操作,就是对一个特定进程的操作,但是如果说修改了一个子系统的参数,那么这个子系统中的所有已经加入的进程都会受到影响,因为它本来就是控制其中所有进程的一个参数,虽然不是系统级,但是控制组级级的,也要立即生效。这里最为直接的要求就是应该能够遍历当前所有已经加入到控制组中的任务,这个责任同样责无旁贷的落在了cgroup的身上,但是这个触发时机并不是cgroup可以可控制的,因为参数调整是直接修改子系统文件,所以各个子系统决定是否遍历控制组任务,而cgroup只是提供遍历的机制。
1、进程附加入何处
显然的,我们必须从进程开始附加的接口开始看起,看新附加的进程滑落谁家,情归何处。直接的接口就是cgroup_tasks_write--->>>attach_task_by_pid--->>cgroup_attach_task
  
 if (!list_empty(&tsk->cg_list)) {
        list_del(&tsk->cg_list);
        list_add(&tsk->cg_list, &newcg->tasks);
    }
这里最为核心的操作就是通过task_struct结构总的cg_list接口将附加任务添加到新创建的css_set结构的tasks链表中,所以当我们遍历一个cgroup的所有已附加任务的时候需要首先遍历这个cgroup引用的所有的css_set实例,然后遍历每个css_set实例的tasks链表,也就是一个不定长的二级链表结构,全部通过list_head结构连接在一起。
2、迭代遍历接口
cgroup_iter_start、cgroup_iter_next、cgroup_iter_end这一组接口来实现对cgroup中所有任务的遍历,这个相当于一个算法类,在C++中对应为iterator类,它的特点就是提供方法,隐藏内部数据结构,从而便于重构和变化。
在cgroup_iter_start函数中,其中有初始化操作为
it->cg_link = &cgrp->css_sets;
也就是说,一个cgroup的css_sets结构指向了自己引用的所有的css_set结构实例链表。
cgroup_iter_next函数中
link = list_entry(it->cg_link, struct cg_cgroup_link, cgrp_link_list);
    if (l == &link->cg->tasks) {此时说明一个css_set中所有的task链表已经遍历结束,也就是说一个二级链表遍历结束,此时需要下一个css_set
        /* We reached the end of this task list - move on to
         * the next cg_cgroup_link */
        cgroup_advance_iter(cgrp, it);
    } else {否则遍历css_set中的task链表
        it->task = l;
    }
3、当参数变化时:以cpuset.cpus为例
cpuset_write_resmask--->>update_cpumask--->>>update_tasks_cpumask(cs, &heap)---->>>cgroup_scan_tasks
    cgroup_iter_start(scan->cg, &it);
    while ((p = cgroup_iter_next(scan->cg, &it))) {
……
}
    cgroup_iter_end(scan->cg, &it);
可以看到,它同样是需要遍历cgroup中所有的进程,并且也是使用了cgroup提供的迭代器功能。但是其中使用了一个额外的堆结构,用来保证对其中任务的遍历,其中堆注册了自己的回调函数,就是比较大小的函数heap_init(heap, PAGE_SIZE, GFP_KERNEL, &started_after),堆中的任务将按照创建时间进行排队。
其中在cgroup_iter_start和cgroup_iter_end中分别获得和释放了css_set_lock锁,此处为了在没有持有锁的情况下运行调用者提供的回调函数,此处首先是在start和end之间只是简单的取出所有的任务列表,退出之后再遍历这个提取的列表,这样可以减少锁的持有时间,因为回调函数执行的时间可能很长。
然后在释放了css_set_lock之后调用回调函数,对于cpuset来说,该回调为cpuset_change_cpumask函数,此后的操作就是Cpuset的特有逻辑了,此处就不再展开讨论了。
4、目录间限制继承:以cpuset.cpus为例
这个限制不是cgroup规定的,而是不同的子系统自己决定是否需要考虑文件夹蕴含的限制关系。例如在cpuset中,一个文件夹限制了它只能在3、4核运行,它的子文件夹就不能要求其中任务可以到1、2核运行。这一点需要不同的子系统自己维护上下级关系限制,对于cpuset的这个上下级限制为:
cpuset_write_resmask--->>update_cpumask--->>validate_change

    /* Each of our child cpusets must be a subset of us */
    list_for_each_entry(cont, &cur->css.cgroup->children, sibling) {
        if (!is_cpuset_subset(cgroup_cs(cont), trial)) 如果子文件夹中已经允许在2、3核上运行,父文件夹不能取消2、3核运行
            return -EBUSY;
    }

    /* Remaining checks don't apply to root cpuset */
    if (cur == &top_cpuset)
        return 0;

    par = cur->parent;

    /* We must be a subset of our parent cpuset */
    if (!is_cpuset_subset(trial, par))如果父进程不允许在新的配置核上运行,则子文件夹也无法设置成功。这里可以看到,它只判断了一级,而没有遍历自己所有的父节点,这是因为集合的包含是一个可传递的关系。A > B && B > C ==>> A >C,所以只要它是父文件夹的一个子集,那么它一定也是祖父文件夹的一个子集
        return -EACCES;
5、子目录向上传递:以cpuacct为例
这个子系统将会统计其中所有任务总共的运行时间,然后体现在自己的文件中。这里它体现了文件夹的包含关系,也就是和文件系统中文件夹容量显示相同,那就是一个文件夹的容量包含了所有子文件夹的容量总和。对于cpuacct文件来说同样如此,一个文件夹中usage_percpu文件将会显示所有子文件夹(对应所有子控制组)的运行时间的总和,所以当子文件夹中数据变化时,他要逐层的向上传递,对应代码为
static void cpuacct_charge(struct task_struct *tsk, u64 cputime)
{
    struct cpuacct *ca;
    int cpu;

    if (unlikely(!cpuacct_subsys.active))
        return;

    cpu = task_cpu(tsk);

    rcu_read_lock();

    ca = task_ca(tsk);

    for (; ca; ca = ca->parent) { 这里是一个向父节点逐层遍历的过程,逐层汇报,直到根节点。
        u64 *cpuusage = per_cpu_ptr(ca->cpuusage, cpu);
        *cpuusage += cputime;
   
}

    rcu_read_unlock();
}
  评论这张
 
阅读(1648)| 评论(0)
推荐 转载

历史上的今天

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2017