Linux程序排程器的設計--Linux程序的管理與排程(十七)
1 前景回顧
1.1 程序排程
記憶體中儲存了對每個程序的唯一描述, 並通過若干結構與其他程序連線起來.
排程器面對的情形就是這樣, 其任務是在程式之間共享CPU時間, 創造並行執行的錯覺, 該任務分為兩個不同的部分, 其中一個涉及排程策略, 另外一個涉及上下文切換.
核心必須提供一種方法, 在各個程序之間儘可能公平地共享CPU時間, 而同時又要考慮不同的任務優先順序.
排程器的一個重要目標是有效地分配 CPU 時間片,同時提供很好的使用者體驗。排程器還需要面對一些互相沖突的目標,例如既要為關鍵實時任務最小化響應時間, 又要最大限度地提高 CPU 的總體利用率.
排程器的一般原理是, 按所需分配的計算能力, 向系統中每個程序提供最大的公正性, 或者從另外一個角度上說, 他試圖確保沒有程序被虧待.
1.2 程序的分類
linux把程序區分為實時程序和非實時程序, 其中非實時程序進一步劃分為互動式程序和批處理程序
型別 | 描述 | 示例 |
---|---|---|
互動式程序(interactive process) | 此類程序經常與使用者進行互動, 因此需要花費很多時間等待鍵盤和滑鼠操作. 當接受了使用者的輸入後, 程序必須很快被喚醒, 否則使用者會感覺系統反應遲鈍 | shell, 文字編輯程式和圖形應用程式 |
批處理程序(batch process) | 此類程序不必與使用者互動, 因此經常在後臺執行. 因為這樣的程序不必很快相應, 因此常受到排程程式的怠慢 | 程式語言的編譯程式, 資料庫搜尋引擎以及科學計算 |
實時程序(real-time process) | 這些程序由很強的排程需要, 這樣的程序絕不會被低優先順序的程序阻塞. 並且他們的響應時間要儘可能的短 | 視訊音訊應用程式, 機器人控制程式以及從物理感測器上收集資料的程式 |
在linux中, 排程演算法可以明確的確認所有實時程序的身份, 但是沒辦法區分互動式程式和批處理程式, linux2.6的排程程式實現了基於程序過去行為的啟發式演算法, 以確定程序應該被當做互動式程序還是批處理程序. 當然與批處理程序相比, 排程程式有偏愛互動式程序的傾向
1.3 不同程序採用不同的排程策略
根據程序的不同分類Linux採用不同的排程策略.
對於實時程序,採用FIFO或者Round Robin的排程策略.
對於普通程序,則需要區分互動式和批處理式的不同。傳統Linux排程器提高互動式應用的優先順序,使得它們能更快地被排程。而CFS和RSDL等新的排程器的核心思想是”完全公平”。這個設計理念不僅大大簡化了排程器的程式碼複雜度,還對各種排程需求的提供了更完美的支援.
注意Linux通過將程序和執行緒排程視為一個,同時包含二者。程序可以看做是單個執行緒,但是程序可以包含共享一定資源(程式碼和/或資料)的多個執行緒。因此程序排程也包含了執行緒排程的功能.
目前非實時程序的排程策略比較簡單, 因為實時程序值只要求儘可能快的被響應, 基於優先順序, 每個程序根據它重要程度的不同被賦予不同的優先順序,排程器在每次排程時, 總選擇優先順序最高的程序開始執行. 低優先順序不可能搶佔高優先順序, 因此FIFO或者Round Robin的排程策略即可滿足實時程序排程的需求.
但是普通程序的排程策略就比較麻煩了, 因為普通程序不能簡單的只看優先順序, 必須公平的佔有CPU, 否則很容易出現程序飢餓, 這種情況下使用者會感覺作業系統很卡, 響應總是很慢,因此在linux排程器的發展歷程中經過了多次重大變動, linux總是希望尋找一個最接近於完美的排程策略來公平快速的排程程序.
1.4 linux排程器的演變
一開始的排程器是複雜度為
然而,linux是集全球很多程式設計師的聰明才智而發展起來的超級核心,沒有最好,只有更好,在
欄位 | 版本 |
---|---|
O(n)的始排程演算法 | linux-0.11~2.4 |
O(1)排程器 | linux-2.5 |
CFS排程器 | linux-2.6~至今 |
2 Linux的排程器組成
2.1 2個排程器
可以用兩種方法來啟用排程
一種是直接的, 比如程序打算睡眠或出於其他原因放棄CPU
另一種是通過週期性的機制, 以固定的頻率執行, 不時的檢測是否有必要
因此當前linux的排程程式由兩個排程器組成:主排程器,週期性排程器(兩者又統稱為通用排程器(generic scheduler)或核心排程器(core scheduler))
並且每個排程器包括兩個內容:排程框架(其實質就是兩個函式框架)及排程器類
2.2 6種排程策略
linux核心目前實現了6中排程策略(即排程演算法), 用於對不同型別的程序進行排程, 或者支援某些特殊的功能
比如SCHED_NORMAL和SCHED_BATCH排程普通的非實時程序, SCHED_FIFO和SCHED_RR和SCHED_DEADLINE則採用不同的排程策略排程實時程序, SCHED_IDLE則在系統空閒時呼叫idle程序.
idle的執行時機
idle 程序優先順序為MAX_PRIO,即最低優先順序。
早先版本中,idle是參與排程的,所以將其優先順序設為最低,當沒有其他程序可以執行時,才會排程執行 idle
而目前的版本中idle並不在執行佇列中參與排程,而是在cpu全域性執行佇列rq中含idle指標,指向idle程序, 在排程器發現執行佇列為空的時候執行, 調入執行
欄位 | 描述 | 所在排程器類 |
---|---|---|
SCHED_NORMAL | (也叫SCHED_OTHER)用於普通程序,通過CFS排程器實現。SCHED_BATCH用於非互動的處理器消耗型程序。SCHED_IDLE是在系統負載很低時使用 | CFS |
SCHED_BATCH | SCHED_NORMAL普通程序策略的分化版本。採用分時策略,根據動態優先順序(可用nice()API設定),分配CPU運算資源。注意:這類程序比上述兩類實時程序優先順序低,換言之,在有實時程序存在時,實時程序優先排程。但針對吞吐量優化, 除了不能搶佔外與常規任務一樣,允許任務執行更長時間,更好地使用快取記憶體,適合於成批處理的工作 | CFS |
SCHED_IDLE | 優先順序最低,在系統空閒時才跑這類程序(如利用閒散計算機資源跑地外文明搜尋,蛋白質結構分析等任務,是此排程策略的適用者) | CFS-IDLE |
SCHED_FIFO | 先入先出排程演算法(實時排程策略),相同優先順序的任務先到先服務,高優先順序的任務可以搶佔低優先順序的任務 | RT |
SCHED_RR | 輪流排程演算法(實時排程策略),後者提供 Roound-Robin 語義,採用時間片,相同優先順序的任務當用完時間片會被放到佇列尾部,以保證公平性,同樣,高優先順序的任務可以搶佔低優先順序的任務。不同要求的實時任務可以根據需要用sched_setscheduler() API設定策略 | RT |
SCHED_DEADLINE | 新支援的實時程序排程策略,針對突發型計算,且對延遲和完成時間高度敏感的任務適用。基於Earliest Deadline First (EDF) 排程演算法 | DL |
linux核心實現的6種排程策略, 前面三種策略使用的是cfs排程器類,後面兩種使用rt排程器類, 最後一個使用DL排程器類
2.3 5個排程器類
而依據其排程策略的不同實現了5個排程器類, 一個排程器類可以用一種種或者多種排程策略排程某一類程序, 也可以用於特殊情況或者排程特殊功能的程序.
排程器類 | 描述 | 對應排程策略 |
---|---|---|
stop_sched_class | 優先順序最高的執行緒,會中斷所有其他執行緒,且不會被其他任務打斷 作用 1.發生在cpu_stop_cpu_callback 進行cpu之間任務migration 2.HOTPLUG_CPU的情況下關閉任務 |
無, 不需要排程普通程序 |
dl_sched_class | 採用EDF最早截至時間優先演算法排程實時程序 | SCHED_DEADLINE |
rt_sched_class | 採用提供 Roound-Robin演算法或者FIFO演算法排程實時程序 具體排程策略由程序的task_struct->policy指定 |
SCHED_FIFO, SCHED_RR |
fair_sched_clas | 採用CFS演算法排程普通的非實時程序 | SCHED_NORMAL, SCHED_BATCH |
idle_sched_class | 採用CFS演算法排程idle程序, 每個cup的第一個pid=0執行緒:swapper,是一個靜態執行緒。排程類屬於:idel_sched_class,所以在ps裡面是看不到的。一般執行在開機過程和cpu異常的時候做dump | SCHED_IDLE |
其所屬程序的優先順序順序為
stop_sched_class -> dl_sched_class -> rt_sched_class -> fair_sched_class -> idle_sched_class
2.4 3個排程實體
排程器不限於排程程序, 還可以排程更大的實體, 比如實現組排程: 可用的CPUI時間首先在一半的程序組(比如, 所有程序按照所有者分組)之間分配, 接下來分配的時間再在組內進行二次分配.
這種一般性要求排程器不直接操作程序, 而是處理可排程實體, 因此需要一個通用的資料結構描述這個排程實體,即seched_entity結構, 其實際上就代表了一個排程物件,可以為一個程序,也可以為一個程序組.
linux中針對當前可排程的實時和非實時程序, 定義了型別為seched_entity的3個排程實體
排程實體 | 名稱 | 描述 | 對應排程器類 |
---|---|---|---|
sched_dl_entity | DEADLINE排程實體 | 採用EDF演算法排程的實時排程實體 | dl_sched_class |
sched_rt_entity | RT排程實體 | 採用Roound-Robin或者FIFO演算法排程的實時排程實體 | rt_sched_class |
sched_entity | CFS排程實體 | 採用CFS演算法排程的普通非實時程序的排程實體 | fair_sched_class |
2.5 排程器類的就緒佇列
另外,對於排程框架及排程器類,它們都有自己管理的執行佇列,排程框架只識別rq(其實它也不能算是執行佇列),而對於cfs排程器類它的執行佇列則是cfs_rq(內部使用紅黑樹組織排程實體),實時rt的執行佇列則為rt_rq(內部使用優先順序bitmap+雙向連結串列組織排程實體), 此外核心對新增的dl實時排程策略也提供了執行佇列dl_rq
2.6 排程器整體框架
本質上, 通用排程器(核心排程器)是一個分配器,與其他兩個元件互動.
排程器用於判斷接下來執行哪個程序.
核心支援不同的排程策略(完全公平排程, 實時排程, 在無事可做的時候排程空閒程序,即0號程序也叫swapper程序,idle程序), 排程類使得能夠以模組化的方法實現這些側露額, 即一個類的程式碼不需要與其他類的程式碼互動
當排程器被呼叫時, 他會查詢排程器類, 得知接下來執行哪個程序在選中將要執行的程序之後, 必須執行底層的任務切換.
這需要與CPU的緊密互動. 每個程序剛好屬於某一排程類, 各個排程類負責管理所屬的程序. 通用排程器自身不涉及程序管理, 其工作都委託給排程器類.
每個程序都屬於某個排程器類(由欄位task_struct->sched_class標識), 由排程器類採用程序對應的排程策略排程(由task_struct->policy )進行排程, task_struct也儲存了其對應的排程實體標識
linux實現了6種排程策略, 依據其排程策略的不同實現了5個排程器類, 一個排程器類可以用一種或者多種排程策略排程某一類程序, 也可以用於特殊情況或者排程特殊功能的程序.
排程器類 | 排程策略 | 排程策略對應的排程演算法 | 排程實體 | 排程實體對應的排程物件 |
---|---|---|---|---|
stop_sched_class | 無 | 無 | 無 | 特殊情況, 發生在cpu_stop_cpu_callback 進行cpu之間任務遷移migration或者HOTPLUG_CPU的情況下關閉任務 |
dl_sched_class | SCHED_DEADLINE | Earliest-Deadline-First最早截至時間有限演算法 | sched_dl_entity | 採用DEF最早截至時間有限演算法排程實時程序 |
rt_sched_class | SCHED_RR SCHED_FIFO |
Roound-Robin時間片輪轉演算法 FIFO先進先出演算法 |
sched_rt_entity | 採用Roound-Robin或者FIFO演算法排程的實時排程實體 |
fair_sched_class | SCHED_NORMAL SCHED_BATCH |
CFS完全公平懂排程演算法 | sched_entity | 採用CFS演算法普通非實時程序 |
idle_sched_class | SCHED_IDLE | 無 | 無 | 特殊程序, 用於cpu空閒時排程空閒程序idle |
它們的關係如下圖
2.7 5種排程器類為什麼只有3種排程實體?
正常來說一個排程器類應該對應一類排程實體, 但是5種排程器類卻只有了3種排程實體?
這是因為排程實體本質是一個可以被排程的物件, 要麼是一個程序(linux中執行緒本質上也是程序), 要麼是一個程序組, 只有dl_sched_class, rt_sched_class排程的實時程序(組)以及fair_sched_class排程的非實時程序(組)是可以被排程的實體物件, 而stop_sched_class和idle_sched_class
2.8 為什麼採用EDF實時排程需要單獨的排程器類, 排程策略和排程實體
linux針對實時程序實現了Roound-Robin, FIFO和Earliest-Deadline-First(EDF)演算法, 但是為什麼SCHED_RR和SCHED_FIFO兩種排程演算法都用rt_sched_class排程類和sched_rt_entity排程實體描述, 而EDF演算法卻需要單獨用rt_sched_class排程類和sched_dl_entity排程實體描述
為什麼採用EDF實時排程不用rt_sched_class排程類排程, 而是單獨實現排程類和排程實體?
暫時沒弄明白
3 程序排程的資料結構
排程器使用一系列資料結構來排序和管理系統中的程序. 排程器的工作方式的這些結構的涉及密切相關, 幾個元件在許多方面
struct task_struct
{
........
/* 表示是否在執行佇列 */
int on_rq;
/* 程序優先順序
* prio: 動態優先順序,範圍為100~139,與靜態優先順序和補償(bonus)有關
* static_prio: 靜態優先順序,static_prio = 100 + nice + 20 (nice值為-20~19,所以static_prio值為100~139)
* normal_prio: 沒有受優先順序繼承影響的常規優先順序,具體見normal_prio函式,跟屬於什麼型別的程序有關
*/
int prio, static_prio, normal_prio;
/* 實時程序優先順序 */
unsigned int rt_priority;
/* 排程類,排程處理函式類 */
const struct sched_class *sched_class;
/* 排程實體(紅黑樹的一個結點) */
struct sched_entity se;
/* 排程實體(實時排程使用) */
struct sched_rt_entity rt;
struct sched_dl_entity dl;
#ifdef CONFIG_CGROUP_SCHED
/* 指向其所在程序組 */
struct task_group *sched_task_group;
#endif
........
}
3.1.1 優先順序
int prio, static_prio, normal_prio;
unsigned int rt_priority;
動態優先順序 靜態優先順序 實時優先順序
其中task_struct採用了三個成員表示程序的優先順序:prio和normal_prio表示動態優先順序, static_prio表示程序的靜態優先順序.
為什麼表示動態優先順序需要兩個值prio和normal_prio
排程器會考慮的優先順序則儲存在prio. 由於在某些情況下核心需要暫時提高程序的優先順序, 因此需要用prio表示. 由於這些改變不是持久的, 因此靜態優先順序static_prio和普通優先順序normal_prio不受影響.
此外還用了一個欄位rt_priority儲存了實時程序的優先順序
欄位 | 描述 |
---|---|
static_prio | 用於儲存靜態優先順序, 是程序啟動時分配的優先順序, ,可以通過nice和sched_setscheduler系統呼叫來進行修改, 否則在程序執行期間會一直保持恆定 |
prio | 儲存程序的動態優先順序 |
normal_prio | 表示基於程序的靜態優先順序static_prio和排程策略計算出的優先順序. 因此即使普通程序和實時程序具有相同的靜態優先順序, 其普通優先順序也是不同的, 程序分叉(fork)時, 子程序會繼承父程序的普通優先順序 |
rt_priority | 用於儲存實時優先順序 |
實時程序的優先順序用實時優先順序rt_priority來表示
linux2.6核心將任務優先順序進行了一個劃分, 實時優先順序範圍是0到MAX_RT_PRIO-1(即99),而普通程序的靜態優先順序範圍是從MAX_RT_PRIO到MAX_PRIO-1(即100到139)。
/* http://lxr.free-electrons.com/source/include/linux/sched/prio.h?v=4.6#L21 */
#define MAX_USER_RT_PRIO 100
#define MAX_RT_PRIO MAX_USER_RT_PRIO
/* http://lxr.free-electrons.com/source/include/linux/sched/prio.h?v=4.6#L24 */
#define MAX_PRIO (MAX_RT_PRIO + 40)
#define DEFAULT_PRIO (MAX_RT_PRIO + 20)
優先順序範圍 | 描述 |
---|---|
0——99 | 實時程序 |
100——139 | 非實時程序 |
3.1.2 排程策略
unsigned int policy;
policy儲存了程序的排程策略,目前主要有以下五種:
/*
* Scheduling policies
*/
#define SCHED_NORMAL 0
#define SCHED_FIFO 1
#define SCHED_RR 2
#define SCHED_BATCH 3
/* SCHED_ISO: reserved but not implemented yet */
#define SCHED_IDLE 5
#define SCHED_DEADLINE 6
欄位 | 描述 | 所在排程器類 |
---|---|---|
SCHED_NORMAL | (也叫SCHED_OTHER)用於普通程序,通過CFS排程器實現。 | |
SCHED_BATCH | SCHED_NORMAL普通程序策略的分化版本。採用分時策略,根據動態優先順序(可用nice()API設定),分配 CPU 運算資源。注意:這類程序比兩類實時程序優先順序低,換言之,在有實時程序存在時,實時程序優先排程。但針對吞吐量優化 | CFS |
SCHED_IDLE | 優先順序最低,在系統空閒時才跑這類程序(如利用閒散計算機資源跑地外文明搜尋,蛋白質結構分析等任務,是此排程策略的適用者) | CFS |
SCHED_FIFO | 先入先出排程演算法(實時排程策略),相同優先順序的任務先到先服務,高優先順序的任務可以搶佔低優先順序的任務 | RT |
SCHED_RR | 輪流排程演算法(實時排程策略),後 者提供 Roound-Robin 語義,採用時間片,相同優先順序的任務當用完時間片會被放到佇列尾部,以保證公平性,同樣,高優先順序的任務可以搶佔低優先順序的任務。不同要求的實時任務可以根據需要用sched_setscheduler()API 設定策略 | RT |
SCHED_DEADLINE | 新支援的實時程序排程策略,針對突發型計算,且對延遲和完成時間高度敏感的任務適用。基於Earliest Deadline First (EDF) 排程演算法 |
CHED_BATCH用於非互動的處理器消耗型程序
CHED_IDLE是在系統負載很低時使用CFS
SCHED_BATCH用於非互動, CPU使用密集型的批處理程序. 排程決策對此類程序給予”冷處理”: 他們絕不會搶佔CF排程器處理的另一個程序, 因此不會干擾互動式程序. 如果打算使用nice值降低程序的靜態優先順序, 同時又不希望該程序影響系統的互動性, 此時最適合使用該排程類.
而SCHED_LDLE程序的重要性則會進一步降低, 因此其權重總是最小的
注意
儘管名稱是SCHED_IDLE但是SCHED_IDLE不負責排程空閒程序. 空閒程序由核心提供單獨的機制來處理
SCHED_RR和SCHED_FIFO用於實現軟實時程序. SCHED_RR實現了輪流排程演算法, 一種迴圈時間片的方法, 而SCHED_FIFO實現了先進先出的機制, 這些並不是由完全貢品排程器類CFS處理的, 而是由實時排程類處理.
3.1.3 排程策略相關欄位
/* http://lxr.free-electrons.com/source/include/linux/sched.h?v=4.6#L1431 */
unsigned int policy;
/* http://lxr.free-electrons.com/source/include/linux/sched.h?v=4.6#L1413 */
const struct sched_class *sched_class;
struct sched_entity se;
struct sched_rt_entity rt;
struct sched_dl_entity dl;
cpumask_t cpus_allowed;
欄位 | 描述 |
---|---|
sched_class | 排程類, 排程類,排程處理函式類 |
se | 普通程序的呼叫實體, 每個程序都有其中之一的實體 |
rt | 實時程序的呼叫實體, 每個程序都有其中之一的實體 |
dl | deadline的排程實體 |
cpus_allowed | 用於控制程序可以在哪裡處理器上執行 |
排程器不限於排程程序, 還可以排程更大的實體, 比如實現組排程: 可用的CPUI時間首先在一半的程序組(比如, 所有程序按照所有者分組)之間分配, 接下來分配的時間再在組內進行二次分配
cpus_allows是一個位域, 在多處理器系統上使用, 用來限制程序可以在哪些CPU上執行
3.2 排程類
sched_class結構體表示排程類, 類提供了通用排程器和各個排程器之間的關聯, 排程器類和特定資料結構中彙集地幾個函式指標表示, 全域性排程器請求的各個操作都可以用一個指標表示, 這使得無需瞭解排程器類的內部工作原理即可建立通用排程器, 定義在kernel/sched/sched.h
struct sched_class {
/* 系統中多個排程類, 按照其排程的優先順序排成一個連結串列
下一優先順序的排程類
* 排程類優先順序順序: stop_sched_class -> dl_sched_class -> rt_sched_class -> fair_sched_class -> idle_sched_class
*/
const struct sched_class *next;
/* 將程序加入到執行佇列中,即將排程實體(程序)放入紅黑樹中,並對 nr_running 變數加1 */
void (*enqueue_task) (struct rq *rq, struct task_struct *p, int flags);
/* 從執行佇列中刪除程序,並對 nr_running 變數中減1 */
void (*dequeue_task) (struct rq *rq, struct task_struct *p, int flags);
/* 放棄CPU,在 compat_yield sysctl 關閉的情況下,該函式實際上執行先出隊後入隊;在這種情況下,它將排程實體放在紅黑樹的最右端 */
void (*yield_task) (struct rq *rq);
bool (*yield_to_task) (struct rq *rq, struct task_struct *p, bool preempt);
/* 檢查當前程序是否可被新程序搶佔 */
void (*check_preempt_curr) (struct rq *rq, struct task_struct *p, int flags);
/*
* It is the responsibility of the pick_next_task() method that will
* return the next task to call put_prev_task() on the @prev task or
* something equivalent.
*
* May return RETRY_TASK when it finds a higher prio class has runnable
* tasks.
*/
/* 選擇下一個應該要執行的程序執行 */
struct task_struct * (*pick_next_task) (struct rq *rq,
struct task_struct *prev);
/* 將程序放回執行佇列 */
void (*put_prev_task) (struct rq *rq, struct task_struct *p);
#ifdef CONFIG_SMP
/* 為程序選擇一個合適的CPU */
int (*select_task_rq)(struct task_struct *p, int task_cpu, int sd_flag, int flags);
/* 遷移任務到另一個CPU */
void (*migrate_task_rq)(struct task_struct *p);
/* 用於程序喚醒 */
void (*task_waking) (struct task_struct *task);
void (*task_woken) (struct rq *this_rq, struct task_struct *task);
/* 修改程序的CPU親和力(affinity) */
void (*set_cpus_allowed)(struct task_struct *p,
const struct cpumask *newmask);
/* 啟動執行佇列 */
void (*rq_online)(struct rq *rq);
/* 禁止執行佇列 */
void (*rq_offline)(struct rq *rq);
#endif
/* 當程序改變它的排程類或程序組時被呼叫 */
void (*set_curr_task) (struct rq *rq);
/* 該函式通常呼叫自 time tick 函式;它可能引起程序切換。這將驅動執行時(running)搶佔 */
void (*task_tick) (struct rq *rq, struct task_struct *p, int queued);
/* 在程序建立時呼叫,不同調度策略的程序初始化不一樣 */
void (*task_fork) (struct task_struct *p);
/* 在程序退出時會使用 */
void (*task_dead) (struct task_struct *p);
/*
* The switched_from() call is allowed to drop rq->lock, therefore we
* cannot assume the switched_from/switched_to pair is serliazed by
* rq->lock. They are however serialized by p->pi_lock.
*/
/* 用於程序切換 */
void (*switched_from) (struct rq *this_rq, struct task_struct *task);
void (*switched_to) (struct rq *this_rq, struct task_struct *task);
/* 改變優先順序 */
void (*prio_changed) (struct rq *this_rq, struct task_struct *task,
int oldprio);
unsigned int (*get_rr_interval) (struct rq *rq,
struct task_struct *task);
void (*update_curr) (struct rq *rq);
#ifdef CONFIG_FAIR_GROUP_SCHED
void (*task_move_group) (struct task_struct *p);
#endif
};
成員 | 描述 |
---|---|
enqueue_task | 向就緒佇列中新增一個程序, 某個任務進入可執行狀態時,該函式將得到呼叫。它將排程實體(程序)放入紅黑樹中,並對 nr_running 變數加 1 |
dequeue_task | 將一個程序從就就緒佇列中刪除, 當某個任務退出可執行狀態時呼叫該函式,它將從紅黑樹中去掉對應的排程實體,並從 nr_running 變數中減 1 |
yield_task | 在程序想要資源放棄對處理器的控制權的時, 可使用在sched_yield系統呼叫, 會呼叫核心API yield_task完成此工作. compat_yield sysctl 關閉的情況下,該函式實際上執行先出隊後入隊;在這種情況下,它將排程實體放在紅黑樹的最右端 |
check_preempt_curr | 該函式將檢查當前執行的任務是否被搶佔。在實際搶佔正在執行的任務之前,CFS 排程程式模組將執行公平性測試。這將驅動喚醒式(wakeup)搶佔 |
pick_next_task | 該函式選擇接下來要執行的最合適的程序 |
put_prev_task | 用另一個程序代替當前執行的程序 |
set_curr_task | 當任務修改其排程類或修改其任務組時,將呼叫這個函式 |
task_tick | 在每次啟用週期排程器時, 由週期性排程器呼叫, 該函式通常呼叫自 time tick 函式;它可能引起程序切換。這將驅動執行時(running)搶佔 |
task_new | 核心排程程式為排程模組提供了管理新任務啟動的機會, 用於建立fork系統呼叫和排程器之間的關聯, 每次新程序建立後, 則用new_task通知排程器, CFS 排程模組使用它進行組排程,而用於實時任務的排程模組則不會使用這個函式 |
對於各個排程器類, 都必須提供struct sched_class的一個例項, 目前核心中有實現以下五種:
// http://lxr.free-electrons.com/source/kernel/sched/sched.h?v=4.6#L1254
extern const struct sched_class stop_sched_class;
extern const struct sched_class dl_sched_class;
extern const struct sched_class rt_sched_class;
extern const struct sched_class fair_sched_class;
extern const struct sched_class idle_sched_class;
排程器類 | 定義 | 描述 |
---|---|---|
stop_sched_class | 優先順序最高的執行緒,會中斷所有其他執行緒,且不會被其他任務打斷。作用: 1.發生在cpu_stop_cpu_callback 進行cpu之間任務migration; 2.HOTPLUG_CPU的情況下關閉任務。 |
|
idle_sched_class | 每個cup的第一個pid=0執行緒:swapper,是一個靜態執行緒。排程類屬於:idel_sched_class,所以在ps裡面是看不到的。一般執行在開機過程和cpu異常的時候做dump |
目前系統中,Scheduling Class的優先順序順序為
stop_sched_class -> dl_sched_class -> rt_sched_class -> fair_sched_class -> idle_sched_class
開發者可以根據己的設計需求,來把所屬的Task配置到不同的Scheduling Class中.
使用者層應用程式無法直接與排程類互動, 他們只知道上下文定義的常量SCHED_XXX(用task_struct->policy表示), 這些常量提供了排程類之間的對映。
SCHED_NORMAL, SCHED_BATCH, SCHED_IDLE被對映到fair_sched_class
SCHED_RR和SCHED_FIFO則與rt_schedule_class相關聯
3.3 就緒佇列
就緒佇列是核心排程器用於管理活動程序的主要資料結構。
各個·CPU都有自身的就緒佇列,各個活動程序只出現在一個就緒佇列中, 在多個CPU上同時執行一個程序是不可能的.
早期的核心中就緒佇列是全域性的, 即即有全域性唯一的rq, 但是 在Linux-2.6核心時代,為了更好的支援多核,Linux排程器普遍採用了per-cpu的run queue,從而克服了多CPU系統中,全域性唯一的run queue由於資源的競爭而成為了系統瓶頸的問題,因為在同一時刻,一個CPU訪問run queue時,其他的CPU即使空閒也必須等待,大大降低了整體的CPU利用率和系統性能。當使用per-CPU的run queue之後,每個CPU不再使用大核心鎖,從而大大提高了並行處理的排程能力。
就緒佇列是全域性排程器許多操作的起點, 但是程序並不是由就緒佇列直接管理的, 排程管理是各個排程器的職責, 因此在各個就緒佇列中嵌入了特定排程類的子就緒佇列(cfs的頂級排程就佇列 struct cfs_rq, 實時排程類的就緒佇列struct rt_rq和deadline排程類的就緒佇列struct dl_rq
每個CPU都有自己的 struct rq 結構,其用於描述在此CPU上所執行的所有程序,其包括一個實時程序佇列和一個根CFS執行佇列,在排程時,排程器首先會先去實時程序佇列找是否有實時程序需要執行,如果沒有才會去CFS執行佇列找是否有進行需要執行,這就是為什麼常說的實時程序優先順序比普通程序高,不僅僅體現在prio優先順序上,還體現在排程器的設計上,至於dl執行佇列,我暫時還不知道有什麼用處,其優先順序比實時程序還高,但是建立程序時如果建立的是dl程序建立會錯誤(具體見sys_fork)。
3.3.1 CPU就緒佇列struct rq
/*每個處理器都會配置一個rq*/
struct rq {
/* runqueue lock: */
spinlock_t lock;
/*
* nr_running and cpu_load should be in the same cacheline because
* remote CPUs use both these fields when doing load calculation.
*/
/*用以記錄目前處理器rq中執行task的數量*/
unsigned long nr_running;
#ifdef CONFIG_NUMA_BALANCING
unsigned int nr_numa_running;
unsigned int nr_preferred_running;
#endif
#define CPU_LOAD_IDX_MAX 5
/*用以表示處理器的負載,在每個處理器的rq中都會有對應到該處理器的cpu_load引數配置,
在每次處理器觸發scheduler tick時,都會呼叫函式update_cpu_load_active,進行cpu_load的更新
在系統初始化的時候會呼叫函式sched_init把rq的cpu_load array初始化為0.
瞭解他的更新方式最好的方式是通過函式update_cpu_load,公式如下
cpu_load[0]會直接等待rq中load.weight的值。
cpu_load[1]=(cpu_load[1]*(2-1)+cpu_load[0])/2
cpu_load[2]=(cpu_load[2]*(4-1)+cpu_load[0])/4
cpu_load[3]=(cpu_load[3]*(8-1)+cpu_load[0])/8
cpu_load[4]=(cpu_load[4]*(16-1)+cpu_load[0]/16
呼叫函式this_cpu_load時,所返回的cpu load值是cpu_load[0]
而在進行cpu blance或migration時,就會呼叫函式
source_load target_load取得對該處理器cpu_load index值,
來進行計算*/
unsigned long cpu_load[CPU_LOAD_IDX_MAX];
unsigned long last_load_update_tick;
#ifdef CONFIG_NO_HZ_COMMON
u64 nohz_stamp;
unsigned long nohz_flags;
#endif
#ifdef CONFIG_NO_HZ_FULL
unsigned long last_sched_tick;
#endif
/* capture load from *all* tasks on this cpu: */
/*load->weight值,會是目前所執行的schedule entity的load->weight的總和
也就是說rq的load->weight越高,也表示所負責的排程單元load->weight總和越高
表示處理器所負荷的執行單元也越重*/
struct load_weight load;
/*在每次scheduler tick中呼叫update_cpu_load時,這個值就增加一,
可以用來反饋目前cpu load更新的次數*/
unsigned long nr_load_updates;
/*用來累加處理器進行context switch的次數,會在呼叫schedule時進行累加,
並可以通過函式nr_context_switches統計目前所有處理器總共的context switch次數
或是可以透過檢視檔案/proc/stat中的ctxt位得知目前整個系統觸發context switch的次數*/
u64 nr_switches;
/*為cfs fair scheduling class 的rq就緒佇列 */
struct cfs_rq cfs;
/*為real-time scheduling class 的rq就緒佇列 */
struct rt_rq rt;
/* 為deadline scheduling class 的rq就緒佇列 */
/* 用以支援可以group cfs tasks的機制*/
#ifdef CONFIG_FAIR_GROUP_SCHED
/* list of leaf cfs_rq on this cpu: */
/*
在有設定fair group scheduling 的環境下,
會基於原本cfs rq中包含有若干task的group所成的排程集合,
也就是說當有一個group a就會有自己的cfs rq用來排程自己所屬的tasks,
而屬於這group a的tasks所使用到的處理器時間就會以這group a總共所分的的時間為上限。
基於cgroup的fair group scheduling 架構,可以創造出有階層性的task組織,
根據不同task的功能群組化在配置給該群主對應的處理器資源,
讓屬於該群主下的task可以透過rq機制使用該群主下的資源。
這個變數主要是管理CFS RQ list,
操作上可以透過函式list_add_leaf_cfs_rq把一個group cfs rq加入到list中,
或透過函式list_del_leaf_cfs_rq把一個group cfs rq移除,
並可以透過for_each_leaf_cfs_rq把一個rq上得所有leaf cfs_rq走一遍
*/
struct list_head leaf_cfs_rq_list;
#endif
/*
* This is part of a global counter where only the total sum
* over all CPUs matters. A task can increase this counter on
* one CPU and if it got migrated afterwards it may decrease
* it on another CPU. Always updated under the runqueue lock:
*/
/*一般來說,linux kernel 的task狀態可以為
TASK_RUNNING, TASK_INTERRUPTIBLE(sleep), TASK_UNINTERRUPTIBLE(Deactivate Task),
此時Task會從rq中移除)或TASK_STOPPED.
透過這個變數會統計目前rq中有多少task屬於TASK_UNINTERRUPTIBLE的狀態。
當呼叫函式active_task時,會把nr_uninterruptible值減一,
並透過該函式enqueue_task把對應的task依據所在的scheduling class放在對應的rq中
並把目前rq中nr_running值加一 */
unsigned long nr_uninterruptible;
/*
curr:指向目前處理器正在執行的task;
idle:指向屬於idle-task scheduling class 的idle task;
stop:指向目前最高等級屬於stop-task scheduling class
的task; */
struct task_struct *curr, *idle;
/*
基於處理器的jiffies值,用以記錄下次進行處理器balancing 的時間點*/
unsigned long next_balance;
/*
用以儲存context-switch發生時,
前一個task的memory management結構並可用在函式finish_task_switch
透過函式mmdrop釋放前一個task的結構體資源 */
struct mm_struct *prev_mm;
unsigned int clock_skip_update;
/* 用以記錄目前rq的clock值,
基本上該值會等於通過sched_clock_cpu(cpu_of(rq))的返回值,
並會在每次呼叫scheduler_tick時通過函式update_rq_clock更新目前rq clock值。
函式sched_clock_cpu會通過sched_clock_local或ched_clock_remote取得
對應的sched_clock_data,而處理的sched_clock_data值,
會通過函式sched_clock_tick在每次呼叫scheduler_tick時進行更新;
*/
u64 clock;
u64 clock_task;
/*用以記錄目前rq中有多少task處於等待i/o的sleep狀態
在實際的使用上,例如當driver接受來自task的呼叫,
但處於等待i/o回覆的階段時,為了充分利用處理器的執行資源,
這時就可以在driver中呼叫函式io_schedule,
此時就會把目前rq中的nr_iowait加一,並設定目前task的io_wait為1
然後觸發scheduling 讓其他task有機會可以得到處理器執行時間*/
atomic_t nr_iowait;
#ifdef CONFIG_SMP
/*root domain是基於多核心架構下的機制,
會由rq結構記住目前採用的root domain,
其中包括了目前的cpu mask(包括span,online rt overload), reference count 跟cpupri
當root domain有被rq參考到時,refcount 就加一,反之就減一。
而cpumask span表示rq可掛上的cpu mask,noline為rq目前已經排程的
cpu mask cpu上執行real-time task.可以參考函式pull_rt_task,當一個rq中屬於
real-time的task已經執行完畢,就會透過函式pull_rt_task從該
rq中屬於rto_mask cpu mask 可以執行的處理器上,找出是否有一個處理器
有大於一個以上的real-time task,若有就會轉到目前這個執行完成
real-time task 的處理器上
而cpupri不同於Task本身有區分140個(0-139)
Task Priority (0-99為RT Priority 而 100-139為Nice值 -20-19).
CPU Priority本身有102個Priority (包括,-1為Invalid,
0為Idle,1為Normal,2-101對應到到Real-Time Priority 0-99).
參考函式convert_prio, Task Priority如果是 140就會對應到
CPU Idle,如果是>=100就會對應到CPU Normal,
若是Task Priority介於0-99之間,就會對應到CPU Real-Time Priority 101-2之間.)
在實際的操作上,例如可以通過函式cpupri_find 傳入入一個要插入的Real-Time Task,
此時就會依據cpupri中pri_to_cpu選擇一個目前執行Real-Time Task
且該Task的優先順序比目前要插入的Task更低的處理器,
並通過CPU Mask(lowest_mask)返回目前可以選擇的處理器Mask.
可以參考kernel/sched_cpupri.c.
在初始化的過程中,通過函式sched_init呼叫函式init_defrootdomain,
對Root Domain和CPU Priority機制進行初始化.
*/
struct root_domain *rd;
/*Schedule Domain是基於多核心架構下的機制.
每個處理器都會有一個基礎的Scheduling Domain,
Scheduling Domain可以通過parent找到上一層的Domain,
或是通過child找到下一層的 Domain (NULL表示結尾.).
也可以通過span欄位,表示這個Domain所能覆蓋的處理器的範圍.
通常Base Domain會涵蓋系統中所有處理器的個數,
而Child Domain所能涵蓋的處理器個火速不超過它的Parent Domain.
而當進行Scheduling Domain 中的Task Balance,就會以該Domain所涵蓋的處理器為最大範圍.
同時,每個Schedule Domain都會包括一個或一個以上的
CPU Groups (結構為struct sched_group),並通過next欄位把
CPU Groups連結在一起(成為一個單向的Circular linked list),
每個CPU Group都會有變數cpumask來定義CPU Group
可以參考Linux Kernel檔案 Documentation/scheduler/sched-domains.txt.
*/
struct sched_domain *sd;
struct callback_head *balance_callback;
unsigned char idle_balance;
/* For active balancing */
int active_balance;
int push_cpu;
struct cpu_stop_work active_balance_work;
/* cpu of this runqueue: */
int cpu;
int online;
/*當RunQueue中此值為1,表示這個RunQueue正在進行
Fair Scheduling的Load Balance,此時會呼叫stop_one_cpu_nowait
暫停該RunQueue所出處理器排程,
並通過函式active_load_balance_cpu_stop,
把Tasks從最忙碌的處理器移到Idle的處理器器上執行. */
int active_balance;
/*用以儲存目前進入Idle且負責進行Load Balance的處理器ID.
呼叫的流程為,在呼叫函式schedule時,
若該處理器RunQueue的nr_running為0 (也就是目前沒有
正在執行的Task),就會呼叫idle_balance,並觸發Load Balance */
int push_cpu;
/* cpu of this runqueue: */
/*用以儲存前運作這個RunQueue的處理器ID*/
int cpu;
/*為1表示目前此RunQueue有在對應的處理器上並執行 */
int online;
/*如果RunQueue中目前有Task正在執行,
這個值會等等於該RunQueue的Load Weight除以目前RunQueue中Task數目的均值.
(rq->avg_load_per_task = rq->load.weight / nr_running;).*/
unsigned long avg_load_per_task;
/*這個值會由Real-Time Scheduling Class呼叫函式update_curr_rt,
用以統計目前Real-Time Task執行時間的均值,
在這個函式中會以目前RunQueue的clock_task減去目前Task執行的起始時間,
取得執行時間的Delta值. (delta_exec = rq->clock_task – curr->se.exec_start; ).
在通過函式sched_rt_avg_update把這個Delta值跟原本RunQueue中的rt_avg值取平均值.
以執行的週期來看,這個值可反應目前系統中Real-Time Task平均被分配到的執行時間值 .*/
u64 rt_avg;
/* 這個值主要在函式sched_avg_update更新 */
u64 age_stamp;
/*這值會在處理Scheduling時,若判斷目前處理器runQueue沒有正在執行的Task,
就會通過函式idle_balance更新這個值為目前RunQueue的clock值.
可用以表示這個處理器何時進入到Idle的狀態 */
u64 idle_stamp;
/*會在有Task執行且idle_stamp不為0 (表示前一個轉檯是在Idle)時
以目前RunQueue的clock減去idle_stmp所計算出的Delta值為依據,
更新這個值, 可反應目前處理器進入Idle狀態的時間長短 */
u64 avg_idle;
/* This is used to determine avg_idle's max value */
u64 max_idle_balance_cost;
#endif
#ifdef CONFIG_IRQ_TIME_ACCOUNTING
u64 prev_irq_time;
endif
#ifdef CONFIG_PARAVIRT
u64 prev_steal_time;
#endif
#ifdef CONFIG_PARAVIRT_TIME_ACCOUNTING
u64 prev_steal_time_rq;
#endif
/* calc_load related fields */
/*用以記錄下一次計算CPU Load的時間,
初始值為目前的jiffies加上五秒與1次的Scheduling Tick的間隔
(=jiffies + LOAD_FREQ,且LOAD_FREQ=(5*HZ+1))*
/
unsigned long calc_load_update;
/*等於RunQueue中nr_running與nr_uninterruptible的總和.
(可參考函式calc_load_fold_active).*/
long calc_load_active;
#ifdef CONFIG_SCHED_HRTICK
#ifdef CONFIG_SMP
int hrtick_csd_pending;
/*在函式it_rq_hrtick初始化RunQueue High-Resolution
Tick時, 此值設為0.
在函式hrtick_start中,會判斷目前觸發的RunQueue跟目前處理器所使用的RunQueue是否一致,
若是,就直接呼叫函式hrtimer_restart,反之就會依據RunQueue中hrtick_csd_pending的值,
如果hrtick_csd_pending為0,就會通過函式__smp_call_function_single讓RunQueue所在的另一個
處理器執行rq->hrtick_csd.func和函式 __hrtick_start.
並等待該處理器執行完畢後,才重新把hrtick_csd_pending設定為1.
也就是說, RunQueue的hrtick_csd_pending是用來作為SMP架構下,
由處理器A觸發處理器B執行*/
struct call_single_data hrtick_csd;
#endif
/*為gh-Resolution Tick的結構,會通過htimer_init初始化.*/
struct hrtimer hrtick_timer;
#endif
#ifdef CONFIG_SCHEDSTATS
/* latency stats */
/*為Scheduling Info.的統計結構,可以參考
include/linux/sched.h中的宣告. 例如在每次觸發
Schedule時,呼叫函式schedule_debug對上一個Task
的lock_depth進行確認(Fork一個新的Process 時,
會把此值預設為-1就是No-Lock,當呼叫
Kernel Lock時, 就會把Current Task的lock_depth加一.),
若lock_depth>=0,就會累加Scheduling Info.的bkl_count值,
用以代表Task Blocking的次數.*/
struct sched_info rq_sched_info;
/*可用以表示Run