CFS(完全公平調度器)是Linux內核2.6.23版本開始采用的進程調度器,它的基本原理是這樣的:設定一個調度周期(sched_latency_ns),目標是讓每個進程在這個周期內至少有機會運行一次,換一種說法就是每個進程等待CPU的時間最長不超過這個調度周期;然後根據進程的數量,大家平分這個調度周期內的CPU使用權,由於進程的優先級即nice值不同,分割調度周期的時候要加權;每個進程的累計運行時間保存在自己的vruntime字段裡,哪個進程的vruntime最小就獲得本輪運行的權利。
那麼問題就來了:
新進程的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值為基礎,給予一定的補償,但不能補償太多。
123456789101112131415161718192021222324252627282930313233static voidplace_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可以禁止喚醒搶占 特性:1#
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值,都會有不同:
123# 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保持相對公平。
12
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25static
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;
...
}