控制组分析——初始化与任务分组

来源:岁月联盟 编辑:exp 时间:2011-09-26

前言
前面介绍了控制组主要数据结构和控制组文件系统的设计,本文继续对控制组进行介绍,内容主要包括:控制组的初始化、任务分组、遍历控制组中任务以及其他相关实现.有了前面文章介绍的基础,本文的内容很容易理解.

1. 初始化控制组

内核会在系统启动时对各个系统模块进行初始化,控制组作为内核的功能模块之一当然也不例外.控制组的初始化分为两个阶段:系统刚刚进入start_kernel()后立刻进行初始化,另外是其他模块建立后进行初始化.为此控制组的初始化主要由两个函数实现cgroup_init_early()和cgroup_init().内核在两个不同位置上对控制组进行初始化的原因在于子系统管理系统中的某些资源,而有些子系统需在系统初始化相应资源(比如内存管理模块)模块之前初始化,此外系统默认控制组rootnode以及init_css_set等数据结构最好也提前初始化,这样在初始化系统其他模块时就能方便使用了.

1.1 cgroup_init_early()实现

系统启动早期(几乎在所有系统模块初始化之前)控制组初始化工作主要包括:初始化rootnode(默认控制组根结构),init_css_set,init_cg_cgrp_link等结构,并将init进程添加到dumptop(默认控制组rootnode.top_cgroup),以及初始化需早期初始化的子系统.该函数代码比较简单,主要工作是初始化各个数据结构并将数据结构关联起来,代码如下:
C代码 
int __init cgroup_init_early(void){ 
    int i; 
    //设定init_css_set的引用计数为1 
    atomic_set(&init_css_set.refcount,1); 
    //初始化和init_css_set结构中的链表头指针(cg_links,tasks)和链表节点 
    INIT_LIST_HEAD(&init_css_set.cg_links); 
    INIT_LIST_HEAD(&init_css_set.tasks); 
    INIT_HLIST_NODE(&init_css_set.hlist); 
    //css_set_count全局变量维护当前系统中css_set结构的数量, 
    //当系统需要创建新的层次结构时,需要将所有任务连接到该层次结构, 
    //此时需要分配的css_set的最大个数为css_set_count个,保存该变量, 
    //使得将所有任务链接到层次结构更加方便,事实上将系统中所有任务关联到 
    //层次结构中只需复制所有的css_set结构(需要一定的修改)并关联到层次结构. 
    css_set_count = 1; 
    //初始化层次结构(rootnode) 
    init_cgroup_root(&rootnode); 
    root_count = 1;//设定系统中层次结构的个数为1 
    //将init_css_set,init_cg_cgrp_link,dumptop(&rootnode.top_cgroup)链接起来 
    //并将init任务关联到init_css_set从而关联到默认层次结构(rootnode)中. 
    init_task.cgroup = &init_css_set; 
    init_css_set_link.cg = &init_css_set; 
    init_css_set_link.cgrp = dumptop; 
    //将init_cg_cgrp_link添加到默认控制组css_sets双向链表中 
    list_add(&init_css_set_link.cgrp_link_list,&rootnode.top_cgroup.css_sets); 
    //将init_cg_cgrp_link添加到init_css_set.cg_links双向链表中 
    //到这里就已经将init任务关联到默认层次结构中了,并属于唯一的控制组rootnode.top_cgroup 
    list_add(&init_css_set_link.cg_link_list,&init_css_set.cg_links); 
    //初始化全局css_set哈希数组,通过和css_set关联的cgroup_subsys_state结构的地址计算其哈希值, 
    //设定该哈希数组是为便于查找满足某些条件的css_set结构,比如在需要创建新的css_set时,查找 
    //是否存在可重用的css_set结构. 
    for(i=0;i<CSS_SET_TABLE_SIZE;i++) 
        INIT_HLIST_HEAD(&css_set_table[i]); 
    //检查子系统参数是否合法,并对需提前初始化的子系统进行初始化. 
    //在系统启动阶段只有内嵌子系统. 
    for(i=0;i<CGROUP_BUILTIN_SUBSYS_COUNT;i++){ 
        struct cgroup_subsys *ss = subsys[i]; 
        //子系统必须定义create和destroy接口,并且名字长度不能超过预定以的长度 
        BUG_ON(!ss->name); 
        BUG_ON(strlen(ss->name) > MAX_CGROUP_TYPE_NAMELEN); 
        BUG_ON(!ss->create); 
        BUG_ON(!ss->destroy); 
        //系统内嵌子系统的subsys_id和在subsys数组中的索引相同 
        if(ss->subsys_id != i){ 
            printk(KERN_ERR "cgroup:Subsys %s id == %d/n",ss->name,ss->subsys_id); 
            BUG(); 
        } 
        //检查是否定义了early_init标记,如果定义了则初始化该子系统 
        if(ss->early_init) 
            cgroup_init_subsys(ss); 
    } 

在上述代码中对于使用cgroup_init_subsys来初始化子系统,具体实现和分析可参考附录.

1.2 cgroup_init()实现

在读完cgroup_init_early()的代码后,也许会有这样的疑问,为什么不把控制组初始化的所有工作都放在cgroup_init_early().将所有的初始化操作都放在一起固然会使初始化操作更加紧凑集中,但系统调用cgroup_init_early()时内核几乎还没有初始化其他模块,而初始化控制组又需要某些模块提供的功能(比如文件系统,控制组需在procfs中创建一些接口,参考上一篇文章),因此只能等到这些模块建立后才能继续进行.此外,将需要提前初始化和不需要提前初始化的工作分开来更有利于设计和实现,针对不同的需求进行不同的设计与处理.cgroup_init()函数要做的工作比cgroup_init_early()稍微多一些,包括:初始化还未初始化的子系统,将init_css_set添加到哈希表中,创建系统cgroup内核对象以及proc系统中的接口,注册文件系统等操作,其代码较为简单便不再给出.

2. 任务分组

从用户的观点来看,将任务添加到控制组,只需将任务的pid写入到tasks控制文件中即可,但内核将任务添加到控制组却不是简单的事情.创建层次结构后,系统中所有任务均会连接到该层次结构根控制组,由于任务在同一个层次结构中只能属于一个控制组,故将任务添加到控制组时,任务需在控制组间迁移.
当用户将任务或进程pid写入到tasks控制文件时,内核会调用该控制文件写操作接口cgroup_tasks_write()和cgroup_procs_write(),这两个接口封装了attach_task_by_pid()函数.attach_task_by_pid()在对输入参数进行处理后调用cgroup_attach_proc()和cgroup_attach_task()实现将进程和任务添加到控制组.在介绍将任务添加到控制组实现前,需解释进程和任务的区别.在Linux环境中,进程和任务一般情况下是可互换的概念,但在控制组中,二者却有所不同.进程是运行中的程序实例,它可能会同时运行多个线程协作完成某些工作,不同线程来处理不同任务.在Linux内核中并没有线程的概念,取而代之的则是轻量进程,而这些轻量进程就是所谓的任务.

2.1 attach_task_by_pid()实现

该函数通过pid找到目标任务(进程)对应的task_struct结构,并根据传递的参数调用cgroup_attach_task()或cgroup_attach_proc()实现任务分组,代码实现如下:
C代码 
//struct cgroup *cgrp:需要添加到的目标控制组 
//u64 pid:任务或者进程的pid 
//boolthreadgroup:标记是将任务还是进程添加到控制组 
static int attach_task_by_pid(struct cgroup *cgrp, u64 pid, bool threadgroup){ 
    struct task_struct *tsk; 
    //任务的安全上下文,用于将任务添加到控制组时的权限检查 
    const struct cred *cred = current_cred(), *tcred; 
    int ret; 
    //将任务迁移到另外的控制组可能会对控制组进行修改,需首先锁定控制组 
    //cgroup_lock_live_cgroup()获得cgroup_mutex全局锁并检查控制组是否合法(未被删除). 
    if(!cgroup_lock_live_cgroup(cgrp)){ 
        return -ENODEV; 
    if(pid){//pid>0,则是将其他任务或者任务添加到控制组 
        rcu_read_lock(); 
        //通过pid查找task_struct结构,该函数通过pidhash哈希表来查找指定任务 
        //关于pidhash可参考附录 
        tsk = find_task_by_vpid(pid); 
        if(!tsk){//目标任务或者进程不存在 
            rcu_read_unlock(); 
            cgroup_unlock();//释放cgroup_mutex锁 
            return -ESRCH; 
        } 
        if(threadcgroup){//指定将线程组(进程)添加到控制组 
            tsk = tsk->group_leader; 
        } else if(tsk->flags & PF_EXITING){//线程已经结束 
            rcu_read_unlock(); 
            cgroup_unlock(); 
            return -ESRCH; 
        } 
        //在这里需要获得目标进程(任务)的安全结构,并对合法性进行检查 
        //只有具有管理员权限或者euid=uid或者euid=suid才能拥有权限将 
        //任务或者进程添加到控制组 
        tcred = __task_cred(tsk);//获取目标进程的安全上下文 
        if(cred->euid &&  
           cred->euid != tcred->uid && 
           cred->euid != tcred->suid){ 
            rcu_read_unlock(); 
            cgroup_unlock();//释放全局cgroup_mutex锁 
            return -EACCESS; 
        } 
        get_task_struct(tsk);//增加tsk的引用计数 
        rcu_read_unlock(); 
    } else {//pid==0,表示将任务或者任务所在线程组添加到控制组 
        if(treadcgroup) 
            tsk = current->group_leader; 
        else 
            tsk = current; 
        get_task_struct(tsk)//增加task_struct引用计数 
    } 
    if(threadcgroup){//线程组 
        //当添加到控制组时获得任务的写锁(threadgroup_fork_lock),这样禁止在将任务添加到 
        //控制组过程中调用fork(),防止threadlist发生改变,cgroup_attach_proc需遍历threadlist. 
        threadgroup_fork_write_lock(tsk); 
        ret = cgroup_attach_proc(cgrp,tsk); 
        threadgroup_fork_write_unlock(tsk); 
    } else { 
        ret = cgroup_attach_task(cgrp,tsk); 
    } 
    put_task_struct(tsk);//减少引用计数 
    cgroup_unlock();//释放cgroup_mutex锁 
    return ret; 

从代码可知,该函数根据pid找到目标任务或者目标任务所在线程组的领头线程,接着通过进程的安全结构(struct cred)实现权限的合法性检查,最后根据threadgroup参数调用cgroup_attach_proc或者cgroup_attach_task将进程(线程组)或任务添加到控制组.

2.2 cgroup_attack_task()实现

将单个任务添加到控制组实现起来显然要比将线程(任务)组添加到控制组要简单一些,因此先分析怎样将任务添加到控制组,然后再分析如何将线程组添加到控制组.将任务添加到控制组的实现并没有想象中的复杂,反而看起来很是简单,该函数需调用连接到目标控制组所在层次结构的子系统定义的接口来协助实现任务添加到控制组.
C代码 
//struct cgroup *cgrp:目标控制组 
//struct task_struct *tsk:目标任务 
int cgroup_attach_task(struct cgroup *cgrp,struct task_struct *tsk){ 
    int retval; 
    struct cgroup_subsys *ss,*failed_ss = NULL; 
    struct cgroup *oldcgrp; 
    struct cgroupfs_root *root = cgrp->root;//获取控制组根结构 
    //获取任务在目标控制组所在层次结构中的控制组 
    //该函数永远会返回一个控制组,因为在创建层次结构时,所有任务会属于根控制组 
    //具体实现见附录 
    oldcgrp = task_cgroup_from_root(root,tsk); 
    if(oldcgrp == cgrp)//目标控制组就是原来控制组,无须移动 
        return 0; 
    //检查子系统是否允许将任务添加到目标控制组 
    for_each_subsys(root,ss){//对连接到控制组所在层次结构的每个子系统 
        if(ss->can_attach){//查看是否定义了can_attach接口 
            retval = ss->can_attach(ss,cgrp,tsk); 
            if(retval){ 
                failed_ss = ss; 
                goto out; 
            } 
        } 
        if(ss->can_attach_task){//查询是否允许添加任务 
            retval = ss->can_attach_task(cgrp,tsk); 
            if(retval){ 
                failed_ss = ss; 
                goto out; 
            } 
        } 
    } 
    //系统允许添加,可放心将任务从原来控制组迁移到目标控制组了 
    retval = cgroup_task_migrate(cgrp,oldcgrp,tsk,false); 
    if(retval) 
        goto out; 
    //通知子系统将任务连接到控制组 
    for_each_subsys(root,ss){ 
        //子系统告诉控制组将会有任务添加到控制组,请做好工作 
        if(ss->pre_attach) 
            ss->pre_attach(cgrp); 
        //将任务添加到控制组 
        if(ss->attach_task) 
            ss->attach_task(cgrp,tsk); 
        //完成链接操作 
        if(ss->attach) 
            ss->attach(ss,cgrp,oldcgrp,tsk); 
    } 
    synchronize_rcu(); 
    //当成功将任务添加到控制组后,可能有任务等待删除该控制组,应该唤醒这些等待的任务. 
    cgroup_wakeup_rmdir_waiter(cgrp); 
out: 
    if(retval){ 
        //通知允许将任务连接到控制组的子系统,有一些子系统不允许此次的链接 
        //操作,需要调用cancel_attach接口做清理工作 
        for_each_subsys(root,ss){ 
            if(ss == failed_ss) 
                break; 
            if(ss->cancel_attach)//清理前面已经链接做的处理工作 
                ss->cancel_attach(ss,cgrp,tsk); 
        } 
    } 
    return retval; 

由于将任务添加到控制组涉及到任务在控制组间的迁移,上面代码的设计首先查询子系统是否允许将任务添加到控制组,如果所有链接到目标控制组所在层次结构的子系统都允许将任务添加到目标控制组,内核就确信将任务迁移到新的控制组一定不会失败,然后内核再将任务迁移到新控制组,最后通知子系统任务已经添加到控制组并调用链接操作接口.以上的设计通过“查询————实施”机制,避免了由于子系统链接操作的失败,导致任务重新迁移到原来控制组.

2.3 cgroup_attack_proc()实现

分析完将任务添加到控制组的代码,想必将任务组添加到控制组也不是一件困难的事情,然而事情往往非人所愿,将线程组添加到控制组不仅仅包括将多个任务添加到控制组,还需更多额外工作,主要原因在于将线程组添加到控制组时需面对更加复杂的环境,比如,在添加过程中,threadlist可能会发生改变等.该函数代码实现较长,其算法流程如下:

伪代码代码 
1) 获取线程组任务结构快照.通过调用宏while_each_thread(leader,tsk)来遍历任务组中的所有任务并保存到快照.此处需注意在获得任务组快照前,需调用rcu_read_lock()来保证在获取快照过程中threadlist(线程双向链表)不会被改变. 
2) 检查线程组是否能够被合法的添加到控制组.通过调用链接到目标控制组所在层次结构的子系统的can_attach和can_attach_task接口检查是否允许线程组中所有任务添加到控制组.只要有一个任务不被允许添加到控制组,那么该添加操作即为非法,其代码和cgroup_attach_task()处理基本相同. 
3) 确保对于所有需要迁移的线程(任务)都存在对应的css_set结构,如果不存在则分配一个css_set结构. 
4) 调用每个连接到层次结构的子系统的pre_attach接口,通知目标控制组会有任务添加到该组,接着调用子系统接口attach_task将每个任务添加到控制组,并调用cgroup_task_imgrate()实现任务的迁移,最后调用子系统接口attach完成链接操作. 
5) 调用cgroup_wakeup_rmdir_waiter()唤醒等待删除该控制组的进程. 


3 遍历控制组中所有任务

为遍历某控制组中所有任务,内核提供了struct cgroup_iter迭代器对象来实现遍历操作,其操作接口如下:
C代码 
void cgroup_iter_start(struct cgroup *cgrp,struct cgroup_iter *it); 
struct task_struct *cgroup_iter_next(struct cgroup *cgrp,struct cgroup_iter *it); 
void cgroup_iter_end(struct cgroup *cgrp,struct cgroup_iter *it); 

以上接口依据cgroup,cg_cgroup_link和css_set之间的关联方式进行遍历.如果遍历过程中发生了css_set被修改的情况可能会导致错误,故需在cgroup_iter_start()和cgroup_iter_end()中获得和释放css_set_lock锁.当需遍历控制组中所有任务时,可按照如下方法实现:
1) 调用cgroup_iter_start()来初始化迭代器.
2) 调用cgroup_iter_next()来检索下一个未被检索过的任务,并返回该任务指针,如果所有的任务均已检索则返回NULL.
3) 调用cgroup_iter_end()来销毁迭代器,主要解除css_set_lock的锁定.

此外,系统还定义了函数cgroup_scan_tasks()来遍历控制组中的所有任务,并对满足条件的任务做指定的处理,其完整声明如下:
C代码 
void cgroup_scan_tasks(struct cgroup_scanner *scan); 

