CFS調度器類
首先明確一點,CFS並不是調度器,而是一種調度器類。
傳統的調度器使用時間片的概念,對系統中的進程分別計算時間片,使得進程運行至時間片結束。在所有的進程時間片都以用盡後,重新計算時間片。而CFS調度器完全摒棄了時間片,會重點考慮進程的等待時間。
CFS調度器的目的是,向系統中每個啟用的進程提供最大的公正性,或者說確保沒有進程被虧待。注意CFS調度器僅對作用於調度類型為SCHED_NORMAL, SCHED_IDLE和SCHED_BATCH的進程。對於調度類型為SCHED_RR和SCHED_FIFO的即時進程,CFS調度器類並不起作用
CFS引入了虛擬時鐘的概念,該時鐘的流逝速度小於系統時鐘,精確的速度依賴於當前等待調度器挑選的進程的數目。假如隊列上有4個進程,那麼虛擬時鐘的速度是實際時鐘的1/4。如果系統時鐘過了20秒,那麼虛擬時鐘僅僅是5秒,從完全公平的方式來分配CPU,那麼虛擬時鐘的5秒恰好可以作為這個基準。
假定就緒隊列的虛擬時鐘為fair_clock,進程的等待時間儲存在wait_runtime,fair_clock表達了在完全公平條件下,進程需要啟動並執行時間,而wait_runtime則表明了該進程受到的不公平程度。fait_clock - wait_runtime越小表示越不公平,因此越需要被調度執行。
CFS引入了紅/黑樹狀結構來管理就緒隊列中的進程,索引值使用fair_clock-wait_runtime,因此最左邊的節點表示最不公平的進程。在這個進程獲得運行後,索引值要減去已經啟動並執行時間,這樣在這棵紅/黑樹狀結構中被移動到了右邊的一點。此時,另外一個進程變成了最左邊的節點,下一次會被調度器選擇執行。
上面的模型僅僅是一種理想的情況,實際運用中,我們還要考慮很多因素
- 進程的不同優先順序,高優先順序的進程理應獲得更多的時間
- 進程不能切換太頻繁,切換會導致進程上下文改變,頁表需要重新重新整理,代價很大。
調度器資料結構
調度器
調度器使用一系列資料結構來排序和管理系統中的進程。調度器的工作方式與這些結構的設計密切相關。
可以使用兩種方法啟用調度:
1. 直接啟用,比如進程打算休眠或者其他原因放棄CPU
2. 周期性機制,核心以固定的頻率,不時的檢測是否有必要進行進程切換。
task_struct成員
各個進程的task_struct有幾個結構和調度相關。
struct task_struct {...... int prio, static_prio, normal_prio; struct list_head run_list; const struct sched_class *sched_class; struct sched_entity se; unsigned int policy; cpumask_t cpus_allowed; unsigned int time_slice; unsigned int rt_priority;.......}
static_prio是進程的靜態優先順序,是在進程啟動時分配的優先順序,可以通過nice調用設定優先權
prio和normal_prio是進程的動態優先順序,
rt_priority是即時進程的優先順序,最低的即時優先順序是0,最高的即時優先順序是99,值越大優先順序越高,這和普通進程的靜態優先順序恰好相反
sched_class 是進程所屬的調度器類
policy表示了該進程的調度策略,當前支援5種調度策略:
- SCHED_NORMAL,我們主要講這一類進程,CFS調度策略可以處理此類進程
- SCHED_BATCH和SCHED_IDLE也通過CFS調度策略處理此類進程
- SCHED_RR和SCHED_FIFO用於實現軟即時進程。這些不是由CFS調度類來出的,而是使用即時調度器類來處理的。
調度類
調度類用來判斷接下來運行哪個進程。核心支援不同的調度策略(完全公平調度,即時調度,在無事可做時調度空閑進程)。每一個進程剛好屬於某一個調度類,各個調度類負責管理所屬的進程。
調度器類提供了通用調度器和各個調度方法之間的關聯。調度器類由特定資料結構中彙集的幾個函數指標表示。全域調度器請求的各個操作都是可以由一個指標表示。這使得無需瞭解不同調度器類的內部工作原理,即可建立通用調度器。
對於各個調度類,都必須提供struct sched_class的一個執行個體。調度類之間的階層是平坦的:即時進程最終要,在完全公平進程之前處理;而完全公平進程則優先於空閑進程,空閑進程只有CPU無事可做時才處於活動狀態。
使用者進程無法直接和調度器類互動,他們只需要定義policy為SCHED_xyz。調度器會負責根據調度策略找到相應的調度器類。SCHED_NORMAL, SCHED_BATCH, SCHED_IDLE會映射為fair_sched_class,而SCHED_RR和SCHED_FIFO與rt_sched_class關聯。fair_sched_class和rt_sched_class都是struct sched_class的執行個體,分別表示完全公平調度器和即時調度器。
就緒隊列
核心調度器用來管理活動進程的主要資料結構稱為就緒隊列。各個CPU都有自己的就緒隊列,各個活動進程只出現在一個就緒隊列中。在多個CPU中同時運行一個進程是不可能的。
就緒隊列是全域調度器許多操作的起點。但是注意,進程並不是由就緒隊列的成員直接管理的。這是調度器的職責,因此在就緒隊列中內嵌了特定與調度器類的子就緒隊列。
就緒隊列是使用下列資料結構實現的。
所有的就緒隊列都在runqueues數組中,該數組的每個元素分別對應於系統中的一個CPU。在單一處理器系統中,由於只有一個CPU,因此數組僅有一個元素。
271 /* 272 * This is the main, per-CPU runqueue data structure. 273 * 274 * Locking rule: those places that want to lock multiple runqueues 275 * (such as the load balancing or the thread migration code), lock 276 * acquire operations must be ordered by ascending &runqueue. 277 */ 278 struct rq { 279 /* runqueue lock: */ 280 spinlock_t lock; 281 282 /* 283 * nr_running and cpu_load should be in the same cacheline because 284 * remote CPUs use both these fields when doing load calculation. 285 */ 286 unsigned long nr_running; 287 #define CPU_LOAD_IDX_MAX 5 288 unsigned long cpu_load[CPU_LOAD_IDX_MAX]; 289 unsigned char idle_at_tick; 290 #ifdef CONFIG_NO_HZ 291 unsigned char in_nohz_recently; 292 #endif 293 /* capture load from *all* tasks on this cpu: */ 294 struct load_weight load; 295 unsigned long nr_load_updates; 296 u64 nr_switches; 297 298 struct cfs_rq cfs; 299 #ifdef CONFIG_FAIR_GROUP_SCHED 300 /* list of leaf cfs_rq on this cpu: */ 301 struct list_head leaf_cfs_rq_list; 302 #endif 303 struct rt_rq rt; 304 305 /* 306 * This is part of a global counter where only the total sum 307 * over all CPUs matters. A task can increase this counter on 308 * one CPU and if it got migrated afterwards it may decrease 309 * it on another CPU. Always updated under the runqueue lock: 310 */ 311 unsigned long nr_uninterruptible; 312 313 struct task_struct *curr, *idle; 314 unsigned long next_balance; 315 struct mm_struct *prev_mm; 316 317 u64 clock, prev_clock_raw; 318 s64 clock_max_delta; 319 320 unsigned int clock_warps, clock_overflows; 321 u64 idle_clock; 322 unsigned int clock_deep_idle_events; 323 u64 tick_timestamp; 324 325 atomic_t nr_iowait; 326 327 #ifdef CONFIG_SMP 328 struct sched_domain *sd; 329 330 /* For active balancing */ 331 int active_balance; 332 int push_cpu; 333 /* cpu of this runqueue: */ 334 int cpu; 335 336 struct task_struct *migration_thread; 337 struct list_head migration_queue; 304 305 /* 306 * This is part of a global counter where only the total sum 307 * over all CPUs matters. A task can increase this counter on 308 * one CPU and if it got migrated afterwards it may decrease 309 * it on another CPU. Always updated under the runqueue lock: 310 */ 311 unsigned long nr_uninterruptible; 312 313 struct task_struct *curr, *idle; 314 unsigned long next_balance; 315 struct mm_struct *prev_mm; 316 317 u64 clock, prev_clock_raw; 318 s64 clock_max_delta; 319 320 unsigned int clock_warps, clock_overflows; 321 u64 idle_clock; 322 unsigned int clock_deep_idle_events; 323 u64 tick_timestamp; 324 325 atomic_t nr_iowait; 326 327 #ifdef CONFIG_SMP 328 struct sched_domain *sd; 329 330 /* For active balancing */ 331 int active_balance; 332 int push_cpu; 333 /* cpu of this runqueue: */ 334 int cpu; 335 336 struct task_struct *migration_thread; 337 struct list_head migration_queue;
nr_running指定了隊列上可運行進程的數目,包括所有優先順序和調度類上的可運行進程。
load提供了就緒隊列當前的負載度量,負載本質上和當前就緒隊列上活動進程的數目成正比,其中每個進程還要使用他們的優先順序作為權重。每個就緒隊列的虛擬時鐘就依賴於這個資訊。
cpu_load 可以用來跟蹤此前的CPU負荷負載狀態,在周期性調度scheduler_tick中會調用update_cpu_load函數來更新CPU負荷狀態,這個數組在負載遷移時會用到。
idle指向idle進程的task_struct結構。
cfs和rt是嵌入的子就緒隊列,分別用於CFS調度和即時調度。
clock,prev_clock_raw,用於實現就緒隊列自身的時鐘,周期性調度器會調用__update_rq_clock更新這兩個值,clock記錄的是本次更新時的時鐘值,而prev_clock_raw是上一次調用__update_rq_clock的值。
tick_timestamp,每次調用scheduler都會更新tick_timestamp為當前的tick
不重要成員變數
clock_warps 和clock_overflows,由於硬體的原因,在更新就緒隊列時鐘時系統會出現時間復原,或者時間前跳的問題,這兩個變數是為了統計需要,記錄復原和前跳發生的次數。
nr_load_updates 是系統調用update_cpu_load的次數。
由於調度器需要調度更具體化的實體,因此需要一個適當的資料結構來描述此類實體。定義如下:
struct sched_entity { struct load_weight load; /* for load-balancing */ struct rb_node run_node; unsigned int on_rq; u64 exec_start; u64 sum_exec_runtime; u64 vruntime; u64 prev_sum_exec_runtime;}
load制定了權重,決定了各個實體占隊列總負荷的比例。計算負荷權重是調度器的一項重要任務。因為CFS所需的虛擬時鐘的速度最終依賴於負荷。
run_node是紅/黑樹狀結構的節點,調度實體通過這個節點掛接在紅/黑樹狀結構上
on_rq 表示該實體當前是否在就緒隊列上調度。
sum_exec_runtime 在進程運行時,我們需要記錄消耗的CPU時間,以變用於CFS調度。這個時間是實際時間。跟蹤已耗用時間是由update_curr不斷積累完成的。每次調用update_curr,都會計算目前時間和exec_start之間的差值,累加到sum_exec_runtime上,然後exec_start更新為目前時間。
vruntime 是執行期間,虛擬時間的流逝數量。
prev_sum_exec_runtime 在進程被撤銷CPU時,sum_exec_runtime儲存到prev_sum_exec_runtime中。在進程搶佔時會用到該資料。
每一個task都會內嵌一個sched_entity,所以進程是一個可調度實體,當然可調度實體不一定是進程。