CFS-完全公平調度器

CFS實現的公平的基本原理是這樣的:指定一個週期,根據進程的數量,大家”平分“這個調度週期內的CPU使用權,調度器保證在一個週期內所有進程都能被執行到。CFS和之前O(n)調度器不同,優先級高的進程能獲得更多運行時間,但不代表優先級高的進程一定就先運行:

調度器使用vruntime來統計進程運行的累計時間,理想狀態下,所有進程的vruntime是相等時代表當前CPU的時間分配是完全公平的。但事實上,即使是多核的系統一般進程數也是大於核心數的,所以一旦有進程佔用CPU運行勢必會造成不公平,完全公平調度器通過讓當前遭受不公最嚴重(vruntime最小)的進程優先運行來緩解不公平的情況。當然,vruntime所指的運行時間並未非和以往一樣每個或每幾個cpu tick週期增加1,需要經過優先級加權換算,優先級高的進程可能運行10個tick之後vruntime才加1,反之優先級低的進程可能運行1個tick之後vruntime就被加了10。

附一張圖:

1.調度週期如何規定?

CFS引入了一個動態變化的調度週期:period。看兩個CFS開放給用戶的參數:

{//字面意思調度最小粒度,即進程每次被調度到最少要佔用多長時間CPU
	.procname	= "sched_min_granularity_ns",
	.data		= &sysctl_sched_min_granularity,
},
{//字面意思調度延遲,即每個進程最長不等待超過調度延遲會被再次調度
	.procname	= "sched_latency_ns",
	.data		= &sysctl_sched_latency,
},

 

設想在最壞的情況下:只有一個CPU,等待運行的所有進程優先級相同所分得時間片相同,當一個進程運行過之後就要等待所有其他進程都運行到之後才能再次被調度。所以用戶設置的這兩個參數其實只有在rq上面進程數nr_running大於sysctl_sched_latency/sysctl_sched_min_granularity時才能被滿足。

//每次更新最小調度粒度和調度延遲兩個參數,都會更新sched_nr_latency
sched_nr_latency = DIV_ROUND_UP(sysctl_sched_latency, sysctl_sched_min_granularity);
//獲取動態調度週期period需要根據sched_nr_latency計算
static u64 __sched_period(unsigned long nr_running)
{
	if (unlikely(nr_running > sched_nr_latency))
		return nr_running * sysctl_sched_min_granularity;
	else
		return sysctl_sched_latency;
}

 

從上面代碼可以看出來,當進程太多時CFS只能先不管調度延遲,只保證最小調度粒度。爲什麼不保證用戶設置調度延遲而保證最小調度粒度?因爲最小調度粒度不保證的話,頻繁搶佔進程只會讓更多時間浪費在context switch上,進一步惡化CPU資源緊促的情況。

 

2.CFS是如何分配時間片的?

CFS引入了vruntime虛擬運行時間的概念,爲了讓所有進程vruntime趨於相等,每次pick_next_task挑選下個要被運行的進程總會挑vruntime最小的進程出來運行。

但是vruntime和wall-time是不相等的,還要通過優先級加權,也就是說同樣運行了10ms,高優先級進程vruntime+1低優先級進程可能要+10。

看看se中和cfs_rq中相關的成員:

tast_struct.se:
struct sched_entity {
	struct load_weight		load;
	unsigned int			on_rq;
};
cfs_rq:
struct cfs_rq {
	struct load_weight load;
	unsigned int nr_running, h_nr_running;
	u64 min_vruntime;
}
load_weight:
struct load_weight {
	unsigned long			weight;
	u32				inv_weight;
};

 

主要就是load_weight這個結構,load_weight字面意思就是權重,調度實體中的load_weight代表的該進程的權重,工作列隊裏的load_weight代表了整個列隊的總權重。

看看CFS如何分配時間片:

static u64 sched_slice(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
	//獲得當前的調度週期
	u64 slice = __sched_period(cfs_rq->nr_running + !se->on_rq);

	for_each_sched_entity(se) {
		struct load_weight *load;
		struct load_weight lw;
		//獲取se所在的rq
		cfs_rq = cfs_rq_of(se);
		//獲取cfs_rq的load指針
		load = &cfs_rq->load;
		
		//如果se不在rq就緒列隊中時,會將rq的load_weight和se的load_weight相加賦值給臨時變量lw,臨時指針load也指向lw
		if (unlikely(!se->on_rq)) {
			lw = cfs_rq->load;
		  	lw->weight += se->load.weight;
			lw->inv_weight = 0;
			load = &lw;
		}
		//調用__calc_delta算出應該分到的時間片
		slice = __calc_delta(slice, se->load.weight, load);
	}
	return slice;
}

/*
 * delta_exec * weight / lw.weight
 *   OR
 * (delta_exec * (weight * lw->inv_weight)) >> WMULT_SHIFT
 */
