1. 程式人生 > >Linux程序排程器的設計--Linux程序的管理與排程(十七)

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排程器的演變

一開始的排程器是複雜度為O(n)的始排程演算法(實際上每次會遍歷所有任務,所以複雜度為O(n)), 這個演算法的缺點是當核心中有很多工時,排程器本身就會耗費不少時間,所以,從linux2.5開始引入赫赫有名的O(1)排程器

然而,linux是集全球很多程式設計師的聰明才智而發展起來的超級核心,沒有最好,只有更好,在O(1)排程器風光了沒幾天就又被另一個更優秀的排程器取代了,它就是CFS排程器Completely Fair Scheduler. 這個也是在2.6核心中引入的,具體為2.6.23,即從此版本開始,核心使用CFS作為它的預設排程器,O(1)排程器被拋棄了, 其實CFS的發展也是經歷了很多階段,最早期的樓梯演算法(SD), 後來逐步對SD演算法進行改進出RSDL(Rotating Staircase Deadline Scheduler), 這個演算法已經是”完全公平”的雛形了, 直至CFS是最終被核心採納的排程器, 它從RSDL/SD中吸取了完全公平的思想,不再跟蹤程序的睡眠時間,也不再企圖區分互動式程序。它將所有的程序都統一對待,這就是公平的含義。CFS的演算法和實現都相當簡單,眾多的測試表明其效能也非常優越

欄位 版本
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