Cgroup之CFS调度

为了对进程使用cpu资源的限制,可以在cgroup中创建对cpu_cgrp_subsys控制的文件系统接口。CFS调度器会根据从cgroup中配置的参数,控制进程对cpu的利用率。

task attach task_group

通过mkdir(或者cgroup_init_subsys)创建一个新cgroup时,会通过cpu_cgroup_css_alloc为cgroup创建一个task_group。其在每个cpu上都会有一个sched entity(se),每个se都会有一个cfs_rq用于控制task_group下面的每一个task se。

static struct cgroup_subsys_state *
cpu_cgroup_css_alloc(struct cgroup_subsys_state *parent_css)
{
	/*如果是root,默认使用的是root_task_group*/
	if (!parent) {
		return &root_task_group.css;
	}
	/*
	功能分析:申请task_group,并且为每个核建立se和cfs_rq
	函数分析:alloc_fair_sched_group为每个核建立se和cfs_rq,并建立其关系
	1.tg每个核上的se都会从tg.shares中分配权重
	注:calc_cfs_shares中通过se.weight=tg.shares*(se.cfs.weight/tg.load_avg)计算se.weight, se.cfs.weight是统计其cfs_rq下se对应的总weight(p.se加入到cfs_rq时,会通过account_entity_enqueue统计cfs.weight)
	*/
	tg = sched_create_group(parent);
}

在这里插入图片描述

在通过fork创建task,或者将task attach到对应的cpu_cgrp时,sched_move_task会将task附着于task_group创建的cfs_rq上。task_group因为其权重(tg.shares)会在一个调度周期内分配一部分slice,然后在分配给其下每个cfs_rq上具体的task.se。

void sched_move_task(struct task_struct *tsk)
{
	/*cfs的调度方法:fair_sched_class*/ 
	... ...
	/*
	功能分析:dequeue_task_fair会将se移出cfs_rq,如果cfs_rq上没有其他的se,会递归移出cfs.se
	函数分析:
	1.dequeue_entity会将task.se从cfs_rq队列中删除,并更新cfs_rq.weight
		-update_curr会更新cfs.curr的vruntime,并且通过__account_cfs_rq_runtime统计cfs_rq在本调度周期内的runtime_remaining
		-cfs.min_vruntime会作为se.vruntime的基准,比如说此时将se dequeue,会算出其相对于min_vruntime的偏移,然后在enqueue到另一个cfs时,其se.vruntime会在另一个cfs.minvruntime基准上加上这个偏移,保证即使切换cfs_rq,不会使se过度执行或者饥饿
		-return_cfs_rq_runtime会为cfs留下min_cfs_rq_runtime的配额,其他会返还给tg,如果有throttled_cfs_rq,tg会将cfs分配给throttled cfs
		-update_min_vruntime会在curr.vruntime,leftmost.vruntime和此时的min_vruntime最小值作为cfs.min_vruntime
		-update_cfs_shares会重新计算curr.se.parent在tg中所占的权重(因为curr移出了cfs_rq)
	2.update_load_avg计算所有没有移出rq的cfs_rq对其tg的权重贡献(tg_load_avg_contrib),其值会在update_cfs_shares用来计算cfs.se在tg中分配的权重
	*/
	if (queued)
		dequeue_task(rq, tsk, DEQUEUE_SAVE);
         /*
         功能分析:put_prev_entity会将其task.se及其parent_se从其cfs.curr中删除
         函数分析:
         1.se.on_rq在dequeue_entity置0,在enqueue_entity时置1
         2.如果se.on_rq,会通过update_curr计算curr的vruntime,并且通过__enqueue_entity将curr重新加入红黑树
         3.check_cfs_rq_runtime会检查是否将cfs throttled
         注:这里并没有将curr从rq.curr中删除,只是从cfs.curr中删除,并且rq.curr一定不能on_rq
         */
	if (unlikely(running))
		put_prev_task(rq, tsk);
        /*根据新的css更新task对应的task_group,并将task挂到task_group对应cpu的cfs_rq上*/
        tg = container_of(task_css_check(tsk, cpu_cgrp_id, true),
			  struct task_group, css);
	tg = autogroup_task_group(tsk, tg);
	tsk->sched_task_group = tg;
	set_task_rq(tsk, task_cpu(tsk));

	/*
	功能分析:set_curr_task_fair将task.se和其parent_se添加到对应cfs.curr
	函数分析:
	1.set_next_entity中如果发现path上se有on_rq,通过__dequeue_entity将se从红黑树中删除,并且统计se对cfs_rq的权重贡献
	注:正在运行se所在的path都会从cfs_rq vruntime红黑树中删除,但是path的on_rq标志没被删,在put_prev_task时会重新加到cfs vruntime红黑树中
	2.account_cfs_rq_runtime会检查新cfs.runtime_remaining是否用完,如果配额用完并且申请不到,就会resched
	*/
	if (unlikely(running))
	tsk->sched_class->set_curr_task(rq);
	/*
	功能分析:enqueue_task_fair将on_rq没置上的task.se和其parent_se加入到对应的cfs_rq,并设置其se.on_rq,此时se和其parent_se才能被调度
	函数分析:
	1.向上遍历se时,遇到throttled cfs就停止
	2.enqueue_entity
	-se.vruntime此时记录着真实vruntime相对于cfs.min_vruntime的偏移
	-update_curr更新cfs.curr.vruntime,并查看cfs是否还有配额运行
	-enqueue_entity_load_avg更新cfs对tg的权重,account_entity_enqueue将se的权重加到cfs中(后续update_cfs_shares会用来计算总的tg_weight),update_cfs_shares重新计算cfs.se所占tg的权重
	-place_entity会在cfs.min_vruntime基础上,对sleeper有一定加成,以适应最新的cfs_rq
	注:cfs.min_vruntime在update_curr和dequeue_entity时都会通过update_min_vruntime更新
	-__enqueue_entity将se加入cfs vruntime红黑树
	-如果cfs下面只有新加的se一个,通过check_enqueue_throttle看是否将cfs throttled
	3.cfs_rq.h_nr_running中记录着该rq上运行所有task(不管深度),而nr_running记录着其下一层运行的se(即使是task_group se)
	4.通过update_load_avg计算cfs对tg的贡献,在通过update_cfs_shares计算每个cfs.se在tg.shares分配的权重

	*/
	if (queued)
		enqueue_task(rq, tsk, ENQUEUE_RESTORE);
         ... ...
}