static u64 __calc_delta(u64 delta_exec, unsigned long weight, struct load_weight *lw)
{
......
}

通過代碼看,sched_slice可以得出某個se在一個調度週期裏需要佔用多少時間,sched_slice準備了三個參數:

1.需要被瓜分的時間delta_exec

2.調度實體的權重weight

3.所有調度實體的總權重lw

最後調用__calc_delta算出se分得的時間,__calc_delta的實現很複雜但是註釋寫的很清楚,返回值有兩種情況:

delta_exec * weight / lw.weight 
或 
(delta_exec * (weight * lw->inv_weight)) >> WMULT_SHIFT

第一種算法很好理解:假設兩個進程分10s的CPU時間,AB進程權重分別是2和3,那麼A進程分得10*2/(2+3)= 4s,B進程分得10*3/(2+3)= 6s。那如果權重分別是1和3就需要浮點運算了。

第二種算法就是要解決浮點運算的問題:

 

其實還是一樣的結果,只是先2^WMULT_SHIFT/inv_weight得到的結果避免了weight / lw.weight 兩個整形直接相除的結果精度太低罷了。

 

3.調度實體se中的load_weight是如何得來的?根據是什麼?

 

在do_fork->sched_fork->set_load_weight方法中會對se中的load_weight做如下初始化:
load->weight = sched_prio_to_weight[prio];
load->inv_weight = sched_prio_to_wmult[prio];

從數組名的字面意思都可以猜到,weight是優先級換算來的,根據上面的分析inv_weight自然也可以通過weight換算得來,根據普通進程所有的優先級先把weight和inv_weight提前算好存起來,取的時候以優先級爲索引直接獲得,避免了頻繁運算:

/*
 * Nice levels are multiplicative, with a gentle 10% change for every
 * nice level changed. I.e. when a CPU-bound task goes from nice 0 to
 * nice 1, it will get ~10% less CPU time than another CPU-bound task
 * that remained on nice 0.
 *
 * The "10% effect" is relative and cumulative: from _any_ nice level,
 * if you go up 1 level, it's -10% CPU usage, if you go down 1 level
 * it's +10% CPU usage. (to achieve that we use a multiplier of 1.25.
 * If a task goes up by ~10% and another task goes down by ~10% then
 * the relative distance between them is ~25%.)
 */
const int sched_prio_to_weight[40] = {
 /* -20 */     88761,     71755,     56483,     46273,     36291,
 /* -15 */     29154,     23254,     18705,     14949,     11916,
 /* -10 */      9548,      7620,      6100,      4904,      3906,
 /*  -5 */      3121,      2501,      1991,      1586,      1277,
 /*   0 */      1024,       820,       655,       526,       423,
 /*   5 */       335,       272,       215,       172,       137,
 /*  10 */       110,        87,        70,        56,        45,
 /*  15 */        36,        29,        23,        18,        15,
};

/*
 * Inverse (2^32/x) values of the sched_prio_to_weight[] array, precalculated.
 *
 * In cases where the weight does not change often, we can use the
 * precalculated inverse to speed up arithmetics by turning divisions
 * into multiplications:
 */
const u32 sched_prio_to_wmult[40] = {
 /* -20 */     48388,     59856,     76040,     92818,    118348,
 /* -15 */    147320,    184698,    229616,    287308,    360437,
 /* -10 */    449829,    563644,    704093,    875809,   1099582,
 /*  -5 */   1376151,   1717300,   2157191,   2708050,   3363326,
 /*   0 */   4194304,   5237765,   6557202,   8165337,  10153587,
 /*   5 */  12820798,  15790321,  19976592,  24970740,  31350126,
 /*  10 */  39045157,  49367440,  61356676,  76695844,  95443717,
 /*  15 */ 119304647, 148102320, 186737708, 238609294, 286331153,
};

註釋寫的非常清楚,nice值每提高1級就能多獲取10%的cpu時間,套公式算一下:

假設有兩個進程分別nice值分別是0和1,它們分得的調度週期百分比:
1024/(1024+820) = 55%
820/(1024+820) = 44%
55% - 44% = 10%

4.CFS是根據在什麼情況下會搶佔當前進程?

一個調度器類的方法實現中,主要和搶佔相關的兩個方法:

task_tick:週期調度器更新完統計值就會調用當前進程所在調度器類的task_tick檢查搶佔。

check_preempt_curr:傳入進程描述符P,檢查是否可以搶佔當前進程,一般在fork新進程、喚醒進程、調整某個進程優先級等操作之後用於檢測某個進程是否可以搶佔當前正在運行的進程。

看下task_tick_fair:

 

