1.1 CFS原理
cfs定義了一種新的模型,它給cfs_rq(cfs的run queue)中的每一個進程安排一個虛擬時鍾,vruntime。如果一個進程得以執行,隨着時間的增長(也就是一個個tick的到來),其vruntime將不斷增大。沒有得到執行的進程vruntime不變。
而調度器總是選擇vruntime跑得最慢的那個進程來執行。這就是所謂的“完全公平”。為了區別不同優先級的進程,優先級高的進程vruntime增長得慢,以至於它可能得到更多的運行機會。
1.2 CFS基本設計思路
CFS思路很簡單,就是根據各個進程的權重分配運行時間(權重怎么來的后面再說)。
進程的運行時間計算公式為:
分配給進程的運行時間 = 調度周期 * 進程權重 / 所有進程權重之和 (公式1)
調度周期很好理解,就是將所有處於TASK_RUNNING態進程都調度一遍的時間,差不多相當於O(1)調度算法中運行隊列和過期隊列切換一次的時間(對O(1)調度算法看得不是很熟,如有錯誤還望各位大蝦指出)。舉個例子,比如只有兩個進程A, B,權重分別為1和2,調度周期設為30ms,那么分配給A的CPU時間為:30ms * (1/(1+2)) = 10ms;而B的CPU時間為:30ms * (2/(1+2)) = 20ms。那么在這30ms中A將運行10ms,B將運行20ms。
公平怎么體現呢?它們的運行時間並不一樣阿?
其實公平是體現在另外一個量上面,叫做virtual runtime(vruntime),它記錄着進程已經運行的時間,但是並不是直接記錄,而是要根據進程的權重將運行時間放大或者縮小一個比例。
我們來看下從實際運行時間到vruntime的換算公式
vruntime = 實際運行時間 * 1024 / 進程權重 。 (公式2)
為了不把大家搞暈,這里我直接寫1024,實際上它等於nice為0的進程的權重,代碼中是NICE_0_LOAD。也就是說,所有進程都以nice為0的進程的權重1024作為基准,計算自己的vruntime增加速度。還以上面AB兩個進程為例,B的權重是A的2倍,那么B的vruntime增加速度只有A的一半。現在我們把公式2中的實際運行時間用公式1來替換,可以得到這么一個結果:
vruntime = (調度周期 * 進程權重 / 所有進程總權重) * 1024 / 進程權重 = 調度周期 * 1024 / 所有進程總權重
看出什么眉目沒有?沒錯,雖然進程的權重不同,但是它們的 vruntime增長速度應該是一樣的 ,與權重無關。好,既然所有進程的vruntime增長速度宏觀上看應該是同時推進的,
那么就可以用這個vruntime來選擇運行的進程,誰的vruntime值較小就說明它以前占用cpu的時間較短,受到了“不公平”對待,因此下一個運行進程就是它。這樣既能公平選擇進程,又能保證高優先級進程獲得較多的運行時間。這就是CFS的主要思想了。
或者可以這么理解:CFS的思想就是讓每個調度實體(沒有組調度的情形下就是進程,以后就說進程了)的vruntime互相追趕,而每個調度實體的vruntime增加速度不同,權重越大的增加的越慢,這樣就能獲得更多的cpu執行時間。
再補充一下權重的來源,權重跟進程nice值之間有一一對應的關系,可以通過全局數組prio_to_weight來轉換,nice值越大,權重越低。
1.3 CFS數據結構
介紹代碼之前先介紹一下CFS相關的結構
第一個是調度實體sched_entity,它代表一個調度單位,在組調度關閉的時候可以把他等同為進程。每一個task_struct中都有一個sched_entity,進程的vruntime和權重都保存在這個結構中。那么所有的sched_entity怎么組織在一起呢?紅黑樹。所有的sched_entity以vruntime為key(實際上是以vruntime-min_vruntime為key,是為了防止溢出,反正結果是一樣的)插入到紅黑樹中,同時緩存樹的最左側節點,也就是vruntime最小的節點,這樣可以迅速選中vruntime最小的進程。
注意只有等待CPU的就緒態進程在這棵樹上,睡眠進程和正在運行的進程都不在樹上。
1.4 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的作用就是處理溢出問題的。
1.5 組調度
關於組調度,詳見:《linux組調度淺析 》。簡單來說,引入組調度是為了實現做一件事的一組進程與做另一件事的另一組進程的隔離。每件“事情”各自有自己的權重,而不管它需要使用多少進程來完成。在cfs中,task_group和進程是同等對待的,task_group的優先級也由用戶來控制(通過cgroup文件cpu.shares)。
實現上,task_group和進程都被抽象成schedule_entity(調度實體,以下簡稱se),上面說到的vruntime、load、等這些東西都被封裝在se里面。而task_group除了有se之外,還有cfs_rq。屬於這個task_group的進程就被裝在它的cfs_rq中(“組”不僅是一個被調度的實體,也是一個容器)。組調度引入以后,一系列task_group的cfs_rq組成了一個樹型結構。樹根是cpu所對應的cfs_rq(也就是root group的cfs_rq)、樹的中間節點是各個task_group的cfs_rq、葉子節點是各個進程。
在一個task_group的兩頭,是兩個不同的世界,就像《盜夢空間》里不同層次的夢境一樣。
以group-1為例,它所對應的se被加入到父組(cpu_rq)的cfs_rq中,接受調度。這個se有自己的load(由對應的cpu.shares文件來配置),不管group-1下面有多少個進程,這個load都是這個值。父組看不到、也不關心group-1下的進程。父組只會根據這個se的load和它執行的時間來更新其vruntime。當group-1被調度器選中后,會繼續選擇其下面的task-11或task-12來執行。這里又是一套獨立的體系,task-11與task-12的vruntime、load、等這些東西只影響它們在group-1的cfs_rq中的調度情況。樹型結構中的每一個cfs_rq都是獨立完成自己的調度邏輯。不過,從cpu配額上看,task_group的配額會被其子孫層層瓜分。
例如上圖中的task-11,它所在的group-1對應se的load是8,而group-1下兩個進程的load是9和3,task-11占其中的3/4。於是,在group-1所對應的cfs_rq內部看,task-11的load是9,而從全局來看,task-11的load是8*3/4=6。而task_group下的進程的時間片也是這樣層層瓜分而來的,比如說group-1的cfs_rq下只有兩個進程,計算得來的調度延遲是20ms。但是task-11並非占其中的3/4(15ms)。因為group-1的se的load占總額的8/(8+3+5)=1/2,所以task-11的load占總額的1/2*3/4=3/8,時間片是20ms*3/8=7.5ms。
這樣的瓜分有可能使得task_group里面的進程分得很小的時間片,從而導致頻繁re-schedule。不過好在這並不影響task_group外面的其他進程,並且也可以盡量讓task_group里面的進程在每個調度延遲內都執行一次。
cfs曾經有過時間片不層層瓜分的實現,比如上圖中的task-11,時間片算出來是15ms就是15ms,不用再瓜分了。這樣做的好處是不會有頻繁的re-schedule。但是task_group里的進程可能會很久才被執行一次。瓜分與不瓜分兩種方案的示例如下(還是繼續上圖的例子,深藍色代表task-11、淺藍色是task-12,空白是其他進程):
兩種方案好像很難說清孰優孰劣,貌似cfs也在這兩種方案間糾結了好幾次。
在進程用完其時間片之前,有可能它所在的task_group的se先用完了時間片,而被其父組re-schedule掉。這種情況下,當這個task_group的se再一次被其父組選中時,上次得到執行、且尚未用完時間片的那個進程將繼續運行,直到它用完時間片。(cfs_rq->last會記錄下這個尚未用完時間片的進程。)
1.6 CFS小結
CFS還有一個重要特點,即調度粒度小。CFS之前的調度器中,除了進程調用了某些阻塞函數而主動參與調度之外,每個進程都只有在用完了時間片或者屬於自己的時間配額之后才被搶占。而CFS則在每次tick都進行檢查,如果當前進程不再處於紅黑樹的左邊,就被搶占。在高負載的服務器上,通過調整調度粒度能夠獲得更好的調度性能。
感謝關注 Ithao123Linux頻道,ithao123.cn是專門為互聯網人打造的學習交流平台,全面滿足互聯網人工作與學習需求,更多互聯網資訊盡在 IThao123!