cgroup学习(七)——cpu子系统

CPU子系统

        对于CPU子系统最常见的参数就是cpu.shares,我们来通过《cgroup学习(三)——伪文件》的表格来跟踪一下对该参数的读写操作。
        通过systemtap我们可以看到读的bt:(cat cpu.shares)

2327 (cat) cpu_shares_read_u64 call trace:
 0xffffffff8104d0a0 : cpu_shares_read_u64+0x0/0x20[kernel]
 0xffffffff810be3aa :cgroup_file_read+0xaa/0x100 [kernel]
 0xffffffff811786a5 : vfs_read+0xb5/0x1a0[kernel]
 0xffffffff811787e1 : sys_read+0x51/0x90[kernel]
 0xffffffff8100b0f2 :system_call_fastpath+0x16/0x1b [kernel]
       在上面我们已经说过,在创建cgroup的时候将对文件的操作cftype保存到file->f_dentry->d_fsdata,同时cgroup信息保存在目录的dentry->d_fsdata,所以当通过vfs进入cgroup文件系统里时,通过cgroup_file_read获得这些信息后,直接调用该文件的cpu_shares_read_u64:
static u64 cpu_shares_read_u64(struct cgroup *cgrp, struct cftype *cft)
{
       struct task_group *tg = cgroup_tg(cgrp);
       return (u64) tg->shares;
}
#define container_of(ptr, type, member) ({               \
       const typeof( ((type *)0)->member ) *__mptr = (ptr);     \
       (type *)( (char *)__mptr - offsetof(type,member) );})
/* return corresponding task_group object of a cgroup */
static inline struct task_group *cgroup_tg(struct cgroup *cgrp)
{
       return container_of(cgroup_subsys_state(cgrp, cpu_cgroup_subsys_id),
                         struct task_group, css);
}
static inline struct cgroup_subsys_state *cgroup_subsys_state(
       struct cgroup *cgrp, int subsys_id)
{
       return cgrp->subsys[subsys_id];
}
       上面的四个函数我们可以清楚的看到如何从一个cgroup转换到对应子系统的控制体的抽象类(cgroup_subsys_state),然后再转换到实现类(task_group)的过程,最后从实现类中取得shares值。

       写操作与上面的流程差不多,不过在介绍写效果前,我们先简单了解一下linux的CFS组调度。
       在linux内核中,使用task_group结构来管理组调度的组。所有存在的task_group组成一个树型结构(与cgroup一样)。一个组也是一个调度实体(最终被抽象为sched_entity,跟普通task一样),这个调度实体被添加到其父task_group的运行队列(se->cfs_rq)。与普通task不一样的是:task_group的sched_entity是每个CPU有一个,并且每个CPU也有对应的运行队列cfs_rq。
        一个task_group可以包含具有任意调度类别的进程(具体来说是实时进程和普通进程两种类别),task_group包含实时进程对应的调度实体和调度队列,以及普通进程对应的调度实体和调度队列。见下面的结构定义:

struct task_group{  
#ifdef CONFIG_FAIR_GROUP_SCHED
       struct sched_entity **se;  普通进程调度实体,每个cpu上一个
       struct cfs_rq **cfs_rq;    普通进程调度队列,每个cpu上一个
#endif

#ifdef CONFIG_RT_GROUP_SCHED
       struct sched_rt_entity **rt_se;  实时进程调度实体,每个cpu上一个
       struct rt_rq **rt_rq;           实时进程调度队列,每个cpu上一个
#endif
}
        如果组在该cpu上有可以运行的进程,则组在该cpu上的调度实体se(组本身的调度实体)就会挂到该cpu上cfs_rq的红黑树上(树的最左边叶子节点最先被调用);组在该cpu上可以运行的进程(组内进程的调度实体)则挂到了组调度实体se中my_q指向的红黑树上。即从根组开始递归调度直到底层组内的普通进程被调度,它们采用的是一样的CFS调度算法。
        CFS组的优先级:CFS 不直接使用优先级而是将其用作允许任务执行的时间的衰减系数。低优先级任务具有更高的衰减系数,而高优先级任务具有较低的衰减系数。这意味着与高优先级任务相比,低优先级任务允许任务执行的时间消耗得更快。组在创建时其优先级是固定的,其nice值为0(它对应的wegiht值是1024,其实在CFS调度器中所有的优先级nice值最终都会变转换为它唯一识别的weight值prio_to_weight)。组在某cpu上默认所能获得的运行时间和一个单独的nice为0的进程获得的运行时间相同。组的se在获得了一定运行时间后,按照CFS算法相同的方法把实际运行时间分配给它的my_q上的所有进程(se本身所在的运行队列为se->cfs_rq,se下面的se存在的运行队列为se->my_q)。
        上面我们说过运行队列是一棵红黑树,那么这棵树的key是什么?在CFS调度算法里维护着一个vruntime,它表示该调度实体的虚拟运行时间,而它也就是这棵红黑树的key。另外,每个调度实体的理想运行时间为ideal_time:
vruntime +=  delta*NICE_0_LOAD/se.load->weight;
ideal_time = __sched_period(nr_running)*se.load->weight/cfs_rq.load->weight

        其中delta为当前se从上次被调度执行到当前的实际执行时间,__sched_period确定延迟调度的周期长度(它由当前cfs_rq的长度线性扩展),从上面两个公式可以知道:在执行时间相等的条件下(delta相同),调度实体的weight值越大,它的vruntime增长的越慢,它也就越容易再被调度(在树的左边);同样获得的理想运行时间也越多,注:该值只是用来确定当前进程是否该被换出,它并不是进程被调度时能够运行的时间(对于CFS不存在这样的时间片),在CFS里进程的换入换出原则上都是由自己决定的。上面两个公司也是cpu.shares最终起作用的地方。所以当两个cgroup它们的shares值为1:2时,那么这两个组的整体运行时间将保持在1:2,而与它们组内的task个数及优先级无关。Group运行时间已由shares值,等待时间等确定了,它们内部的所有tasks只能去共享这些时间(如果组内的进程有优先级不同,那么它们同样按照CFS算法去分配这个总的时间,高优先级的获得的时间多,低优先级的获得的时间少),而不会去增加组的总共运行时间。
        下面我们再来看一下写过程,它最终会调用sched_group_set_shares来修改该task_group的权重:

…
tg->shares = shares;
       for_each_possible_cpu(i) {
              struct rq *rq = cpu_rq(i);
              struct sched_entity *se;

              se = tg->se[i];
              /* Propagate contribution to hierarchy */
              spin_lock_irqsave(&rq->lock, flags);
              for_each_sched_entity(se)
                     update_cfs_shares(group_cfs_rq(se));
              spin_unlock_irqrestore(&rq->lock, flags);
}
       首先更新该task_group的shares值,然后更新该task_group在每个CPU上的运行队列上的该调度实体的相应值se->load->weight(update_cfs_shares):
      load = cfs_rq->load.weight;  //这个值在reweight_entity里可能被更新
       load_weight = atomic_read(&tg->load_weight);
       load_weight -= cfs_rq->load_contribution;
       load_weight += load;
       shares = (tg->shares * load);
       if (load_weight)
              shares /= load_weight;

reweight_entity(cfs_rq_of(se), se, shares);
        可以看到这个se->load->weight是经过tg->shares重新计算的结果,最终调用reweight_entity去update_load_set(&se->load, weight);这样不是更新完该tg的在每个CPU上的se->load->weight吗,为什么在sched_group_set_shares还需要调用for_each_sched_entity(se)来对该se至顶层root的所有se进行更新?原因在于当我们更新该层的se->load时,该se所在的上层se->cfs_rq权重也会被更新(reweight_entity先减去原来的se->load值,再加上新的值),通过上面update_cfs_shares函数我们可以看到se->load->weight是由当前层的cfs_rq->load.weight决定的,即当下层的se->load->weight被更新时,它可能会更新该se所在的cfs_rq的权重(而不是它管理的下层运行队列my_q),从而影响到上层se的load->weight。这些更新将最终体现在下次计算vruntime的结果上。

       上面我们介绍了对shares这个伪文件的操作,及这个值是如何去影响组内的tasks。其它的参数伪文件也是类似的分析过程。另外,在前面的attach task中我们介绍了attach的第一个过程,下面我们分析一下第二个过程在cpu子系统中的实现,简单的跟踪一下代码可以查找该过程最终调用__sched_move_task:

void __sched_move_task(struct task_struct *tsk)
{
         int on_rq, running;
         struct rq *rq;

         rq = task_rq(tsk);
         running = task_current(rq, tsk);
         on_rq = tsk->se.on_rq;

         if (on_rq)  //如果该进程已经在运行队列里,则先出队列
                   dequeue_task(rq, tsk, 0);
         if (unlikely(running))  //如果该进程正在运行,那么先把它变为不可运行状态
                   tsk->sched_class->put_prev_task(rq, tsk);

#ifdef CONFIG_FAIR_GROUP_SCHED
         if (tsk->sched_class->moved_group) 
                   tsk->sched_class->moved_group(tsk, on_rq); //对于CFS组调度该函数为task_move_group_fair,该函数最终也是调用set_task_rq,只是它会判断当前进程是否已经处于运行队列里,如果是的话,那么它要重新计算一下vruntime,这就是attach task的最终结果
         else
#endif
                   set_task_rq(tsk, task_cpu(tsk)); //该将进程的se->cfs_rq置为新的task_group在原来cpu上的运行队列,同时se->parent置为新task_group在原来cpu上的se,这样以后调度该进程时都将受到task_group的影响(每次pick_next_task_fair总是从上往下,所以vruntime也是从先上级确定给由下级的所有se共享分摊)

         if (unlikely(running)) //重新运行该进程
                   tsk->sched_class->set_curr_task(rq);
         if (on_rq)  //重新把该进程放到运行队列里
                   enqueue_task(rq, tsk, 0);
}
         这样我们就把cpu子系统的shares文件读写及attach task操作介绍完了。写操作的简单理解就是通过更新task_group的shares值来更新调度实体的weight,最终影响该group及上层group的vruntime;attach则是简单地把一个task从一个cgroup转换到另一个cgroup(这其中要考虑进程是否已经在运行队列里或者已经在执行)。虽然尽力想把CFS调度看明白,但由于时间及能力有限,所以上面关系CFS的内容可能有出错,期待大家指正。

参考:
http://hi.baidu.com/_kouu/item/0fe32610e493314be75e06d1
http://blog.chinaunix.net/uid-27052262-id-3239260.html

  


你可能感兴趣的:(cgroup学习(七)——cpu子系统)