static void task_tick_fair(struct rq *rq, struct task_struct *curr, int queued)
{
	struct cfs_rq *cfs_rq;
	struct sched_entity *se = &curr->se;

	//不考慮組調度的情況,該循環只走一遍
	for_each_sched_entity(se) {
		cfs_rq = cfs_rq_of(se);
		entity_tick(cfs_rq, se, queued);
	}

}

entity_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr, int queued)
{
	/*
	 * Update run-time statistics of the 'current'.
	 */
	//先統計一把當前進程運行時間
	update_curr(cfs_rq);

	if (cfs_rq->nr_running > 1)
		//檢查要不要調度
		check_preempt_tick(cfs_rq, curr);
}

update_curr負責統計當前cfs_rq運行時間相關的變量:

static void update_curr(struct cfs_rq *cfs_rq)
{
	struct sched_entity *curr = cfs_rq->curr;
	//獲取當前rq去掉被中斷的時間進程運行的總tick數
	u64 now = rq_clock_task(rq_of(cfs_rq));
	u64 delta_exec;

	if (unlikely(!curr))
		return;
	/*+ 算出距離上次調用update_curr統計至今,tick的增量*/
	delta_exec = now - curr->exec_start;
	if (unlikely((s64)delta_exec <= 0))
		return;
	
	curr->exec_start = now;
	/*- 算出距離上次調用update_curr統計至今,tick的增量*/
	
	//把算出來的時間增量加到sum_exec_runtime
	curr->sum_exec_runtime += delta_exec;
	//把算出來的時間增量通過權重換算下加到vruntime
	curr->vruntime += calc_delta_fair(delta_exec, curr);
	
	//更新列隊的min_vruntime
	update_min_vruntime(cfs_rq);

	if (entity_is_task(curr)) {
		struct task_struct *curtask = task_of(curr);

		trace_sched_stat_runtime(curtask, delta_exec, curr->vruntime);
		cpuacct_charge(curtask, delta_exec);
		account_group_exec_runtime(curtask, delta_exec);
	}

	account_cfs_rq_runtime(cfs_rq, delta_exec);
}

static inline u64 calc_delta_fair(u64 delta, struct sched_entity *se)
{
	if (unlikely(se->load.weight != NICE_0_LOAD))
		delta = __calc_delta(delta, NICE_0_LOAD, &se->load);

	return delta;
}

calc_delta_fair負責將時間增量換算成vruntime:

看到這不難發現,CFS在處理高低優先級進程上CPU時間分配上,主要邏輯如下:

1.高優先級分配的CPU時間片更多

2.高優先級的vruntime流逝的更慢

回頭看看check_preempt_tick:

check_preempt_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr)
{
	unsigned long ideal_runtime, delta_exec;
	struct sched_entity *se;
	s64 delta;
	//如上文sched_slice算出來的時間片,至少是保證了最小調度粒度的
	ideal_runtime = sched_slice(cfs_rq, curr);
	//算出當前進程已經運行了多少tick
	delta_exec = curr->sum_exec_runtime - curr->prev_sum_exec_runtime;
	//超出了分配的時間片,設置搶佔
	if (delta_exec > ideal_runtime) {
		resched_curr(rq_of(cfs_rq));
		return;
	}

	/* 下面這段代碼有點詭異,看是和喚醒搶佔相關的
	 * Ensure that a task that missed wakeup preemption by a
	 * narrow margin doesn't have to wait for a full slice.
	 * This also mitigates buddy induced latencies under load.
	 */
	if (delta_exec < sysctl_sched_min_granularity)
		return;

	se = __pick_first_entity(cfs_rq);
	delta = curr->vruntime - se->vruntime;

	if (delta < 0)
		return;

	if (delta > ideal_runtime)
		resched_curr(rq_of(cfs_rq));
}

但是從check_preempt_tick的情況來看,ideal_runtime也就是進程分得的時間片纔是週期性調度器最終決定是否搶佔的標準。

 

再看看check_preempt_wakeup:

static void check_preempt_wakeup(struct rq *rq, struct task_struct *p, int wake_flags)
{
...
	update_curr(cfs_rq_of(se));
	//se是當前進程的調度實體,pse是要判斷的調度實體
	if (wakeup_preempt_entity(se, pse) == 1) {
...
		goto preempt;
	}

	return;

preempt:
	resched_curr(rq);
...
}

wakeup_preempt_entity(struct sched_entity *curr, struct sched_entity *se)
{
	s64 gran, vdiff = curr->vruntime - se->vruntime;

	if (vdiff <= 0)
		return -1;
	gran = wakeup_gran(curr, se);
	if (vdiff > gran)
		return 1;
	if (vdiff > 0) && (vdiff <= gran)
    	return 0;
}

wakeup_gran(struct sched_entity *curr, struct sched_entity *se)
{
	unsigned long gran = sysctl_sched_wakeup_granularity;
	return calc_delta_fair(gran, se);
}