其中,struct cgroup_scanner为控制组扫描器,定义如下:
C代码 
struct cgroup_scanner{ 
    struct cgroup *cg;//被扫描的控制组 
    int (*test_task)(struct task_struct *p,struct cgroup_scanner *scan); 
    void (*process_task)(struct task_struct *p,struct cgroup_scanner *scan); 
    struct ptr_heap *heap;//扫描时使用的堆指针,该对为小顶堆 
    void *data;//保存私有数据信息 
}; 

当cgroup_scan_tasks()扫描控制组中所有任务时,会对每个任务调用test_task进行测试,如果测试为真,则调用process_task进行处理,其遍历过程是采用cgroup_iter对象实现.heap数据项为系统自定义小顶堆对象指针,在cgroup_scan_tasks()中用于获取当前系统中未被处理的创建时间最早的任务,从而保证遍历任务的顺序按照任务创建的先后顺序进行.

4. 小结

本文着重介绍了控制组的初始化以及如何将任务(进程)添加到控制组.由于某些子系统需在其他模块初始化之前进行初始化,控制组的初始化分为了两个阶段:系统刚启动时的初始化和其他模块初始化后的初始化操作.将任务添加到控制组需将任务从原控制组迁移到新的控制组,此时会调用子系统接口来协助实现任务的迁移.此外,为方便遍历控制组中所有任务,内核提供了cgroup_iter和cgroup_scanner对象来实现遍历操作.

