Linux 組排程淺析

發表於2016-09-28

cgroup 與組排程

linux核心實現了control group功能(cgroup,since linux 2.6.24),可以支援將程式分組,然後按組來劃分各種資源。比如:group-1擁有30%的CPU和50%的磁碟IO、group-2擁有10%的CPU和20%的磁碟IO、等等。具體參閱cgroup相關文章。

cgroup支援很多種資源的劃分,CPU資源就是其中之一,這就引出了組排程。

linux核心中,傳統的排程程式是基於程式來排程的(參閱《linux程式排程淺析》)。假設使用者A和B共用一臺機器,這臺機器主要用來編譯程式。我們可能希望A和B能公平的分享CPU資源,但是如果使用者A使用make -j8(8個執行緒並行make)、而使用者B直接使用make的話(假設他們的make程式都使用了預設的優先順序),A使用者的make程式將產生8倍於B使用者的程式數,從而佔用(大致)8倍於B使用者的CPU。因為排程程式是基於程式的,A使用者的程式越多,被排程的機率就越大,就越具有對CPU的競爭力。

如何保證A、B使用者公平分享CPU呢?組排程就能做到這一點。把屬於使用者A和B的程式各分為一組,排程程式將先從兩個組中選擇一個組,再從選中的組中選擇一個程式來執行。如果兩個組被選中的機率相當,那麼使用者A和B將各佔有約50%的CPU。

相關資料結構

在linux核心中,使用task_group結構來管理組排程的組。所有存在的task_group組成一個樹型結構(與cgroup的目錄結構相對應)。

一個task_group可以包含具有任意排程類別的程式(具體來說是實時程式和普通程式兩種類別),於是task_group需要為每一種排程策略提供一組排程結構。這裡所說的一組排程結構主要包括兩個部分,排程實體和執行佇列(兩者都是每CPU一份的)。排程實體會被新增到執行佇列中,對於一個task_group,它的排程實體會被新增到其父task_group的執行佇列。

為什麼要有排程實體這樣的東西呢?因為被排程的物件有task_group和task兩種,所以需要一個抽象的結構來代表它們。如果排程實體代表task_group,則它的my_q欄位指向這個排程組對應的執行佇列;否則my_q欄位為NULL,排程實體代表task。在排程實體中與my_q相對的是X_rq(具體是針對普通程式的cfs_rq和針對實時程式的rt_rq),前者指向這個組自己的執行佇列,裡面會放入它的子節點;後者指向這個組的父節點的執行佇列,也就是這個排程實體應該被放入的執行佇列。

於是,排程實體和執行佇列又組成了另一個樹型結構,它的每一個非葉子節點都跟task_group的樹型結構是相對應的,而葉子節點都對應到具體的task。就像非TASK_RUNNING狀態的程式不會被放入執行佇列一樣,如果一個組中不存在TASK_RUNNING狀態的程式,則這個組(對應的排程實體)也不會被放入它的上一級執行佇列。明確一點,只要排程組建立了,其對應的task_group就肯定存在於由task_group組成的樹型結構中;而其對應的排程實體是否存在於由執行佇列和排程實體組成的樹型結構中,要取決於這個組中是否存在TASK_RUNNING狀態的程式。

作為根節點的task_group是沒有排程實體的,排程程式總是從它的執行佇列出發,來選擇下一個排程實體(根節點必定是第一個被選中的,沒有其他候選者,所以根節點不需要排程實體)。根節點task_group所對應的執行佇列被包裝在一個rq結構中,裡面除了包含具體的執行佇列以外,還有一些全域性統計資訊等欄位。

排程發生的時候,排程程式從根task_group的執行佇列中選擇一個排程實體。如果這個排程實體代表一個task_group,則排程程式需要從這個組對應的執行佇列繼續選擇一個排程實體。如此遞迴下去,直到選中一個程式。除非根task_group的執行佇列為空,否則遞迴下去一定能找到一個程式。因為如果一個task_group對應的執行佇列為空,它對應的排程實體就不會被新增到其父節點對應的執行佇列中。

最後,對於一個task_group來說,它的排程實體和執行佇列都是每CPU一份的,一個(task_group對應的)排程實體只會被加入到相同CPU所對應的執行佇列。而對於task來說,它的排程實體則只有一份(沒有按CPU劃分),排程程式的負載均衡功能可能會將(task對應的)排程實體從不同CPU所對應的執行佇列移來移去。(參見《linux核心SMP負載均衡淺析》)

組的排程策略

組排程的主要資料結構已經理清了,這裡還有一個很重要的問題。我們知道task擁有其對應的優先順序(靜態優先順序 or 動態優先順序),排程程式根據優先順序來選擇執行佇列中的程式。那麼,既然task_group和task一樣,都被抽象成排程實體,接受同樣的排程,task_group的優先順序又該如何定義呢?這個問題需要具體到排程類別來解答(不同的排程類別,其優先順序定義方式不一樣),具體來說就是rt(實時排程)和cfs(完全公平排程)兩種類別。