sysctl_sched_wakeup_granularity也是開放給用戶的一個參數,字面意思是喚醒調度粒度,用來防止短時間進出休眠狀態的進程頻繁搶佔導致的性能浪費,默認是1ms,只有噹噹前進程vruntime大於嘗試搶佔的進程的vruntime且大於gran * (NICE_0_LOAD / se->load.weight)時才能搶佔。

 

5.列隊的min_vruntime用在哪?

列隊的min_vruntime其實想算也可算出來,CFS在設計時特地留了個空間放整個列隊的min_vruntime這種以空間換時間的操作證明min_vruntime肯定是有用的。

先看看min_vruntime如何求得的:

 

static void update_min_vruntime(struct cfs_rq *cfs_rq)
{
	struct sched_entity *curr = cfs_rq->curr;
	struct rb_node *leftmost = rb_first_cached(&cfs_rq->tasks_timeline);

	u64 vruntime = cfs_rq->min_vruntime;
	
	if (curr) {
		if (curr->on_rq)
			vruntime = curr->vruntime;
		else
			curr = NULL;
	}
	/**/
	if (leftmost) { /* non-empty tree */
		struct sched_entity *se;
		se = rb_entry(leftmost, struct sched_entity, run_node);

		/*curr既然都不在rq中了,那麼它的vruntime自然就沒有參考意義了*/
		if (!curr)
			vruntime = se->vruntime;
		else /*curr->on_rq is true,先獲取當前運行進程和隊頭進程中較小的vruntime*/
			vruntime = min_vruntime(vruntime, se->vruntime);
	}

	/* ensure we never gain time by being placed backwards. 保證min_vruntime是單調遞增的*/
	cfs_rq->min_vruntime = max_vruntime(cfs_rq->min_vruntime, vruntime);
}

在CFS紅黑樹的進程都是就緒態或者運行態的,它們的vruntime會隨着時間的推移不停地增加,但是有三種情況會給這棵樹帶來新的成員,需要特殊處理防止造成不公平(某個進程的vruntime過大或者過小導致自己被餓死或者餓死其他進程):

1.新進程的加入,會將父進程的當前的vruntime賦值給新進程,再加上一點懲罰值,如果設置內核先運行,那就交換父子進程的vruntime。因爲得到了父進程的vruntime,但是子進程實際運行可能在其他cpu上rq,所以要先減去當前rq的min_vruntime,確定分配到哪個cpu時在加上所在cpu上rq的min_vruntime。

2.從其他調度策略切換到普通調度策略,參考1

3.進程喚醒重新加入到列隊中運行,因爲不在隊中,睡眠進程的vruntime一段實際都不再增加,若不處理直接入隊可能會導致其他進程被餓死。所以在喚醒進程入隊時會以當前rq的min_vruntime再減去一點作爲補償。

上述三個問題的相關代碼:

static void task_fork_fair(struct task_struct *p)
{
	struct cfs_rq *cfs_rq;
	struct sched_entity *se = &p->se, *curr;
	struct rq *rq = this_rq();
	struct rq_flags rf;

	rq_lock(rq, &rf);
	update_rq_clock(rq);

	cfs_rq = task_cfs_rq(current);
	curr = cfs_rq->curr;
	if (curr) {
		update_curr(cfs_rq);
		//將父進程的vruntime拷貝給新進程
		se->vruntime = curr->vruntime;
	}
	place_entity(cfs_rq, se, 1);

	//如果設定了子進程先運行的參數,確保子進程的vruntime比父進程小
	if (sysctl_sched_child_runs_first && curr && entity_before(curr, se)) {
		swap(curr->vruntime, se->vruntime);
		resched_curr(rq);
	}
	
	//進程在這個cpu創建並非一定會在這個進程運行,入隊到真正要執行的CPU上的rq時,會加上rq的min_vruntime
	se->vruntime -= cfs_rq->min_vruntime;
	rq_unlock(rq, &rf);
}

place_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int initial)
{
	u64 vruntime = cfs_rq->min_vruntime;

	//新進程的vruntime要加上,自己時間片*(NICE_0_LOAD/weight)
	if (initial && sched_feat(START_DEBIT))
		vruntime += sched_vslice(cfs_rq, se);

	
	//剛被喚醒的進程vruntime要減去調度延遲或者調度延遲的一半
	if (!initial) {
		unsigned long thresh = sysctl_sched_latency;

		/*
		 * Halve their sleep time's effect, to allow
		 * for a gentler effect of sleepers:
		 */
		if (sched_feat(GENTLE_FAIR_SLEEPERS))
			thresh >>= 1;

		vruntime -= thresh;
	}

	/* ensure we never gain time by being placed backwards. */
	se->vruntime = max_vruntime(se->vruntime, vruntime);
}

 

 

 

 

 

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章