本文意在透過閱讀 Linux 核心程式碼理解 CPU Scheduler 機制,並配合 Linux 核心設計: Scheduler 系列 補充以及記錄我個人的疑問和心得。
以下程式碼為 Linux 核心 v6.8-rc7
版本,本篇專注於 EEVDF。
由於 EEVDF 中存在許多基於 CFS 的機制,如 nice level 以及 vruntime,本文在說明這些機制時將以 CFS 角度說明,若不了解 CFS 以及 EEVDF 機制,建議先行閱讀 Demystifying the Linux CPU Scheduler 閱讀筆記 2024。
後續發現,由於 EEVDF 近日才被引進,許多改進仍在進行中,建議讀者在有一定理解後可以參考 Yiwei Lin 以 Patch 追蹤的方式來跟進和驗證 EEVDF 之實作。
為了編撰 Demystifying the Linux CPU Scheduler 用於紀錄我對 EEVDF Patches 的理解 :
EEVDF notes
pick_next_entity()
CFS 是為了做到比 更細緻的排程所改進的排程器,引入 nice 的概念,能夠盡可能「公平」的讓各行程獲得 CPU,即便是低優先權的任務也能夠分配到一定比例的 CPU 使用時間。
具體如何做?我們先看定義在 kernel/sched/core.c
中的 schedule()
以此我們可以紀錄其 call hierachy 直至 EEVDF 取用下一任務的函式 pick_next_entity()
:(pp1)
以上函式均在 CFS 中扮演重要腳色,我們先由 pick_next_entity()
開始分析。
pick_next_entity()
實作如下:
首先我們可以看到 NEXT_BUDDY
,在 "Demystifying the Linux CPU Scheduler" 提到
NEXT BUDDY is a feature that works after WAKEUP_PREEMPTION failed to make the waking task (wakee) preempt the currently running task. This feature gives the scheduler a preference to preempt the running task with the wakee.
NEXT_BUDDY presumes the wakee is going to consume the data produce by the waker, this feature allows us to wakes the producer/consumer pair in a consecutive manner, making them to run before any other task, increasing cache locality. Without this feature, other task may interleave the producer/consumer pair, potentially result in cache thrashing.
cfs_rq->next
是被目前任務選定的下一個任務,它們可能是 producer/consumer 的關係,能夠提升 cache locality;註釋的第二點 " 2) pick the "next" process, since someone really wants that to run" 亦驗證我們的想法。
Kuanch
cfs_rq->next
cfs_rq->last
cfs_rq->skip
實作上怎麼被挑選待追蹤;注意last
和skip
在v6.8-rc7
已不存在,在v5.19-rc7
仍可見;原因待討論,在 Main Patches 亦有討論。
vruntime_eligible
這是一個在 EEVDF 中十分重要的函式,簡而言之,它判斷一個 sched_entity
是不是夠資格使用 CPU,換句話說,其 vruntime
需要足夠小才能夠使用 CPU (小於 average vruntime 以及 小於 deadline),也是 EEVDF 的主要機制,若 則被認為是 eligible。
注意,以下程式碼事實上即是 avg_vruntime() > se->vruntime
,但因為定點數計算將導致不精確的結果,故採用此法。
想像一個情景,由於 RB tree 的平衡是最差狀況下是 ,當 leftmost node (with smallest vruntime
) 不斷被取出,平衡可能都沒有完成,但 avg_vruntime
和 avg_load
的更新是 ,我們可以透過 vruntime_eligible()
判斷該 sched_entity
是否有足夠小的 vruntime
,或是說 :
Define Where is the ideal service time and is it's virtual time counterpart, so is the difference between deserved and it really run, and
A task is eligile when
then we will have
where is cfs_rq->min_vruntime
, is se->vruntime
, is the sum of weights of se in cfs
.
即 avg >= (s64)(vruntime - cfs_rq->min_vruntime) * load;
。
cfs_rq->next
而我們如何得到 cfs_rq->next
呢?透過 check_preempt_wakeup_fair()
呼叫 set_next_buddy()
;
vruntime
什麼是 virtual runtime?是一根據任務權重計算出來的時間,越高權重之任務,其 vruntime
愈小,反之愈大。
為什麼要使用 vruntime
?為了解決高權重任務容易占用大量 CPU 時間,導致低權重任務幾乎無法使用 CPU 的狀況,故設計 vruntime
,當任務使用過 CPU 後,其 vruntime
將會被上調,越小的 vruntime
表示其越不需要 (不急迫、不具資格) 使用 CPU;理想的狀態下,我們希望所有任務的 vruntime
相等,也就是所有任務對於 CPU 的需求都相同。
Practically, it is the actual execution time normalized by the number of runnable tasks. CFS uses it to determine the next task to put on the core. The target is to maintain the virtual runtime of every task close to each other (ideally, to the same value), therefore CFS always picks the task with the smallest runtime.
任務使用 CPU 後如何增加 vruntime
?在 update_curr()
當中的 line 1168 即用於增加任務的 vruntime
,update_curr_se()
也很值得關注,它回傳的 delta_exec
來自於 rq->clock_task
,即該任務目前為止消耗的 CPU 時間。
rq->clock_task
?vruntime
本質是什麼?就是加權後的執行時間,那我們總歸要將它和真實執行時間連結起來;往上尋找 rq->clock_task
會發現它在 core.c
中被 update_rq_clock()
更新 (line 742),其更新值來自 sched_clock()
定義在 kernel/sched/clock.c
,根據 "Demystifying the Linux CPU Scheduler":
sched_clock() An important function is sched_clock(): it returns the system’s uptime in nanoseconds. An architecture may provide an implementation, but if it is not provided, the system will use the jiffies counter to calculate the system’s uptime.
由 calc_delta_fair()
往下追尋,我們會發現 __calc_delta()
進行了 delta_exec
的加權,考慮加權公式
我們可以看到 if (unlikely(se->load.weight != NICE_0_LOAD))
,因為若 NICE 值為 0 則不須加權 ( );此處有趣的是我們透過 fact = mul_u32_u32(fact, lw->inv_weight);
(計算 ) 和 ret = mul_u32_u32(al, mul) >> shift;
(計算 ) 避免使用除法,考慮 ,即是:
另外,在這之前,如 sched_fork()
時,任務就已經透過 set_load_weight()
根據其優先級設定了 struct load_weight *load
:
可以注意到 sched_prio_to_wmult
即是 sched_prio_to_weight
之倒數,如 或 ,所以需要透過 mul_u64_u32_shr()
右移。
為什麼上一個任務決定下一個任務不會影響到 fairness 呢?
The fairness here is about the long-term proportion of CPU time that each task receives. Even if NEXT_BUDDY causes a task to be scheduled sooner than others in the short term, over the long term, the CFS will ensure that each task gets its fair share of CPU time.
update_deadline()
EEVDF 使用 se->deadline
排序,故如何更新亦十分重要,update_deadline()
存在於 update_curr()
中,展示如何更新 se->deadline
。
透過 se->deadline = se->vruntime + calc_delta_fair(se->slice, se);
理解 deadline 事實上是:
其中 是 sysctl_sched_min_granularity
預設為 0.75ms。
向下追尋可以發現,事實上 se->deadline
使用的權重與 se->vruntime
相同,仍是來自於相同的 nice level,而非 latency nice,你可在 [RFC][PATCH 12/15] sched: Introduce latency-nice as a per-task attribute 看到 latency nice 的相關開發,但最後未被加入;而在 [RFC][PATCH 00/10] sched/fair: Complete EEVDF 中被接續改進。
sched_fork()
另一個主要的疑惑是 task_struct *p
是何時被指令優先級、任務類別以及其他參數的,我們能夠在 kernel/sched/core.c
找到 sched_fork()
函式,其主要在 kernel/fork.c
中被 copy_process()
呼叫,其又被 kernel_clone()
呼叫,兩者都是創造新行程的重要函式。
首先在 __sched_fork()
明顯是一個初始化操作;其後我們也可以學習到 sched_class
的不同,包含 rt_sched_class
fair_sched_class
idle_sched_class
dl_sched_class
等。
scheduler 的定義透過 DEFINE_SCHED_CLASS 這個 macro 完成,後者將 scheduler 根據 linker script 擺放到編譯出的 linux image 的對應位置。設計上,每一個執行單元在建立之後,都要選擇一種調度策略,而每一種調度策略會對應一個 sched_class。因此,除了執行單元在 scheduler 中有優先級,不同的 scheduler 之間也存在先後之分。
透過 sched_class
,我們可以提供一種類似的 OOP 實作方法,如 p->sched_class->enqueue_task
p->sched_class->dequeue_task
p->sched_class->update_curr
等等,使用同一介面使用不同 sched_class
的操作。
sched_class
and Scheduling policies is seperate ?首先,sched_class
是一種類似的 OOP 實作方法,重點在提供 OOP 介面;而 Scheduling policies 用途之一是用於設定 weight
的大小
SCHED_IDLE
的優先級甚至可以小於最小的 nice value。
- SCHED_IDLE: This is even weaker than nice 19, but its not a true idle timer scheduler in order to avoid to get into priority inversion problems which would deadlock the machine.
由於多處理器多核心架構被現代運算廣泛使用,平行程式也開始普及,但一個重要的問題隨即浮現:是否會有些處理器負載特別高,而有些負載特別低的不平衡狀況呢?顯然是會的,故我們需要一種方法來衡量每個處理器的負載,並動態調整派發任務;Per-Entity Load Tracking (PELT) 的概念因此被廣泛應用,如果我們有能力評估每個任務對核心的負載,就能夠評估個別處理器的負載了。
考慮如果使用例如權重 (load.weight),CPU-bound 和 I/O-bound 很可能有相同的權重,但後者大多數時間在等待;PELT 是持續監測任務使用 CPU 的時間,唯有這樣才能夠貼合我們的目的:負載平衡 (Load Balance)。
To accurately measure load, it is necessary to monitor the amount of time a task utilizes the CPU. Thus, a task’s load becomes a combination of its weight and average CPU utilization, and the core’s load is the sum of its tasks’ loads.
如何“監測任務使用 CPU 的時間” ?首先找到儲存負載的變數,其存在於 sched_entity
中:
進一步觀察 sched_avg
enqueue_task_fair()
用於加入任務到等待執行的佇列中,並且重新評估該任務的各項數值,譬如調整頻率;首先出現的兩個函式 util_est_enqueue
和 cpufreq_update_util()
即是:
util_est_enqueue()
可以看到,當任務每次被加入到 cfs_rq
之前 (h_nr_running++
),必須先更新 cfs_rq->avg.util_est
;此外,p->se.avg.util_est
則是在 dequeue_task_fair()
時被 util_est_update()
更新。
p->se.avg.util_est
有四種更新分支被定義在 util_est_update()
:
大於 ewma (使用率上升)
p->se.avg.util_est = p->se.avg.util_avg
小於 ewma 但在誤差內 (使用率不變)
p->se.avg.util_est = p->se.avg.util_est
任務得到超過 CPU capacity 的使用時間
p->se.avg.util_est = p->se.avg.util_est
任務並沒有得到理論上的使用時間
p->se.avg.util_est = p->se.avg.util_avg
小於 ewma
p->se.avg.util_est = 0.25 * (p->se.avg.util_est) + 0.75 * (p->se.avg.util_avg)
此處的命名很容易混淆 ewma = READ_ONCE(p->se.avg.util_est);
和 dequeued = task_util(p);
,前者應該是指 p->se.avg.util_est
是過去資料的 EWMA,而後者為 p->se.avg.util_avg
才是該任務當前的使用率。
se->avg. util_avg
updated?se.avg.util_avg
cpufreq_update_util()
cpufreq
EEVDF 的重要函式之一便是接續 pick_next_entity()
的 pick_eevdf()
:
Line 899 - 906 便闡述了 EEVDF 的核心:
curr
使用 CPU 直到 non-eligible