task resched

对于非抢占式内核,会在时钟中断中通过entity_tick检查curr运行时间,如果发现curr需要调度,会将其need_resched标志置上,在sys_call返回时调用schedule选择其他task执行。need_resched标志置上,主要考虑两个因素:vruntime和cgroup quota。如果在处理时钟中断时发现curr运行时间过长(vruntime过大),或者发现curr所在的cgroup所分配的cpu使用时间(quota)用完,都会促使curr resched。

vruntime

对于时间片调度而言,每个task都会在一个调度周期内根据其nice值分配一定的时间片。对于O(n)调度器而言,curr在时钟中断中会递减自己的时间片,然后在整个调度周期结束之后再遍历O(n)的task队列,为每个task分配新时间片。但是对于CFS调度器而言,会通过vruntime统计task的运行时间,并且会通过选择最小vruntime执行,保证所有task的vruntime complete fair。所以CFS会在时钟中断中递增vruntime(真实时间间隔通过nice值换算而成),而不用在调度周期完成之后遍历O(n)的task队列。

  1. 调度周期和时间片

    static u64 sched_slice(struct cfs_rq *cfs_rq, struct sched_entity *se)
    {
        /*调度周期:cfs_rq中se数目*0.75ms。cfs调度器是以一个cfs_rq而不是以rq计算调度周期。*/
        u64 slice = __sched_period(cfs_rq->nr_running + !se->on_rq);
        
        /*
        1.__calc_delta依靠se占其cfs的权重从而计算se在cfs_period分配的slice。se.weight值是依靠其nice值经过prio_to_weight转化而来,vruntime=(real_delta*1024)/se.weight。所以real_delta的权重是1024.
        2.因为parent_se所获得的时间片与其cfs.nr_running有关,所以会通过向上遍历parent_se稀释se slice。因为cfs下每个se slice都会被稀释,所以相当于cfs period被稀释。假设每个se vruntime都是0.75ms的情况下,大致能保证每个se都是从其parent_se slice下分配cpu时间
        3.如果没有on_rq,说明是curr,也会在计算period和cfs总权重时加进去
        */
        for_each_sched_entity(se) {
    		struct load_weight *load;
    		struct load_weight lw;
    
    		cfs_rq = cfs_rq_of(se);
    		load = &cfs_rq->load;
    
    		if (unlikely(!se->on_rq)) {
    			lw = cfs_rq->load;
    
    			update_load_add(&lw, se->load.weight);
    			load = &lw;
    		}
    		slice = __calc_delta(slice, se->load.weight, load);
    	}
    	return slice;
    }
    
  2. resched

