從幾個問題開始理解CFS調度器

從幾個問題開始理解CFS調度器

2019/01/17 VMUNIX

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值爲基礎,給予一定的補償,但不能補償太多。

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

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喚醒搶佔 特性解決了問題:

 

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值,都會有不同:

 

1

2

3

# 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保持相對公平。

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

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;

...

}

 

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