在上一節我們瞭解了CFS的設計原理,包括CFS的引入,CFS是如何實現公平,CFS工作原理的。本小節我們重點在分析CFS調度器中涉及到的一些常見的數據結構,對這些數據結構做一個簡單的概括,梳理各個數據結構之間的關係圖出來。
調度類
CFS調度器是在Linux2.6.23引入的,在當時就提出了調度類概念,調度類就是將調度策略模塊化,有種面向對象的感覺。先來看下調度類的數據結構,調度類是通過struct sched_class數據結構表示
struct sched_class {
const struct sched_class *next;
void (*enqueue_task) (struct rq *rq, struct task_struct *p, int flags);
void (*dequeue_task) (struct rq *rq, struct task_struct *p, int flags);
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,
struct rq_flags *rf);
void (*put_prev_task)(struct rq *rq, struct task_struct *p);
void (*set_curr_task)(struct rq *rq);
void (*task_tick)(struct rq *rq, struct task_struct *p, int queued);
由於sched_class成員比較多,我們這裏先介紹幾個常用的。可以看到調度類中基本都是一些函數指針,這些函數指針分別代表的不同的含義。
- next 是用來指向下一級的一個調度類,內核中爲每個調度策略提供了一個調度類,這些調度類是通過next成員鏈接到一起
- enqueue_task: 用來將一個進程添加到就緒隊列中,同時會增加它的可運行的進程數
- dequeue_task: 用來將一個進程從就緒隊列移除,同時減少可運行進程的數量
- check_preempt_curr: 用來檢測當一個進程的狀態設置爲runnable時,檢查當前進程是否可以發生搶佔
- pick_next_task: 在運行隊列中選擇下一個最合適的進程來運行
- put_prev_task: 獲得當前進程之前的那個進程
- set_curr_task: 用來設置當前進程的調度狀態等
- task_tick: 在每個時鐘tick的時候會調度各個調度類中的tick函數
Linux內核都提供了那些調度類
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;
Linux內核定義了五種調度類,而且每種調度有對應的調度策略,而每種調度策略有會對應調度哪裏進程。
/*
* 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
同時也提供了六種調度策略。下表示調度策略和調度類之間的關係
調度類 | 調度策略 | 調度對象 |
---|---|---|
stop_sched_class(停機調度類) | 無 | 停機的進程 |
dl_sched_class(限期調度類) | SCHED_DEADLINE | dl進程 |
rt_sched_class(實時調度類) | SCHED_RR 或者 SCHED_FIFO | 實時進程 |
fair_sched_class(公平調度類) | SCHED_NORMAL 或者 SCHED_BATCH | 普通進程 |
idle_sched_class(空閒調度類) | SCHED_IDLE | idle進程 |
同時這些調度類之間是有優先級關係的。
#ifdef CONFIG_SMP
#define sched_class_highest (&stop_sched_class)
#else
#define sched_class_highest (&dl_sched_class)
#endif
#define for_each_class(class) \
for (class = sched_class_highest; class; class = class->next)
如果定義了SMP,則最高優先級的是stop調度類。調度類的優先級關係
stop_sched_class > dl_sched_class > rt_sched_class > fair_sched_class > idle_shced_class
調度實體
struct sched_entity {
/* For load-balancing: */
struct load_weight load;
unsigned long runnable_weight;
struct rb_node run_node;
struct list_head group_node;
unsigned int on_rq;
u64 exec_start;
u64 sum_exec_runtime;
u64 vruntime;
u64 prev_sum_exec_runtime;
u64 nr_migrations;
struct sched_statistics statistics;
從Linux2.6.23開始引入了調度實體的概念,調度實體封裝了進程的一些重要的信息。在之前的O(1)算法中調度的單位都是task_struct,而在Linux2.6.23引入調度模塊化後,調度的單位成爲調度實體sched_entity
- load就是此進程的權重
- run_node:CFS調度是通過紅黑樹來管理進程的,這個是紅黑樹的節點
- on_rq: 此值爲1時,代表此進程在運行隊列中
- exec_start: 記錄這個進程在CPU上開始執行任務的時間
- sum_exec_runtime: 記錄這個進程總的運行時間
- vruntime: 代表的是進程的虛擬運行時間
- prev_sum_exec_runtime: 記錄前面一個進程的總的運行時間
- nr_migrations: 負載均衡時進程的遷移次數
- statistics:進程的統計信息
紅黑樹
紅黑樹大家肯定不陌生了,樹左邊節點的值永遠比樹右邊接的值小。
在O(n)和O(1)的調度器中運行隊列都是通過數組鏈表來管理的,而在CFS調度中拋棄了之前的數據結構,採用了以時間爲鍵值的一棵紅黑樹。其中的時間鍵值就是進程的vruntime。
CFS維護了一課以時間爲排序的紅黑樹,所有的紅黑樹節點都是通過進程的se.vruntime來作爲key來進行排序。CFS每次調度的時候總是選擇這棵紅黑樹最左邊的節點,然後來調度它。隨着時間的推移,之前在最左邊的節點隨着運行,進程的vruntime也隨之增大,這些進程慢慢的會添加到紅黑樹的右邊。循環往復這個樹上的所有進程都會被調度到,從而達到的公平。
同時CFS也會維護這棵樹上最小的vruntime的值cfs.min_vruntime,而且這個值是單調遞增的。此值用來跟蹤運行隊列中最小的vruntime的值。
運行隊列
系統中每個CPU上有有一個運行隊列struct rq數據結構,這個struct rq是個PER-CPU的,每個CPU上都要這樣的一個運行隊列,可以可以防止多個CPU去併發訪問一個運行隊列。
/*
* This is the main, per-CPU runqueue data structure.
*
* Locking rule: those places that want to lock multiple runqueues
* (such as the load balancing or the thread migration code), lock
* acquire operations must be ordered by ascending &runqueue.
*/
struct rq {
unsigned int nr_running;
/* capture load from *all* tasks on this CPU: */
struct load_weight load;
unsigned long nr_load_updates;
u64 nr_switches;
struct cfs_rq cfs;
struct rt_rq rt;
struct dl_rq dl;
可以從註釋看到struct rq是一個per-cpu的變量。
- nr_running: 代表這個運行隊列上總的運行進程數
- load: 在這個CPU上所有進程的權重,這個CPU上可能運行的進程有實時進程,普通進程等
- nr_switches: 進程切換的統計數
- struct cfs_rq: 就是CFS調度類的一個運行隊列
- struct rt_rq: 代表的是rt調度類的運行隊列
- struct dl_rq: 代表的是dl調度類的運行隊列
可以得出的一個結論是,一個struct rq中包括了各種類型的進程,有DL的,有實時的,有普通的。通過將不同進程的掛到不同的運行隊列中管理。
/* CFS-related fields in a runqueue */
struct cfs_rq {
struct load_weight load;
unsigned long runnable_weight;
unsigned int nr_running;
unsigned int h_nr_running;
u64 exec_clock;
u64 min_vruntime;
從註釋上看struct cfs_rq代表的是CFS調度策略對應的運行隊列
- load: 是這個CFS_rq的權重,包含着CFS就緒隊列中的所有進程
- nr_running: 代表的是這個CFS運行隊列中可運行的進程數
- min_vruntime: 此值代表的是CFS運行隊列中所有進程的最小的vruntime
看下運行隊列的關係圖
每個CPU中都存在一個struct rq運行隊列,struct rq中根據進程調度策略分爲不同的運行隊列,比如普通進程就會掛載到cfs_rq中,在struct cfs_rq中則定義了每一個調度實體,每一個調度實體根據vruntime的值添加到紅黑樹的節點中。