Linux的公平調度(CFS)原理


原文鏈接:https://www.jianshu.com/p/673c9e4817a8

參考:https://blog.csdn.net/yiyeguzhou100/article/details/74906510?utm_medium=distribute.pc_relevant.none-task-blog-2~default~baidujs_title~default-4.base&spm=1001.2101.3001.4242

CFS中一些調度參數的實現原理 https://blog.csdn.net/wh8_2011/article/details/52042035

CFS-完全公平調度器https://blog.csdn.net/crazy_koala/article/details/104196387?utm_medium=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-1.base&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-1.base

O(n)、O(1)和CFS調度器http://www.wowotech.net/process_management/scheduler-history.html

Linux進程調度與管理  https://blog.csdn.net/qq_38712943/category_7584059.html  

Linux調度器 https://blog.csdn.net/wudongxu

Linux CFS調度算法核心解析 https://blog.csdn.net/dog250/article/details/96012688?utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-3.base&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-3.base

1、CFS的基本思路

在CFS算法引入之前,Linux使用過幾種不同的調度算法,一開始的調度器是復雜度為O(n)的始調度算法(實際上每次會遍歷所有任務,所以復雜度為O(n)), 這個算法的缺點是當內核中有很多任務時,調度器本身就會耗費不少時間,所以,從linux2.5開始引入赫赫有名的O(1)調度器,然而,linux是集全球很多程序員的聰明才智而發展起來的超級內核,沒有最好,只有更好,在O(1)調度器風光了沒幾天就又被另一個更優秀的調度器取代了,它就是CFS調度器Completely Fair Scheduler. 這個也是在2.6內核中引入的,具體為2.6.23,即從此版本開始,內核使用CFS作為它的默認調度器,O(1)調度器被拋棄了。:

  • O(n)調度:內核調度算法理解起來簡單:在每次進程切換時,內核依次掃描就緒隊列上的每一個進程,計算每個進程的優先級,再選擇出優先級最高的進程來運行;盡管這個算法理解簡單,但是它花費在選擇優先級最高進程上的時間卻不容忽視。系統中可運行的進程越多,花費的時間就越大,時間復雜度為O(n)
  • O(1)調度:其基本思想是根據進程的優先級進行調度。進程有兩個優先級,一個是靜態優先級,一個是動態優先級.靜態優先級是用來計算進程運行的時間片長度的,動態優先級是在調度器進行調度時用到的,調度器每次都選取動態優先級最高的進程運行.由於其數據結構設計上采用了一個優先級數組,這樣在選擇最優進程時時間復雜度為O(1),所以被稱為O(1)調度。

這兩種調度算法,其基本思路都是通過一系列運行指標確定進程的優先級,然后根據進程的優先級確定調度哪個進程,而CFS則轉換了一種思路,它不計算優先級,而是通過計算進程消耗的CPU時間(標准化以后的虛擬CPU時間)來確定誰來調度。從而到達所謂的公平性。

  • 絕對公平性:
    cfs定義了一種新的模型,其基本思路很簡單,他把CPU當做一種資源,並記錄下每一個進程對該資源使用的情況,在調度時,調度器總是選擇消耗資源最少的進程來運行。這就是所謂的“完全公平”。但這種絕對的公平有時也是一種不公平,因為有些進程的工作比其他進程更重要,我們希望能按照權重來分配CPU資源。
  • 相對公平性:
    為了區別不同優先級的進程,就是會根據各個進程的權重分配運行時間(權重怎么來的后面再說)。進程的運行時間計算公式為:

分配給進程的運行時間 = 調度周期 * 進程權重 / 所有進程權重之和 (公式1)
調度周期很好理解,就是將所處於TASK_RUNNING態進程都調度一遍的時間。

舉個例子來說明一下,比如系統中只兩個進程A, B,權重分別為1和2,假設調度周期設為30ms,那么分配給A的CPU時間為:30ms * (1/(1+2)) = 10ms;而B的CPU時間為:30ms * (2/(1+2)) = 20ms。那么在這30ms中A將運行10ms,B將運行20ms。

2、實現原理

在實現層面,Linux通過引入virtual runtime(vruntime)來完成上面的設想,具體的,我們來看下從實際運行時間到vruntime的換算公式

vruntime = 實際運行時間 * 1024 / 進程權重 。 (公式2)

實際上vruntime就是根據權重將實際運行時間標准化,標准化之后,各個進程對資源的消耗情況就可以直接通過比較vruntime來知道,比如某個進程的vruntime比較小,我們就可以知道這個進程消耗CPU資源比較少,反之消耗CPU資源就比較多。

