cgroup学习(七)——cpu子系统 && (八)——CPUSET子系统

CPU子系统

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

[html] view plaincopy
  1. 2327 (cat) cpu_shares_read_u64 call trace:  
  2.  0xffffffff8104d0a0 : cpu_shares_read_u64+0x0/0x20[kernel]  
  3.  0xffffffff810be3aa :cgroup_file_read+0xaa/0x100 [kernel]  
  4.  0xffffffff811786a5 : vfs_read+0xb5/0x1a0[kernel]  
  5.  0xffffffff811787e1 : sys_read+0x51/0x90[kernel]  
  6.  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:
  1. static u64 cpu_shares_read_u64(struct cgroup *cgrp, struct cftype *cft)  
  2. {  
  3.        struct task_group *tg = cgroup_tg(cgrp);  
  4.        return (u64) tg->shares;  
  5. }  
  6. #define container_of(ptr, type, member) ({               \  
  7.        const typeof( ((type *)0)->member ) *__mptr = (ptr);     \  
  8.        (type *)( (char *)__mptr - offsetof(type,member) );})  
  9. /* return corresponding task_group object of a cgroup */  
  10. static inline struct task_group *cgroup_tg(struct cgroup *cgrp)  
  11. {  
  12.        return container_of(cgroup_subsys_state(cgrp, cpu_cgroup_subsys_id),  
  13.                          struct task_group, css);  
  14. }  
  15. static inline struct cgroup_subsys_state *cgroup_subsys_state(  
  16.        struct cgroup *cgrp, int subsys_id)  
  17. {  
  18.        return cgrp->subsys[subsys_id];  
  19. }  
       上面的四个函数我们可以清楚的看到如何从一个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包含实时进程对应的调度实体和调度队列,以及普通进程对应的调度实体和调度队列。见下面的结构定义:

  1. struct task_group{    
  2. #ifdef CONFIG_FAIR_GROUP_SCHED  
  3.        struct sched_entity **se;  普通进程调度实体,每个cpu上一个  
  4.        struct cfs_rq **cfs_rq;    普通进程调度队列,每个cpu上一个  
  5. #endif  
  6.   
  7. #ifdef CONFIG_RT_GROUP_SCHED  
  8.        struct sched_rt_entity **rt_se;  实时进程调度实体,每个cpu上一个  
  9.        struct rt_rq **rt_rq;           实时进程调度队列,每个cpu上一个  
  10. #endif  
  11. }  
        如果组在该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:
  1. vruntime +=  delta*NICE_0_LOAD/se.load->weight;  
  2. 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的权重:

  1. …  
  2. tg->shares = shares;  
  3.        for_each_possible_cpu(i) {  
  4.               struct rq *rq = cpu_rq(i);  
  5.               struct sched_entity *se;  
  6.   
  7.               se = tg->se[i];  
  8.               /* Propagate contribution to hierarchy */  
  9.               spin_lock_irqsave(&rq->lock, flags);  
  10.               for_each_sched_entity(se)  
  11.                      update_cfs_shares(group_cfs_rq(se));  
  12.               spin_unlock_irqrestore(&rq->lock, flags);  
  13. }  
       首先更新该task_group的shares值,然后更新该task_group在每个CPU上的运行队列上的该调度实体的相应值se->load->weight(update_cfs_shares):
  1. load = cfs_rq->load.weight;  //这个值在reweight_entity里可能被更新  
  2.  load_weight = atomic_read(&tg->load_weight);  
  3.  load_weight -= cfs_rq->load_contribution;  
  4.  load_weight += load;  
  5.  shares = (tg->shares * load);  
  6.  if (load_weight)  
  7.         shares /= load_weight;  
  8.   
  9. ht_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:

  1. void __sched_move_task(struct task_struct *tsk)  
  2. {  
  3.          int on_rq, running;  
  4.          struct rq *rq;  
  5.   
  6.          rq = task_rq(tsk);  
  7.          running = task_current(rq, tsk);  
  8.          on_rq = tsk->se.on_rq;  
  9.   
  10.          if (on_rq)  //如果该进程已经在运行队列里,则先出队列  
  11.                    dequeue_task(rq, tsk, 0);  
  12.          if (unlikely(running))  //如果该进程正在运行,那么先把它变为不可运行状态  
  13.                    tsk->sched_class->put_prev_task(rq, tsk);  
  14.   
  15. #ifdef CONFIG_FAIR_GROUP_SCHED  
  16.          if (tsk->sched_class->moved_group)   
  17.                    tsk->sched_class->moved_group(tsk, on_rq); //对于CFS组调度该函数为task_move_group_fair,该函数最终也是调用set_task_rq,只是它会判断当前进程是否已经处于运行队列里,如果是的话,那么它要重新计算一下vruntime,这就是attach task的最终结果  
  18.          else  
  19. #endif  
  20.                    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共享分摊)  
  21.   
  22.          if (unlikely(running)) //重新运行该进程  
  23.                    tsk->sched_class->set_curr_task(rq);  
  24.          if (on_rq)  //重新把该进程放到运行队列里  
  25.                    enqueue_task(rq, tsk, 0);  
  26. }  
         这样我们就把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学习(八)——CPUSET子系统


       对于CPUSET子系统我们主要解释对cpuset.cpus的操作。Read操作根据《cgroup学习(三)——伪文件》可以很容易的跟踪到代码,并解读,所以我们这里就不赘述,直接解释write及attach操作。首先我们看一下write操作的bt(使用systemtap见《使用systemtap获得内核函数的局部变量》):