为了防止因为task切换频繁而导致系统吞吐量下降,check_preempt_tick中会确保每个task至少会运行sysctl_sched_min_granularity。如果超过了这个最小运行时间,check_preempt_tick在以下两种情况下都会因为vruntime而切换task:

  • 从cfs_rq中分配的sched_slice用完
  • vruntime不是cfs_rq中最小的(curr.vruntime会在时钟中断时通过update_curr更新)

因为在task_tick_fair中向上依次遍历其parent_se,所以如果parent_se调度周期内的时间片使用完,即使cfs的period没到,也会因为parent_se slice用完而被resched。同理即使se.vruntime是其cfs下最小的,但是因为其parent_se.vruntime不是最小的同样会导致curr resched。

Cgroup Quota

cpu_cgroup中设置"cfs_quota_us"和"cfs_period_us"时,会为tg分配runtime,并且会设置一个period_timer定期更新cfb.runtime。如果在时间中断中account_cfs_rq_runtime发现cfs分配的时间配额(runtime_remaining)用完,会通过assign_cfs_rq_runtime向cfb_runtime申请时间配额,如果申请不到会将task设置为need_resched,并且会在schedule时将cfs throttled。

在这里插入图片描述

当period时间到(period_timer)重置cfb.runtime,或者有se dequeue将cfs.runtime_remaining返还一部分给cfb.runtime时(slack_timer),distribute_cfs_runtime会将runtime分配给throttled cfs,并且将cfs unthrottled.

__schedule

当curr从系统调用中返回时发现need_resched置上,会在schedule中通过pick_next_task选择另一个task执行。在发现tg配额全部用完之后,会将其cfs throttled,将其从rq中删除,避免tg中的se再次被调度,从而实现对进程使用cpu资源的限制。

static struct task_struct *
pick_next_task_fair(struct rq *rq, struct task_struct *prev)
{
	struct cfs_rq *cfs_rq = &rq->cfs;
	... ...
	/*从rq.cfs向下遍历找到一个se执行*/
	do {
		struct sched_entity *curr = cfs_rq->curr;
		if (curr) {
			if (curr->on_rq)
				/*
				1.更新curr vruntime
				2.account_cfs_rq_runtime如果发现cfs.runtime_remaining用完,会向cfb.runtime申请时间配额
				*/
				update_curr(cfs_rq);
			... ...
			/*
			1.如果cfs没申请到时间配额,会将cfs throttled
			2.这里因为update_curr申请过了时间配额,所以使用check_cfs_rq_runtime。还有一个接口check_enqueue_throttle,它是在se enqueue时会先通过account_cfs_rq_runtime检查cfs时间配额,如果申请不到会将cfs throttled
			*/
			if (unlikely(check_cfs_rq_runtime(cfs_rq)))
				goto simple;
		}
		/*
		1.选cfs_rq中curr leftmost last next中vruntime最小的se
		2.如果设置了cfs.skip,当其是curr leftmost时,会选vruntime次一级的se
		*/
		se = pick_next_entity(cfs_rq, curr);
		cfs_rq = group_cfs_rq(se);
	}
	/*
	1.后续通过put_prev_entity递归释放当前的cfs.curr树,并通过check_cfs_rq_runtime检查是否将cfs throttled;然后通过set_next_entity递归设置cfs.curr树
	2.如果rq下没有se(rq.cfs.nr_running=0),进行idle_balance从其他核上拉task过来执行
	注:只是通过mkdir创建一个空的tg,而不将task attach tg,是不会导致空的tg.se被调度。
	原因是:在cgroup mkdir时只是创建tg.se,虽然se.cfs_rq指向parent_cfs,但是只有在task attach到tg时才会通过enqueue_entity将task.se和tg.se加入到rq中,此时才会递增nr_running h_nr_running,并且将tg.se.vruntime加入到rq的红黑树中,此时tg.se才能被调度。
	*/
}
。
	原因是:在cgroup mkdir时只是创建tg.se,虽然se.cfs_rq指向parent_cfs,但是只有在task attach到tg时才会通过enqueue_entity将task.se和tg.se加入到rq中,此时才会递增nr_running h_nr_running,并且将tg.se.vruntime加入到rq的红黑树中,此时tg.se才能被调度。
	*/
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值