5. 附录

5.1 cgroup_init_subsys()实现

该函数在系统启动阶段初始化子系统,其主要工作包括如下几个方面:
1) 将子系统添加到rootnode.subsys_list双向链表中,即将子系统连接到默认的层次结构.
2) 初始化子系统结构包括:将子系统结构数据项root指向rootnode,并调用子系统create接口创建和rootnode.top_cgroup关联的css,然后调用init_cgroup_css将css和rootnode.top_cgroup关联起来.
3) 设定init_css_set全局变量的数据项subsys[]数组中的相关指针.
4) 检查是否指定了子系统对象中的fork和exit接口,并据此设定need_forkexit_callback变量.
5) 设定锁依赖映射关系.

5.2 pidhash结构

在某些情况下,内核必须能够从进程的PID导出对应的进程描述符指针,顺序扫描进程链表并检查进程描述符的pid字段可行但是相当低效.为加速查找,内核引入四个散列表,之所以需要四个不同的散列表是因为进程描述符中包含了四个表示不同类型PID的字段,每种类型的字段对应于一个散列表,具体类型如下:
Hash表类型  字段名  说明
PIDTYPE_PID  pid  进程PID
PIDTYPE_TGID  tgid  线程组领头进程PID
PIDTYPE_PGID  pgrp  进程组领头进程PID
PIDTYPE_SID  session 会话领头进程PID
以上四个散列表指针在pid_hash[]数组中,在attach_task_by_pid()函数中调用的find_task_by_vpid()便是通过查找散列表实现的,更多内容可参考相关资料.

5.3 task_cgroup_from_root()实现

该函数声明为:
C代码 
struct cgroup *task_cgroup_from_root(struct task_struct *task, struct cgroupfs_root *root); 

用于查找任务在层次结构root中所属的控制组.该函数通过task->cgroup找到和该任务关联的css_set结构,进而借助于cg_cgroup_link找到关联的控制组,如果某个关联的控制组的root字段和函数第二个参数相同,则该控制组即为在该层次结构中关联的控制组(每个任务只能添加到层次结构中一个控制组).需额外注意的是需判断和任务关联的css_set结构是否为init_css_set,如果是init_css_set则返回rootnode.top_cgroup。

结束语
控制组的初始化发生在系统建立阶段,该过程主要涉及建立默认层次结构并将相关数据结构关联起来,其中子系统的初始化较为特别,根据不同子系统的要求在系统初始化的不同阶段进行初始化.将任务添加到控制组是较为复杂的过程,控制组系统通过调用关联到层次结构的子系统预定义的接口协助实现任务链接控制组的过程.

冗长的论述有时会让人厌烦,希望您可以忍受我杂乱无章的表述,good luck!

作者“xiaohui-p”