[html] view plaincopy
  1. sudo sh -c "echo 0-3 > cpuset.cpus"  
  2. 13219 (sh) cpuset_change_cpumask call trace:  
  3.  0xffffffff810c2fc0 :cpuset_change_cpumask+0x0/0x20 [kernel]  
  4.  0xffffffff810bfc6b :cgroup_scan_tasks+0x17b/0x270 [kernel]  
  5.  0xffffffff810c4c2c :cpuset_write_resmask+0x17c/0x350 [kernel]  
  6.  0xffffffff810be14f :cgroup_file_write+0x16f/0x320 [kernel]  
  7.  0xffffffff81177e68 :vfs_write+0xb8/0x1a0 [kernel]  
  8.  0xffffffff81178871 : sys_write+0x51/0x90[kernel]  
  9.  0xffffffff8100b0f2 :system_call_fastpath+0x16/0x1b [kernel]  

       注:cpuset在attach之前要求先设置cpus及mems的值,如果tasks里没有task的话,也不会调用到cpuset_change_cpumask,会在前面返回。
      因为代码被优化的原因所有在cpuset_write_resmask与cgroup_scan_tasks之间的update_cpumask及update_tasks_cpumask函数被优化掉了,其中前者主要完成参数的解析,验证(validate_change这个新的值必须是它的parent的子集,并且它的所有children还必须是新值的子集,以及exclusive的排除),然后才将新的值cpumask_copy(cs->cpus_allowed,trialcs->cpus_allowed);最后才更新它下面的所有进程的cpus_allowed(这里只是更新它这级的进程,并没有更新它下级的cgroup里面的进程,因为这些进程由这个子cgroup自己管理,它们也还是新值的一个子集)update_tasks_cpumask(该函数主要初始化一个cgroup_scanner结构,cgroup用它来遍历cgroup管理的每个进程,并定义对每个进程的test及process操作),然后就到了cgroup_scan_tasks函数,它就是扫描cgroup_scanner(即cgroup下的每个进程,使用cgroup_iter_{start,next,end}三个接口),这里使用了一个大顶堆(key为进程的create_time)来缓存需要更新的进程,这样做的原因是:保证在更新的过程中新创建的进程也会被更新到,所以这里有一个goto语句,直到heap->size=0才跳出,这样就可以减少在fork的代码里加锁。最后就是对heap里面的所有进程进行process处理cpuset_change_cpumask,该函数最终就把cgroup的cpus_allowed复制到task->cpus_allowed,并且判断这个进程是否从旧的CPU运行队列,迁移到新的CPUSET的运行队列(该过程由migrate_task完成)。
注:这里用到了cgroup最重要的几结构之间的转换,所以我们再画一张简易的图来表示一下:


图 cgroup 与task的转换

       第一行从cgroup查找CPUSET子系统对应的控制体实现类,subsys[]数组保存了该cgroup所属的所有子系统抽象类,最后通过container_of获得实现类(注:container_of是中task_group内存储了整个cgroup_subsys_state内容,而不是指向cgroup_subsys_state的指针);第二行是从cgroup控制体的CPUSET子系统实现类到它所属的cgroup再到该cgroup所管理的所有tasks,这里cgroup_iter->cg_link则保存cgroup的css_set list,而cgroup_iter->task则分别保存每次css_set的task list,即task会因为遍历的css_set而变化。(这些结构关系见《cgroup学习(二)——cgroup框架结构》)
     可以看出整个wirte过程,最终的目的就是在于更新该cgroup下的每个进程的cpus_allowed,并对已在运行队列里或正在运行的进程进行迁移。下面我们再来看一下cpuset的attach过程:
     通过《cgroup学习(三)——伪文件》表格的cpuset_subsys全局变量,我们可以找到CPUSET子系统的attach函数为:cpuset_attach,可以想象代码应该就是把新的cgroup的cpu_allowed赋到将要attach的task的cpu_allowed,然后该迁移的迁移,不过如果你看代码的话会发现其实不是这样的,多了一个guarantee_online_cpus函数,一开始百想不得其解,后来查了一下online cpu的概念才明白,原来linux可以把物理cpu直接禁止掉(修改/sys/devices/system/cpu下的所有core目录下的online文件,这也就是CPU的热插拔),所以这个函数的目的就是排除掉所有offline的cpu后的cpu_allowed。有了cpu_allowed进程在被创建时wake_up_new_task(继承自父进程的cpu_allowed)或者被唤醒时try_to_wake_up,都会通过select_task_rq(该函数后面调用CFS调度器里的select_task_rq_fair来选择cpu_allowed里的某一个cpu,可能是load最低的)来确定它应该被置于哪个CPU的运行队列及运行,一个进程在某一时刻只能存在于一个CPU的运行队列里。


展开阅读全文

没有更多推荐了,返回首页