初始化与任务分组

作者: 操作系统  发布:2019-09-06

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

cgroups 数据结构设计

我们从进程出发来剖析cgroups相关数据结构之间的关系。

在Linux中,管理进程的数据结构是task_struct,其中与cgroups有关的:

#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的进程组织成一个链表。下面我们来看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;

};

其中refcount是该css_set的引用数,因为一个css_set可以被多个进程共用,只要这些进程的cgroups信息相同,比如:在所有已创建的层级里面都在同一个cgroup里的进程。

hlist是嵌入的hlist_node,用于把所有css_set组织成一个hash表,这样内核可以快速查找特定的css_set。

tasks指向所有连到此css_set的进程连成的链表。

cg_links指向一个由struct cg_cgroup_link连成的链表。

Subsys是一个指针数组,存储一组指向cgroup_subsys_state的指针。一个cgroup_subsys_state就是进程与一个特定子系统相关的信息。通过这个指针数组,进程就可以获得相应的cgroups控制信息了。

下面我们就来看cgroup_subsys_state的结构:

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。

下面我们再来看cgroup的结构:

struct cgroup {

    unsigned long flags;       

    atomic_t count;

    struct list_head sibling;  

    struct list_head children; 

    struct cgroup *parent;     

    struct dentry *dentry;     

    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;

    struct list_head pidlists;

    struct mutex pidlist_mutex;

    struct rcu_head rcu_head;

    struct list_head event_list;

    spinlock_t event_list_lock;

};

sibling,children和parent三个嵌入的list_head负责将同一层级的cgroup连接成一颗cgroup树。

subsys是一个指针数组,存储一组指向cgroup_subsys_state的指针。这组指针指向了此cgroup跟各个子系统相关的信息,这个跟css_set中的道理是一样的。

root指向了一个cgroupfs_root的结构,就是cgroup所在的层级对应的结构体。这样以来,之前谈到的几个cgroups概念就全部联系起来了。

top_cgroup指向了所在层级的根cgroup,也就是创建层级时自动创建的那个cgroup。

css_set指向一个由struct cg_cgroup_link连成的链表,跟css_set中cg_links一样。

下面我们来分析一个css_set和cgroup之间的关系。我们先看一下 cg_cgroup_link的结构

struct cg_cgroup_link {

    struct list_head cgrp_link_list;

    struct cgroup *cgrp;

    struct list_head cg_link_list;

    struct css_set *cg;

};

cgrp_link_list连入到cgroup->css_set指向的链表,cgrp则指向此cg_cgroup_link相关的cgroup。

Cg_link_list则连入到css_set->cg_links指向的链表,cg则指向此cg_cgroup_link相关的css_set。

那为什么要这样设计呢?

那是因为cgroup和css_set是一个多对多的关系,必须添加一个中间结构来将两者联系起来,这跟数据库模式设计是一个道理。cg_cgroup_link中的cgrp和cg就是此结构体的联合主键,而cgrp_link_list和cg_link_list分别连入到cgroup和css_set相应的链表,使得能从cgroup或css_set都可以进行遍历查询。

那为什么cgroup和css_set是多对多的关系呢?

一个进程对应css_set,一个css_set就存储了一组进程(应该有可能被几个进程共享,所以是一组)跟各个子系统相关的信息,但是这些信息有可能不是从一个cgroup那里获得的,因为一个进程可以同时属于几个cgroup,只要这些cgroup不在同一个层级。举个例子:我们创建一个层级A,A上面附加了cpu和memory两个子系统,进程B属于A的根cgroup;然后我们再创建一个层级C,C上面附加了ns和blkio两个子系统,进程B同样属于C的根cgroup;那么进程B对应的cpu和memory的信息是从A的根cgroup获得的,ns和blkio信息则是从C的根cgroup获得的。因此,一个css_set存储的cgroup_subsys_state可以对应多个cgroup。另一方面,cgroup也存储了一组cgroup_subsys_state,这一组cgroup_subsys_state则是cgroup从所在的层级附加的子系统获得的。一个cgroup中可以有多个进程,而这些进程的css_set不一定都相同,因为有些进程可能还加入了其他cgroup。但是同一个cgroup中的进程与该cgroup关联的cgroup_subsys_state都受到该cgroup的管理(cgroups中进程控制是以cgroup为单位的)的,所以一个cgrouop也可以对应多个css_set。

那为什么要这样一个结构呢?

从前面的分析,我们可以看出从task到cgroup是很容易定位的,但是从cgroup获取此cgroup的所有的task就必须通过这个结构了。每个进程都会指向一个css_set,而与这个css_set关联的所有进程都会链入到css_set->tasks链表.而cgroup又通过一个中间结构cg_cgroup_link来寻找所有与之关联的所有css_set,从而可以得到与cgroup关联的所有进程。

最后让我们看一下层级和子系统对应的结构体。层级对应的结构体是cgroupfs_root:

struct cgroupfs_root {

    struct super_block *sb;

    unsigned long subsys_bits;

    int hierarchy_id;

    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];

    char name[MAX_CGROUP_ROOT_NAMELEN];

};

sb指向该层级关联的文件系统超级块

subsys_bits和actual_subsys_bits分别指向将要附加到层级的子系统和现在实际附加到层级的子系统,在子系统附加到层级时使用

hierarchy_id是该层级唯一的id

top_cgroup指向该层级的根cgroup

number_of_cgroups记录该层级cgroup的个数

root_list是一个嵌入的list_head,用于将系统所有的层级连成链表

子系统对应的结构体是cgroup_subsys:

struct cgroup_subsys {

    struct cgroup_subsys_state *(*create)(struct cgroup_subsys *ss,

                          struct cgroup *cgrp);

    int (*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, bool threadgroup);

    void (*cancel_attach)(struct cgroup_subsys *ss, struct cgroup *cgrp,

              struct task_struct *tsk, bool threadgroup);

    void (*attach)(struct cgroup_subsys *ss, struct cgroup *cgrp,

            struct cgroup *old_cgrp, struct task_struct *tsk,

            bool threadgroup);

    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;

    bool use_id;

#define MAX_CGROUP_TYPE_NAMELEN 32

    const char *name;

    struct mutex hierarchy_mutex;

    struct lock_class_key subsys_key;

    struct cgroupfs_root *root;

    struct list_head sibling;

    struct idr idr;

    spinlock_t id_lock;

    struct module *module;

};

Cgroup_subsys定义了一组操作,让各个子系统根据各自的需要去实现。这个相当于C++中抽象基类,然后各个特定的子系统对应cgroup_subsys则是实现了相应操作的子类。类似的思想还被用在了cgroup_subsys_state中,cgroup_subsys_state并未定义控制信息,而只是定义了各个子系统都需要的共同信息,比如该cgroup_subsys_state从属的cgroup。然后各个子系统再根据各自的需要去定义自己的进程控制信息结构体,最后在各自的结构体中将cgroup_subsys_state包含进去,这样通过Linux内核的container_of等宏就可以通过cgroup_subsys_state来获取相应的结构体。

作者曰:从cgroups的数据结构设计,我们可以看出内核开发者的智慧,其中即包含了数据库模式设计来解决数据冗余问题,又包含了OO思想来解决通用操作的问题。

  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 == %dn",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系统中的接口,注册文件系统等操作,其代码较为简单便不再给出.

  1. 任务分组

从用户的观点来看,将任务添加到控制组,只需将任务的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()中用于获取当前系统中未被处理的创建时间最早的任务,从而保证遍历任务的顺序按照任务创建的先后顺序进行.

  1. 小结

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

  1. 附录

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”

前面介绍了控制组主要数据结构和控制组文件系统的设计,本文继续对控制组进行介绍,内容主要包括:控制组的初始化、任务分组、遍历...

本文由今晚买四不像发布于操作系统,转载请注明出处:初始化与任务分组

关键词:

上一篇:定时抓取服务器状态
下一篇:没有了