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调度算法。
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的内容可能有出错,期待大家指正。 参考: