本文轉載自從幾個問題開始理解CFS調度器
導語
CFS(完全公平調度器)是Linux內核2.6.23版本開始采用的進程調度器,它的基本原理是這樣的:設定一個調度周期(sched_latency_ns
),目標是讓每個進程在這個周期內至少有機會運行一次,換一種說法就是每個進程等待CPU的時間最長不超過這個調度周期;然后根據進程的數量,大家平分這個調度周期內的CPU使用權,由於進程的優先級即nice值不同,分割調度周期的時候要加權;每個進程的累計運行時間保存在自己的vruntime
字段里,哪個進程的vruntime
最小就獲得本輪運行的權利。
如果你覺沒有從cfs中看到什么,那么最簡單我告訴你,就是提高了響應速度,為何呢?在shell下執行vmstat,觀察cs字段,也就是1秒內進程切換的次數,然后盡量滿載系統,觀察cs和誰有關,它的值和進程的數量,進程的優先級有關,當進程的優先級很大的時候,cs的值就會減少,反之cs值會增加,我們看看這是為什么,O(1)調度器中,時間片的分配是絕對的分配,也就是說排除進程飢餓和內核搶占,每個進程運行的時間片是絕對的,是按照它的nice值計算出來的,如果說有變動,那也只是由於優先級動態調整導致的變化,那時的時間片只和進程的優先級有關,優先級變化幅度不大,時間片調整的也不多,因此我們不考慮優先級調整,一旦系統負載增加,調度完整個系統的所有進程的時間就會很長,活動進程數量越多,這一段時間就會越長,O(1)調度器中的時間片是固定的,那么cfs中的呢?
cfs中的時間片是動態分配的,是按照比例分配的而不是按照優先級固定分配的,其精髓就是系統擁有一個可配置的系統調度周期,在該周期內運行完所有的進程,如果系統負載高了,那么每一個進程在該周期內被分配的時間片都會減少,將這些進程減少的部分累積正好就是新進程的時間片,其實完全可以實現一個更簡單的cfs版本,按照固定的順序運行進程,就是將紅黑樹退化成一個先入先出的隊列,每個進程都排入進程,然后運行完按比例分配給它的動態時間片之后排入隊列最后,然后繼續下一個進程,如此反復,但是這樣實現的話很不靈活,很難實現內核的實時搶占和新進程的搶占以及進程喚醒后的補償,於是紅黑樹完美的解決了這一個問題,於是就出現了虛擬時鍾的概念,每一個進程都有一個虛擬時鍾,按照不同的速率在每一個物理時鍾節拍內向前推進,越高權值的進程的推進速率越慢,這樣它就可以運行更多的物理時間。cfs調度器選擇當前最慢的虛擬時鍾進行推進,做到了公平。
cfs調度器中分配的動態時間片和HZ沒有關系,它只和進程的權值以及當前紅黑樹進程的總權值還有調度周期有關,它本質上是一個相對的概念而不像以前是一個絕對的概念,相對的概念就是比絕對的概念要靈活,比如用相對目錄的程序就比用絕對目錄的程序擁有更好的移植性。cfs調度器將時間片的概念進行了相對了,抽象出了虛擬時鍾的概念,如此一來1秒鍾內的進程切換次數就不再和進程優先值有關了,而是和調度周期和進程數量有關,理論上就是(進程數量)*(1秒/調度周期)次,當然加上搶占和新進程創建就不是這么理想了,經過試驗,和理論數據差別不大,在同一負載下,提高或者降低多個進程的優先級在O(1)調度器下會引起vmstat中cs的變化,但是在cfs中vmstat中的cs值卻不會受到影響。
那么問題就來了:
新進程的vruntime的初值是不是0啊?
假如新進程的vruntime
初值為0的話,比老進程的值小很多,那么它在相當長的時間內都會保持搶占CPU的優勢,老進程就要餓死了,這顯然是不公平的。所以CFS是這樣做的:每個CPU的運行隊列cfs_rq
都維護一個min_vruntime
字段,記錄該運行隊列中所有進程的vruntime
最小值,新進程的初始vruntime
值就以它所在運行隊列的min_vruntime
為基礎來設置,與老進程保持在合理的差距范圍內。參見后面的源代碼。
新進程的vruntime
初值的設置與兩個參數有關:
sched_child_runs_first
:規定fork
之后讓子進程先於父進程運行;
sched_features
的START_DEBIT
位:規定新進程的第一次運行要有延遲。
注: sched_features是控制調度器特性的開關,每個bit表示調度器的一個特性。在sched_features.h文件中記錄了全部的特性。START_DEBIT是其中之一,如果打開這個特性,表示給新進程的vruntime初始值要設置得比默認值更大一些,這樣會推遲它的運行時間,以防進程通過不停的fork來獲得cpu時間片。
如果參數 sched_child_runs_first打開,意味着創建子進程后,保證子進程會在父進程之前運行。
子進程在創建時,vruntime
初值首先被設置為min_vruntime
;然后,如果sched_features
中設置了START_DEBIT
位,vruntime
會在min_vruntime
的基礎上再增大一些。設置完子進程的vruntime
之后,檢查sched_child_runs_first
參數,如果為1的話,就比較父進程和子進程的vruntime
,若是父進程的vruntime
更小,就對換父、子進程的vruntime
,這樣就保證了子進程會在父進程之前運行。
休眠進程的vruntime一直保持不變嗎?
如果休眠進程的 vruntime
保持不變,而其他運行進程的 vruntime
一直在推進,那么等到休眠進程終於喚醒的時候,它的vruntime
比別人小很多,會使它獲得長時間搶占CPU的優勢,其他進程就要餓死了。這顯然是另一種形式的不公平。CFS是這樣做的:在休眠進程被喚醒時重新設置vruntime
值,以min_vruntime
值為基礎,給予一定的補償,但不能補償太多。
static void
place_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int initial)
{
u64 vruntime = cfs_rq->min_vruntime;
/*
* The 'current' period is already promised to the current tasks,
* however the extra weight of the new task will slow them down a
* little, place the new task so that it fits in the slot that
* stays open at the end.
*/
if (initial && sched_feat(START_DEBIT)) /* initial表示新進程 */
vruntime += sched_vslice(cfs_rq, se);
/* sleeps up to a single latency don't count. */
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)) /* 若設了GENTLE_FAIR_SLEEPERS */
thresh >>= 1; /* 補償減為調度周期的一半 */
vruntime -= thresh;
}
/* ensure we never gain time by being placed backwards. */
vruntime = max_vruntime(se->vruntime, vruntime);
se->vruntime = vruntime;
}
休眠進程在喚醒時會立刻搶占CPU嗎?
這是由CFS的喚醒搶占 特性決定的,即sched_features
的*WAKEUP_PREEMPT`位。
由於休眠進程在喚醒時會獲得vruntime
的補償,所以它在醒來的時候有能力搶占CPU是大概率事件,這也是CFS調度算法的本意,即保證交互式進程的響應速度,因為交互式進程等待用戶輸入會頻繁休眠。除了交互式進程以外,主動休眠的進程同樣也會在喚醒時獲得補償,例如通過調用sleep
()、nanosleep
()的方式,定時醒來完成特定任務,這類進程往往並不要求快速響應,但是CFS不會把它們與交互式進程區分開來,它們同樣也會在每次喚醒時獲得vruntime
補償,這有可能會導致其它更重要的應用進程被搶占,有損整體性能。
我曾經處理過一個案例,服務器上有兩類應用進程:
A進程定時循環檢查有沒有新任務,如果有的話就簡單預處理后通知B進程,然后調用nanosleep
()主動休眠,醒來后再重復下一個循環;
B進程負責數據運算,是CPU消耗型的;
B進程的運行時間很長,而A進程每次運行時間都很短,但睡眠/喚醒卻十分頻繁,每次喚醒就會搶占B,導致B的運行頻繁被打斷,大量的進程切換帶來很大的開銷,整體性能下降很厲害。
那有什么辦法嗎?有,最后我們通過禁止CFS喚醒搶占 特性解決了問題:
# echo NO_WAKEUP_PREEMPT > /sys/kernel/debug/sched_features
禁用喚醒搶占 特性之后,剛喚醒的進程不會立即搶占運行中的進程,而是要等到運行進程用完時間片之后。在以上案例中,經過這樣的調整之后B進程被搶占的頻率大大降低了,整體性能得到了改善。
如果禁止喚醒搶占特性對你的系統來說太過激進的話,你還可以選擇調大以下參數:
sched_wakeup_granularity_ns
這個參數限定了一個喚醒進程要搶占當前進程之前必須滿足的條件:只有當該喚醒進程的vruntime
比當前進程的vruntime
小、並且兩者差距(vdiff)大於sched_wakeup_granularity_ns
的情況下,才可以搶占,否則不可以。這個參數越大,發生喚醒搶占就越不容易。
進程占用的CPU時間片可以無窮小嗎?
假設有兩個進程,它們的vruntime
初值都是一樣的,第一個進程只要一運行,它的vruntime
馬上就比第二個進程更大了,那么它的CPU會立即被第二個進程搶占嗎?答案是這樣的:為了避免過於短暫的進程切換造成太大的消耗,CFS設定了進程占用CPU的最小時間值,sched_min_granularity_ns
,正在CPU上運行的進程如果不足這個時間是不可以被調離CPU的。
sched_min_granularity_ns
發揮作用的另一個場景是,本文開門見山就講過,CFS把調度周期sched_latency
按照進程的數量平分,給每個進程平均分配CPU時間片(當然要按照nice值加權,為簡化起見不再強調),但是如果進程數量太多的話,就會造成CPU時間片太小,如果小於sched_min_granularity_ns
的話就以sched_min_granularity_ns
為准;而調度周期也隨之不再遵守sched_latency_ns
,而是以 (sched_min_granularity_ns
* 進程數量) 的乘積為准。
進程從一個CPU遷移到另一個CPU上的時候vruntime會不會變?
在多CPU的系統上,不同的CPU的負載不一樣,有的CPU更忙一些,而每個CPU都有自己的運行隊列,每個隊列中的進程的vruntime
也走得有快有慢,比如我們對比每個運行隊列的min_vruntime
值,都會有不同:
# grep min_vruntime /proc/sched_debug
.min_vruntime : 12403175.972743
.min_vruntime : 14422108.528121
如果一個進程從min_vruntime
更小的CPU (A) 上遷移到min_vruntime
更大的CPU (B) 上,可能就會占便宜了,因為CPU (B) 的運行隊列中進程的vruntime
普遍比較大,遷移過來的進程就會獲得更多的CPU時間片。這顯然不太公平。
CFS是這樣做的:
當進程從一個CPU的運行隊列中出來 (dequeue_entity
) 的時候,它的vruntime
要減去隊列的min_vruntime
值;
而當進程加入另一個CPU的運行隊列 ( enqueue_entiry
) 時,它的vruntime
要加上該隊列的min_vruntime
值。
這樣,進程從一個CPU遷移到另一個CPU之后,vruntime
保持相對公平。
static void
dequeue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags)
{
...
/*
* Normalize the entity after updating the min_vruntime because the
* update can refer to the ->curr item and we need to reflect this
* movement in our normalized position.
*/
if (!(flags & DEQUEUE_SLEEP))
se->vruntime -= cfs_rq->min_vruntime;
...
}
static void
enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags)
{
/*
* Update the normalized vruntime before updating min_vruntime
* through callig update_curr().
*/
if (!(flags & ENQUEUE_WAKEUP) || (flags & ENQUEUE_WAKING))
se->vruntime += cfs_rq->min_vruntime;
...
}