實時程式的組排程

從《linux程式排程淺析》一文可以看到,實時程式是對CPU有著實時性要求的程式,它的優先順序是跟具體任務相關的,完全由使用者來定義的。排程器總是會選擇優先順序最高的實時程式來執行。

發展到組排程,組的優先順序就被定義為“組內最高優先順序的程式所擁有的優先順序”。比如組內有三個優先順序分別為10、20、30的程式,則組的優先順序就是10(數值越小優先順序越大)。

組的優先順序如此定義,引出了一個有趣的現象。當task入隊或者出隊時,要把它的所有祖先節點都先出隊,然後再重新由底向上依次入隊。因為組節點的優先順序是依賴於它的子節點的,task的入隊和出隊將影響它的每一個祖先節點。

於是,當排程程式從根節點的task_group出發選擇排程實體時,總是能沿著正確的路徑,找到所有TASK_RUNNING狀態的實時程式中優先順序最高的那一個。這個實現似乎理所當然,但是仔細想想,這樣一來,將實時程式分組還有什麼意義呢?無論分組與否,排程程式要做的事情都是“在所有TASK_RUNNING狀態的實時程式中選擇優先順序最高的那一個”。這裡似乎還缺了些什麼……

現在需要先介紹一下linux系統中的兩個proc檔案:/proc/sys/kernel/sched_rt_period_us和/proc/sys/kernel/sched_rt_runtime_us。這兩個檔案規定了,在以sched_rt_period_us為一個週期的時間內,所有實時程式的執行時間之和不超過sched_rt_runtime_us。這兩個檔案的預設值是1s和0.95s,表示每秒種為一個週期,在這個週期中,所有實時程式執行的總時間不超過0.95秒,剩下的至少0.05秒會留給普通程式。也就是說,實時程式佔有不超過95%的CPU。而在這兩個檔案出現之前,實時程式的執行時間是沒有限制的(就像《linux程式排程淺析》裡面描述的那樣),如果一直有處於TASK_RUNNING狀態的實時程式,則普通程式會一直不能得到執行。相當於sched_rt_runtime_us等於sched_rt_period_us。

為什麼要有sched_rt_runtime_us和sched_rt_period_us兩個變數呢?直接使用一個表示CPU佔有百分比的變數不可以麼?我想這應該是由於很多實時程式實際上都是週期性地在幹某件事情,比如某語音程式每20ms傳送一個語音包、某視訊程式每40ms重新整理一幀、等等。週期是很重要的,僅僅使用一個巨集觀的CPU佔有比無法準確描述實時程式需求。

而實時程式的分組就把sched_rt_runtime_us和sched_rt_period_us的概念擴充套件了,每個task_group都有自己的sched_rt_runtime_us和sched_rt_period_us,保證自己組內的程式在以sched_rt_period_us為週期的時間內,最多隻能執行sched_rt_runtime_us這麼多時間。CPU佔有比為sched_rt_runtime_us/sched_rt_period_us。

對於根節點的task_group,它的sched_rt_runtime_us和sched_rt_period_us就等於上面兩個proc檔案中的值。而對於一個task_group節點來說,假設它下面有n個排程子組和m個TASK_RUNNING狀態的程式,它的CPU佔有比為A、這n個子組的CPU佔有比為B,則B必須小於等於A,而A-B剩下的CPU時間將分給那m個TASK_RUNNING狀態的程式。(這裡討論的是CPU佔有比,因為每個排程組可能有著不同的週期值。)

為了實現sched_rt_runtime_us和sched_rt_period_us的邏輯,核心在更新程式的執行時間的時候(比如由週期性的時鐘中斷觸發的時間更新)會給當前程式的排程實體及其所有祖先節點都增加相應的runtime。如果一個排程實體達到了sched_rt_runtime_us所限定的時間,則將其從對應的執行佇列中剔除,並將對應的rt_rq置throttled狀態。在這個狀態下,這個rt_rq對應的排程實體不會再次進入執行佇列。而每個rt_rq都會維護一個週期性的定時器,定時週期為sched_rt_period_us。每次定時器觸發,其對應的回撥函式就會將rt_rq的runtime減去一個sched_rt_period_us單位的值(但要保持runtime不小於0),然後將rt_rq從throttled狀態中恢復回來。

還有一個問題,前面說到,預設情況下,系統中每秒鐘內實時程式的執行時間不超過0.95秒。如果實時程式實際對CPU的需求不足0.95秒(大於等於0秒、小於0.95秒),則剩下的時間都會分配給普通程式。而如果實時程式的對CPU的需求大於0.95秒,它也只能夠執行0.95秒,剩下的0.05秒會分給其他普通程式。但是,如果這0.05秒內沒有任何普通程式需要使用CPU(一直沒有TASK_RUNNING狀態的普通程式)呢?這種情況下既然普通程式對CPU沒有需求,實時程式是否可以執行超過0.95秒呢?不能。在剩下的0.05秒中核心寧可讓CPU一直閒著,也不讓實時程式使用。可見sched_rt_runtime_us和sched_rt_period_us是很有強制性的。