有了vruntime的概念后,調度算法就非常簡單了,誰的vruntime值較小就說明它以前占用cpu的時間較短,受到了“不公平”對待,因此下一個運行進程就是它。這樣既能公平選擇進程,又能保證高優先級進程獲得較多的運行時間。這就是CFS的主要思想了。
或者可以這么理解:CFS的思想就是讓每個調度實體(沒組調度的情形下就是進程,以后就說進程了)的vruntime互相追趕,而每個調度實體的vruntime增加速度不同,權重越大的增加的越慢,這樣就能獲得更多的cpu執行時間。

具體實現上,Linux采用了一顆紅黑樹(對於多核調度,實際上每一個核有一個自己的紅黑樹),記錄下每一個進程的vruntime,需要調度時,從紅黑樹中選取一個vruntime最小的進程出來運行。

3、更多的細節

3.1、權重如何決定

權重由nice值確定,具體的,權重跟進程nice值之間有一一對應的關系,可以通過全局數組prio_to_weight來轉換,nice值越大,權重越低。

nice值共有40個,與權重之間,每一個nice值相差10%左右。 static const int 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, }; 

3.2、新創建進程的vruntime是多少?

假如新進程的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打開,意味着創建子進程后,保證子進程會在父進程之前運行。

創建子進程的具體流程如下:

  1. 子進程在創建時,vruntime初值首先被設置為min_vruntime;
  2. 然后,如果sched_features中設置了START_DEBIT位,vruntime會在min_vruntime的基礎上再增大一些。
  3. 設置完子進程的vruntime之后,檢查sched_child_runs_first參數,如果為1的話,就比較父進程和子進程的vruntime,若是父進程的vruntime更小,就對換父、子進程的vruntime,這樣就保證了子進程會在父進程之前運行。

3.3、休眠進程的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; } 

3.4、休眠進程在喚醒時會立刻搶占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 1
# echo NO_WAKEUP_PREEMPT > /sys/kernel/debug/sched_features

禁用喚醒搶占 特性之后,剛喚醒的進程不會立即搶占運行中的進程,而是要等到運行進程用完時間片之后。在以上案例中,經過這樣的調整之后B進程被搶占的頻率大大降低了,整體性能得到了改善。
如果禁止喚醒搶占特性對你的系統來說太過激進的話,你還可以選擇調大以下參數:

sched_wakeup_granularity_ns
這個參數限定了一個喚醒進程要搶占當前進程之前必須滿足的條件:只有當該喚醒進程的vruntime比當前進程的vruntime小、並且兩者差距(vdiff)大於sched_wakeup_granularity_ns的情況下,才可以搶占,否則不可以。這個參數越大,發生喚醒搶占就越不容易。

3.5、進程占用的CPU時間片可以無窮小嗎?

假設有兩個進程,它們的vruntime初值都是一樣的,第一個進程只要一運行,它的vruntime馬上就比第二個進程更大了,那么它的CPU會立即被第二個進程搶占嗎?CFS是這樣做的:為了避免過於短暫的進程切換造成太大的消耗,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 * 進程數量) 的乘積為准。

3.6、進程從一個CPU遷移到另一個CPU上的時候vruntime會不會變?

在多CPU的系統上,不同的CPU的負載不一樣,有的CPU更忙一些,而每個CPU都有自己的運行隊列,每個隊列中的進程的vruntime也走得有快有慢,比如我們對比每個運行隊列的min_vruntime值,都會有不同:

# grep min_vruntime /proc/sched_debug
.min_vruntime : 12403175.972743
.min_vruntime : 14422108.528121

# 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; ... } 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; ... } 

3.7、Vruntime溢出問題

之前說過紅黑樹中實際的作為key的不是vruntime而是vruntime - min_vruntime。min_vruntime是當前紅黑樹中最小的key。這是為什么呢,我們先看看vruntime 的類型,是usigned long類型的,再看看key的類型,是signed long類型的,因為進程的虛擬時間是一個遞增的正值,因此它不會是負 數,但是它有它的上限,就是unsigned long所能表示的最大值,如果溢出了,那么它就會從0開始回滾,如果這樣的話,結果會怎樣?結果很嚴重 啊,就是說會本末倒置的,比如以下例子,以unsigned char說明問題:

unsigned char a = 251,b = 254; b += 5;//到此判斷a和b的大小看看上面的例子,b回滾了,導致a遠遠大於b,其實真正的結果應該是b比a大8,怎么做到真正的結果呢?改為以下: unsigned char a = 251,b = 254; b += 5; signed char c = a - 250,d = b - 250;//到此判斷c和d的大小 

結果正確了,要的就是這個效果,可是進程的vruntime怎么用unsigned long類型而不處理溢出問題呢?因為這個vruntime的作用就是 推進虛擬時鍾,並沒有別的用處,它可以不在乎,然而在計算紅黑樹的key的時候就不能不在乎了,於是減去一個最小的vruntime將所有進程的key圍 繞在最小vruntime的周圍,這樣更加容易追蹤。運行隊列的min_vruntime的作用就是處理溢出問題的。



作者:kummerwu
鏈接:https://www.jianshu.com/p/673c9e4817a8
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM