CFS 排程器

1. 概述

CFS 代表“完全公平排程器”,是由 Ingo Molnar 實現併合併到 Linux 2.6.23 中的“桌面”程序排程器。 最初合併時,它是對先前 vanilla 排程器的 SCHED_OTHER 互動性程式碼的替代。 如今,CFS 正在為 EEVDF 讓路,有關 EEVDF 的文件可以在 EEVDF 排程器 中找到。

CFS 的 80% 的設計可以用一句話概括:CFS 基本上在真實硬體上模擬了一個“理想的、精確的多工 CPU”。

“理想的多工 CPU”是一個(不存在的 :-))CPU,它具有 100% 的物理能力,並且可以以精確相同的速度並行執行每個任務,每個任務的速度為 1/nr_running。 例如:如果有 2 個任務正在執行,則它以 50% 的物理能力執行每個任務 --- 也就是說,實際上是並行執行。

在真實的硬體上,我們一次只能執行一個任務,因此我們必須引入“虛擬執行時間”的概念。 任務的虛擬執行時間指定了其下一個時間片將在上述理想多工 CPU 上開始執行的時間。 實際上,任務的虛擬執行時間是其實際執行時間,並已標準化為正在執行的任務的總數。

2. 一些實現細節

在 CFS 中,虛擬執行時間透過每個任務的 p->se.vruntime(納秒單位)值來表達和跟蹤。 這樣,就可以準確地對任務應該獲得的“預期 CPU 時間”進行時間戳和測量。

小細節:在“理想”硬體上,在任何時候,所有任務都將具有相同的 p->se.vruntime 值 --- 也就是說,任務將同時執行,並且沒有任務會因“理想”的 CPU 時間份額而“失去平衡”。

CFS 的任務選擇邏輯基於此 p->se.vruntime 值,因此非常簡單:它總是嘗試執行具有最小 p->se.vruntime 值的任務(即,到目前為止執行最少的任務)。 CFS 始終嘗試在可執行任務之間儘可能接近“理想的多工硬體”地分配 CPU 時間。

CFS 的其餘大部分設計都源於這個非常簡單的概念,並添加了一些額外的修飾,例如 nice 級別、多處理和各種演算法變體來識別睡眠者。

3. RBTREE

CFS 的設計非常激進:它不使用舊的資料結構用於執行佇列,而是使用時間排序的 rbtree 來構建未來任務執行的“時間線”,因此沒有“陣列切換”偽像(之前的 vanilla 排程器和 RSDL/SD 都受到影響)。

CFS 還維護 rq->cfs.min_vruntime 值,該值是一個單調遞增的值,用於跟蹤執行佇列中所有任務中最小的 vruntime。 系統完成的總工作量使用 min_vruntime 跟蹤; 該值用於將新啟用的實體儘可能放置在樹的左側。

執行佇列中正在執行的任務總數透過 rq->cfs.load 值來計算,該值是在執行佇列中排隊的任務的權重的總和。

CFS 維護一個時間排序的 rbtree,其中所有可執行的任務都按 p->se.vruntime 鍵排序。 CFS 從這棵樹中選擇“最左邊”的任務並堅持下去。 隨著系統向前推進,執行的任務越來越多地放置在樹的右側 --- 緩慢但肯定地讓每個任務都有機會成為“最左邊的任務”,從而在確定的時間內在 CPU 上執行。

總而言之,CFS 的工作方式如下:它執行一個任務一會兒,當任務排程(或發生排程程式滴答)時,任務的 CPU 使用率被“記錄下來”:它剛剛花費在物理 CPU 上的(少量)時間被新增到 p->se.vruntime。 一旦 p->se.vruntime 足夠高,以至於另一個任務成為它維護的時間排序 rbtree 的“最左邊的任務”(加上相對於最左邊的任務的少量“粒度”距離,以便我們不會過度排程任務並破壞快取),然後選擇新的最左邊的任務並搶佔當前任務。

4. CFS 的一些特性

CFS 使用納秒粒度記帳,並且不依賴於任何節拍或其他 HZ 細節。 因此,CFS 排程器沒有先前排程器那樣的“時間片”概念,也沒有任何啟發式方法。 只有一箇中心可調引數

/sys/kernel/debug/sched/base_slice_ns

可用於將排程器從“桌面”(即低延遲)調整為“伺服器”(即良好的批處理)工作負載。 它預設為適合桌面工作負載的設定。 SCHED_BATCH 也由 CFS 排程器模組處理。

如果 CONFIG_HZ 導致 base_slice_ns < TICK_NSEC,則 base_slice_ns 的值對工作負載幾乎沒有影響。

由於其設計,CFS 排程器不易受到當今針對庫存排程器的啟發式方法的任何“攻擊”:fiftyp.c、thud.c、chew.c、ring-test.c、massive_intr.c 都可以正常工作,並且不會影響互動性,並且產生預期的行為。