最後還有多CPU的問題,前面也提到,對於每一個task_group,它的排程實體和執行佇列是每CPU維護一份的。而sched_rt_runtime_us和sched_rt_period_us是作用在排程實體上的.所以如果系統中有N個CPU,實時程式實際佔有CPU的上限N*sched_rt_runtime_us/sched_rt_period_us。也就是說,儘管預設情況下限制了每秒鐘之內,實時程式只能執行0.95秒。但是對於某個實時程式來說,如果CPU有兩個核,也還是能滿足它100%佔有CPU的需求的(比如執行死迴圈)。然後,按道理說,這個實時程式佔有的100%的CPU應該是由兩部分組成的(每個CPU佔有一部分,但都不超過95%)。但是實際上,為了避免程式在CPU間的遷移導致上下文切換、快取失效等一系列問題,一個CPU上的排程實體可以向另一個CPU上對應的排程實體借用時間。其結果就是,巨集觀上既滿足了sched_rt_runtime_us的限制,又避免了程式的遷移。

普通程式的組排程

文章一開頭提到了希望A、B兩個使用者在程式數不相同的情況下也能平分CPU的需求,但是上面關於實時程式的組排程策略好像與此不太相干,其實這就是普通程式的組排程所要乾的事。

相比實時程式,普通程式的組排程就沒有這麼多講究。組被看作是跟程式幾乎完全相同的實體,它擁有自己的靜態優先順序、排程程式也動態地調整它的優先順序。對於一個組來說,組內程式的優先順序並不影響組的優先順序,只有這個組被排程程式選中時,這些程式的優先順序才被考慮。

為了設定組的優先順序,每個task_group都有一個shares引數(跟前面提到的sched_rt_runtime_us和sched_rt_period_us兩個引數並列)。shares並不是優先順序,而是排程實體的權重(這是CFS排程器的玩法),這個權重和優先順序是有一一對應的關係的。普通程式的優先順序也會被轉換成其對應排程實體的權重,所以可以說shares就代表了優先順序。

shares的預設值跟普通程式預設優先順序對應的權重是一樣的。所以在預設情況下,組和程式是平分CPU的。

示例

(環境:ubuntu 10.04,kernel 2.6.32,Intel Core2 雙核)

掛載一個只劃分CPU資源的cgroup,並建立grp_a和grp_b兩個子組:

分別開三個shell,第一個加入grp_a,後兩個加入grp_b:

(為什麼要用ttt.sh來寫cgroup下的tasks檔案呢?因為寫這個檔案需要root許可權,當前shell沒有root許可權,而sudo只能賦予被它執行的程式的root許可權。其實sudo sh,然後再在新開的shell裡面執行echo操作也是可以的。)

回到cgroup目錄下,確認這幾個shell都被加進去了:

現在準備在這三個shell下同時執行一個死迴圈的程式(a.out),為了避免多CPU帶來的影響,將程式繫結到第二個核上:

編譯生成a.out,然後在前面的三個shell中分別執行。三個shell分別會fork出一個子程式來執行a.out,這些子程式都會繼承其父程式的cgroup分組資訊。然後top一下,可以觀察到屬於grp_a的a.out佔了50%的CPU,而屬於grp_b的兩個a.out各佔25%的CPU(加起來也是50%):

接下來再試試實時程式,把a.out程式改造如下:

然後設定grp_a的rt_runtime值:

現在的配置是每秒為一個週期,屬於grp_a的實時程式每秒種只能執行300毫秒。執行a.out(設定實時程式需要root許可權),然後top看看:

可以看到,CPU雖然閒著,但是卻不分給a.out程式使用。由於雙核的原因,a.out實際的CPU佔用是60%而不是30%。

其他

前段時間,有一篇“200+行Kernel補丁顯著改善Linux桌面效能”的新聞比較火。這個核心補丁能讓高負載條件下的桌面程式響應延遲得到大幅度降低。其實現原理是,自動建立基於TTY的task_group,所有程式都會被放置在它所關聯的TTY組中。通過這樣的自動分組,就將桌面程式(Xwindow會佔用一個TTY)和其他終端或偽終端(各自佔用一個TTY)劃分開了。終端上執行的高負載程式(比如make -j64)對桌面程式的影響將大大減少。(根據前面描述的普通程式的組排程的實現可以知道,如果一個任務給系統帶來了很高的負載,只會影響到與它同組的程式。這個任務包含一個或是一萬個TASK_RUNNING狀態的程式,對於其他組的程式來說是沒有影響的。)

相關文章