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]
- 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];
- }
写操作与上面的流程差不多,不过在介绍写效果前,我们先简单了解一下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
- }
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);
- }
- 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;
- ht_entity(cfs_rq_of(se), se, shares);
上面我们介绍了对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);
- }
参考:
http://hi.baidu.com/_kouu/item/0fe32610e493314be75e06d1
http://blog.chinaunix.net/uid-27052262-id-3239260.html