與之前的 vanilla 排程器相比,CFS 排程器對 nice 級別和 SCHED_BATCH 的處理要強得多:兩種型別的工作負載都被更積極地隔離。

SMP 負載平衡已被重新設計/清理:執行佇列遍歷假設已從負載平衡程式碼中刪除,並且使用了排程模組的迭代器。 結果,平衡程式碼變得簡單得多。

5. 排程策略

CFS 實現了三種排程策略

  • SCHED_NORMAL(傳統上稱為 SCHED_OTHER):用於常規任務的排程策略。

  • SCHED_BATCH:不會像常規任務那樣頻繁地搶佔,從而允許任務執行更長時間並更好地利用快取,但以互動性為代價。 這非常適合批處理作業。

  • SCHED_IDLE:這甚至比 nice 19 還要弱,但它不是真正的空閒計時器排程器,以避免陷入優先順序反轉問題,這會使機器死鎖。

SCHED_FIFO/_RR 在 sched/rt.c 中實現,並且符合 POSIX 規範。

來自 util-linux-ng 2.13.1.1 的命令 chrt 可以設定所有這些,除了 SCHED_IDLE。

6. 排程類

新的 CFS 排程器被設計成引入“排程類”的方式,它是排程器模組的可擴充套件層次結構。 這些模組封裝了排程策略細節,並由排程器核心處理,而核心程式碼對此假設不多。

sched/fair.c 實現了上述 CFS 排程器。

sched/rt.c 以比之前的 vanilla 排程器更簡單的方式實現了 SCHED_FIFO 和 SCHED_RR 語義。 它使用 100 個執行佇列(對於所有 100 個 RT 優先順序級別,而不是先前排程器中的 140 個),並且不需要過期的陣列。

排程類透過 sched_class 結構實現,該結構包含每當發生有趣事件時必須呼叫的函式的鉤子。

這是(部分的)鉤子列表

  • enqueue_task(...)

    當任務進入可執行狀態時呼叫。 它將排程實體(任務)放入紅黑樹中,並增加 nr_running 變數。

  • dequeue_task(...)

    當任務不再可執行時,呼叫此函式以使相應的排程實體脫離紅黑樹。 它減少 nr_running 變數。

  • yield_task(...)

    除非啟用了 compat_yield sysctl,否則此函式基本上只是一個出隊後跟一個入隊; 在這種情況下,它將排程實體放置在紅黑樹的最右端。

  • wakeup_preempt(...)

    此函式檢查進入可執行狀態的任務是否應搶佔當前正在執行的任務。

  • pick_next_task(...)

    此函式選擇最適合接下來執行的任務。

  • set_next_task(...)

    當任務更改其排程類、更改其任務組或被排程時,將呼叫此函式。

  • task_tick(...)

    此函式主要從時間滴答函式呼叫; 它可能會導致程序切換。 這驅動正在執行的搶佔。

7. CFS 的組排程器擴充套件

通常,排程器對單個任務進行操作,並努力為每個任務提供公平的 CPU 時間。 有時,可能需要對任務進行分組併為每個這樣的任務組提供公平的 CPU 時間。 例如,可能需要首先為系統上的每個使用者提供公平的 CPU 時間,然後為屬於使用者的每個任務提供公平的 CPU 時間。

CONFIG_CGROUP_SCHED 努力實現這一目標。 它允許對任務進行分組,並在這些組之間公平地分配 CPU 時間。

CONFIG_RT_GROUP_SCHED 允許對即時(即 SCHED_FIFO 和 SCHED_RR)任務進行分組。

CONFIG_FAIR_GROUP_SCHED 允許對 CFS(即 SCHED_NORMAL 和 SCHED_BATCH)任務進行分組。

這些選項需要定義 CONFIG_CGROUPS,並允許管理員使用“cgroup”偽檔案系統建立任意的任務組。 有關此檔案系統的更多資訊,請參閱 控制組

定義 CONFIG_FAIR_GROUP_SCHED 後,將為使用偽檔案系統建立的每個組建立一個“cpu.shares”檔案。 請參閱下面的示例步驟,以建立任務組並使用“cgroups”偽檔案系統修改其 CPU 份額

# mount -t tmpfs cgroup_root /sys/fs/cgroup
# mkdir /sys/fs/cgroup/cpu
# mount -t cgroup -ocpu none /sys/fs/cgroup/cpu
# cd /sys/fs/cgroup/cpu

# mkdir multimedia      # create "multimedia" group of tasks
# mkdir browser         # create "browser" group of tasks

# #Configure the multimedia group to receive twice the CPU bandwidth
# #that of browser group

# echo 2048 > multimedia/cpu.shares
# echo 1024 > browser/cpu.shares

# firefox &     # Launch firefox and move it to "browser" group
# echo <firefox_pid> > browser/tasks

# #Launch gmplayer (or your favourite movie player)
# echo <movie_player_pid> > multimedia/tasks