# Linux 核心專題: CPU 排程器研究
> 執行人: linhoward0522
> [專題解說錄影](https://youtu.be/la0i6O4JS48)
:::success
:question: 提問清單
* BORE 排程器跟CFS比犧牲了什麼??數據看的出來嗎
> 我認為犧牲的是「公平性」,可以從下面實驗看出來
> 公平性 : 兩個具有相同優先權級的任務應該要運行幾乎相同的時間
> #### CPU-bound and yield
> 在處理器核編號 2 上執行的任務分別是 CPU-bound 與 yield 類型的任務
> - CPU_bound 任務 : 在 main 函數中有一個 for 迴圈,每次迭代都會進行矩陣乘法,共迭代四次
> - yield 任務 : 與 CPU-bound 任務相同,只是每次迭代進行矩陣乘法後,會呼叫 `sched_yield` 主動放棄處理器資源
> - [ ] BORE
>![](https://hackmd.io/_uploads/BJInXCTIn.png)
> - [ ] CFS
>![](https://hackmd.io/_uploads/rJIXw06I3.png)
>
>由實驗結果可以看出當每次輪到 yield 類型的任務使用 CPU 資源時, BORE 會給予更多的 timeslice (因為可以看到最後面 CPU-bound 任務使用一大段時間的 CPU 資源,表示 yield 類型的任務被給予更多的 timeslice),所以 yield 類型的任務會比 CPU-bound 類型的任務還要早執行完成。而 CFS 則是非常公平的平均分配 timeslice 給兩種任務。此實驗結果也與 BORE 原作者所說的 : 「稍微偏離 CFS 原有的 complete fairness 原則」相同。
* 「EEVDF 非常頻繁的切換執行,因為 EEVDF 的 timeslice 較短」這陳述是否意味著 EEVDF 的執行成本較高?
> 在處理器核編號 2,3 號上使用 [stress](https://linux.die.net/man/1/stress) 來各產生 2 個 CPU-bound 任務,並且利用 taskset 將其指定到對應的處理器核編號上
> 我利用 [perf](https://perf.wiki.kernel.org/index.php/Main_Page) 來簡單觀察一下,以下是收集 10 秒鐘的結果
- CFS
```
-------------------------------------------------------------------------------------------------------------------------------------------
Task | Runtime ms | Switches | Avg delay ms | Max delay ms | Max delay start | Max delay end |
-------------------------------------------------------------------------------------------------------------------------------------------
stress:(4) | 20001.528 ms | 1255 | avg: 15.907 ms | max: 16.008 ms | max start: 101497.845449 s | max end: 101497.861457 s
migration/3:34 | 0.000 ms | 1 | avg: 0.000 ms | max: 0.000 ms | max start: 0.000000 s | max end: 0.000000 s
migration/2:28 | 0.000 ms | 1 | avg: 0.000 ms | max: 0.000 ms | max start: 0.000000 s | max end: 0.000000 s
-----------------------------------------------------------------------------------------------------------------
TOTAL: | 20001.528 ms | 1257 |
---------------------------------------------------
```
- BORE
```
-------------------------------------------------------------------------------------------------------------------------------------------
Task | Runtime ms | Switches | Avg delay ms | Max delay ms | Max delay start | Max delay end |
-------------------------------------------------------------------------------------------------------------------------------------------
stress:(4) | 20001.590 ms | 3308 | avg: 6.042 ms | max: 8.014 ms | max start: 101527.792998 s | max end: 101527.801012 s
migration/3:34 | 0.000 ms | 1 | avg: 0.000 ms | max: 0.000 ms | max start: 0.000000 s | max end: 0.000000 s
migration/2:28 | 0.000 ms | 1 | avg: 0.000 ms | max: 0.000 ms | max start: 0.000000 s | max end: 0.000000 s
-----------------------------------------------------------------------------------------------------------------
TOTAL: | 20001.590 ms | 3310 |
---------------------------------------------------
```
- EEVDF
```
-------------------------------------------------------------------------------------------------------------------------------------------
Task | Runtime ms | Switches | Avg delay ms | Max delay ms | Max delay start | Max delay end |
-------------------------------------------------------------------------------------------------------------------------------------------
stress:(4) | 20005.383 ms | 5007 | avg: 3.994 ms | max: 4.009 ms | max start: 21546.291197 s | max end: 21546.295206 s
migration/3:34 | 0.000 ms | 1 | avg: 0.000 ms | max: 0.000 ms | max start: 0.000000 s | max end: 0.000000 s
migration/2:28 | 0.000 ms | 1 | avg: 0.000 ms | max: 0.000 ms | max start: 0.000000 s | max end: 0.000000 s
-----------------------------------------------------------------------------------------------------------------
TOTAL: | 20005.383 ms | 5009 |
---------------------------------------------------
```
可以看出 EEVDF 的切換次數遠高於 CFS , BORE,切換次數越高表示執行成本較高。
:::
## 任務簡述
研讀《Demystifying the Linux CPU Scheduler》,更新書中關於 [BORE](https://github.com/firelzrd/bore-scheduler) 及 [schedgraph](https://gitlab.inria.fr/schedgraph/schedgraph) 的描述,採用 Linux v6.1。
## 事前準備
[Demystifying the Linux CPU Scheduler 筆記](https://hackmd.io/@linhoward0522/HyC28W9N3)
### 更新至 Linux v6.1
:::spoiler 後來發現應該要使用自行編譯的 Linux 核心,改用以下方法:
查看目前版本
```shell
$ uname -mrs
Linux 5.19.0-41-generic x86_64
```
更新所有 package
```shell
$ sudo apt update
```
新增 `cappelikan` repository 以及下載 Ubuntu Mainline Kernel Installer
```shell
$ sudo add-apt-repository ppa:cappelikan/ppa -y
$ sudo apt install mainline -y
```
打開 Ubuntu Mainline Kernel Installer 找出所需要的版本並安裝,最後重開機即可
```shell
$ uname -mrs
Linux 6.1.28-060128-generic x86_64
```
:::
下載需要的套件
```shell
$ sudo apt install gcc make libncurses-dev flex bison libssl-dev libelf-dev initramfs-tools-bin
```
下載指定的核心版本並解壓縮
```shell
$ wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.1.29.tar.xz
$ tar -xvf linux-6.1.29.tar.xz
```
進入該目錄,並複製核心開啟的檔案到當下目錄的 config 檔
```shell
$ cd linux-6.1.29
$ cp /boot/config-5.19.0-41-generic .config
```
建立編輯選單,進入選單後將 config 檔載入
```shell
$ make menuconfig
```
- 選擇 load .config
- 選擇 save .config
- 按兩次 ESC 離開選單
修改 `.config` 檔案,將下列兩行內容刪除,如下所示
```shell
$ vim .config
CONFIG_SYSTEM_TRUSTED_KEYS=""
CONFIG_SYSTEM_REVOCATION_KEYS=""
```
編譯模組,安裝核心後重開機
```shell
$ make -j 8
$ sudo make modules_install
$ sudo make install
$ sudo reboot
```
確認是否安裝成功
```shell
$ uname -mrs
Linux 6.1.29 x86_64
```
## 開發環境
```shell
$ gcc --version
gcc (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0
$ uname -mrs
Linux 6.1.29 x86_64
$ lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Address sizes: 39 bits physical, 48 bits virtual
Byte Order: Little Endian
CPU(s): 12
On-line CPU(s) list: 0-11
Vendor ID: GenuineIntel
Model name: 12th Gen Intel(R) Core(TM) i5-12400
CPU family: 6
Model: 151
Thread(s) per core: 2
Core(s) per socket: 6
Socket(s): 1
Stepping: 2
CPU max MHz: 5600.0000
CPU min MHz: 800.0000
BogoMIPS: 4992.00
Virtualization features:
Virtualization: VT-x
Caches (sum of all):
L1d: 288 KiB (6 instances)
L1i: 192 KiB (6 instances)
L2: 7.5 MiB (6 instances)
L3: 18 MiB (1 instance)
NUMA:
NUMA node(s): 1
NUMA node0 CPU(s): 0-11
```
## TODO: 在 Linux v6.1 測試 [BORE](https://github.com/firelzrd/bore-scheduler) 並解釋其原理
參見 [BORE (Burst-Oriented Response Enhancer) CPU Scheduler](https://hackmd.io/@foxhoundsk/bore-sched) 及 [BMQ 排程器研究](https://hackmd.io/@SmallHanley/linux2022-projects),搭配《Demystifying the Linux CPU Scheduler》的第 3 章,在 Linux v6.1 (LTS) 重現實驗,解讀實驗數據,並評估是否該更新電子書描述。
> 留意 [BORE](https://github.com/firelzrd/bore-scheduler) 專案頁面的 "How it works",已更新 BORE 的設計描述和相關圖表,其中 EEVDF 可參照 LWN: [An EEVDF CPU scheduler for Linux
](https://lwn.net/Articles/925371/)
### BORE(Burst-Oriented Response Enhancer)
是 CFS 和 EEVDF 的改進,在既有的程式碼基礎之上,儘快對應用程式作出回應。
BORE 為每個任務引入 `burstiness` ,稍微偏離 CFS 原有的 complete fairness 原則。 `Burstiness` 是指一個任務在明確放棄 CPU 後消耗的 CPU 時間所得出的分數,無論是進入睡眠、 I/O waiting 或放棄 CPU 資源。
利用這個 `burstiness` 指標,BORE 動態調整每個任務的權重和延遲。因此在不同類型負載的系統中,BORE 會優先處理需要快速回應的任務,以提高整體系統回應能力。
#### How it works
- 排程器會追蹤每個任務的 burst time,也就是任務從上次放棄 CPU 資源、睡眠或 I/O waiting 以來消耗的 CPU 時間。
- 當任務處於活動狀態時,透過正規化的 burst time 以及使用預設的偏移量跟係數來計算 burst score。
- burst score 的功能類似於 niceness ,介於 0-39 之間。當數值每減少 1,任務可以消耗大約 1.25 倍長的 timeslice。
- burst score 會影響剛睡醒任務搶佔目前執行任務的積極度。(只有 CFS 版本,EEVDF 版本沒有)。
- 較不貪婪的任務被賦予更多 timeslice 以及睡醒任務搶佔目前執行任務的積極度;貪婪任務則被賦予較少的權重。
- 最終是希望在各種工作負載共存的情況下,在 CPU-bound 與 I/O-bound 任務之間取得平衡,提高整體系統回應能力。
### BORE 程式碼解釋
>![](https://hackmd.io/_uploads/r1rlhmsB3.png)
BORE 主要實作於 `update_curr()` 中,用來更新目前行程在 CPU 上執行所花費的實際時間以及 vruntime
首先在 `include/linux/sched.h` 中的 [`struct sched_entity`](https://github.com/torvalds/linux/blob/master/include/linux/sched.h#L549) 增加以下成員
```diff
@@ -555,6 +555,12 @@ struct sched_entity {
u64 sum_exec_runtime;
u64 vruntime;
u64 prev_sum_exec_runtime;
+#ifdef CONFIG_SCHED_BORE
+ u64 prev_burst_time;
+ u64 burst_time;
+ u64 max_burst_time;
+ u8 penalty_score;
+#endif // CONFIG_SCHED_BORE
```
#### `update_curr()`
```diff=
@@ -905,6 +1005,14 @@ static void update_curr(struct cfs_rq *cfs_rq)
curr->sum_exec_runtime += delta_exec;
schedstat_add(cfs_rq->exec_clock, delta_exec);
+#ifdef CONFIG_SCHED_BORE
+ curr->burst_time += delta_exec;
+ curr->max_burst_time = max(curr->max_burst_time, curr->burst_time);
+ update_burst_score(curr);
+ if (sched_bore & 1)
+ curr->vruntime += penalty_scale(calc_delta_fair(delta_exec, curr), curr);
+ else
+#endif // CONFIG_SCHED_BORE
curr->vruntime += calc_delta_fair(delta_exec, curr);
update_min_vruntime(cfs_rq);
```
- 第 6 行是更新 burst_time ,delta_exec 是指任務到目前為止的執行時間
- 第 7 行是更新 max_burst_time ,取 burst_time 與 max_burst_time 的最大值
- 若呼叫 `dequeue_task_fair()` 或 `yield_task_fair()` 函式時,會透過 `restart_burst()` 函式將 burst_time 與 prev_burst_time 做正規化(依照目前設定是相加除以二)來更新 max_burst_time
- 第 8 行是呼叫 `update_burst_score()` 函式來更新 burst_score
- 第 10 行是透過 `penalty_scale()` 函式來更新 vruntime
- 呼叫 `calc_delta_fair()` 函式來更新 vruntime
- 將 vruntime 乘上對 `sched_prio_to_wmult` 查表後得到的數值
- burst_score 越高,查表得到的數值越大,相乘後使得 vruntime 成長幅度越大
#### `update_burst_score()`
```diff=
+#define FIXED_SHIFT 10
+static void update_burst_score(struct sched_entity *se) {
+ u64 burst_time = se->max_burst_time;
+
+ int msb = fls64(burst_time);
+ fixed integer_part = msb << FIXED_SHIFT;
+ fixed fractional_part = burst_time << (64 - msb) << 1 >> (64 - FIXED_SHIFT);
+ fixed greed = integer_part | fractional_part;
+
+ fixed tolerance = sched_burst_penalty_offset << FIXED_SHIFT;
+ fixed penalty = max(0, (s32)greed - (s32)tolerance);
+ fixed scaled_penalty = penalty * sched_burst_penalty_scale >> 10;
+
+ u8 score = min(39U, scaled_penalty >> FIXED_SHIFT);
+ se->penalty_score = score;
+}
```
- 第 3 行是把 burst_time 設定成透過 `update_curr()` 函式中更新過的 max_burst_time
- 第 5 ~ 8 行是將 burst_time 以 2 為底取對數並加上 1
- integer_part 是以 2 為底取對數並加上 1 的結果
- fractional_part 是以 2 為底取對數時所失去的小數部份。最後會儲存在 `greed` 中較低位元的 `FIXED_SHIFT` 長度
- 第 11 行是將 `penalty` 設為 `greed` 減去 `tolerance` 並與 0 取 `max`
- `tolerance` 是透過預設的偏移量所計算出來
- `sched_burst_penalty_offset` 增加這個數值可防止較短 burst_time 的任務過於強大。
- 第 12 ~ 15 行是先對 `penalty` 做 scaling ,並右移 10 位將小數部份去掉,最後將該數值與 39 取 min 作為 burst score
- `sched_burst_penalty_scale` 增加這個數值會使 burst score 隨著 burst time 的增加而快速增長。導致 CPU-Bound 任務可能很難被執行到
### EEVDF (Earliest Eligible Virtual Deadline First)
某些行程可能不需要大量的 CPU 時間,但當行程需要 CPU 時間時,必須盡快分配給該行程。 CFS 沒有給行程表示 `latency requirements` 的方法,確保某些行程(即時任務)可以快速存取 CPU 。
與 CFS 一樣,EEVDF 會公平的分配 CPU 時間。較低的 nice value 具有較高優先級。
對於每個行程,EEVDF 計算行程本應獲得的時間與實際獲得的時間之間的差值,稱為 `lag` 。 具有 `positive lag value` 的行程表示未獲得其應獲得的時間,要比具有 `negative lag value` 的行程更早被排程到。
當行程的 `lag value` 大於等於 0 ,才會被認為是 `eligible` 。任何 `negative lag value` 行程都沒有資格運行。在未來某個時間,該行程本應獲得的時間追上實際獲得的時間,會再次被認為是 `eligible` ,該時間稱為 `eligible time` 。
> 依據[劍橋辭典](https://dictionary.cambridge.org/dictionary/english/eligible),"eligible" 寓意是合格、具備特定資格者,用於 CPU 排程器就是挑選出有資格成為下個任務的候選者
`lag value` 的計算是 EEVDF 排程的關鍵部分,而另一個因素是 `virtual deadline` 。是行程收到其應有的 CPU 時間的最早時間,透過行程分配到的 timeslice 加上 `eligible time` 來計算的。 EEVDF 會優先執行 `virtual deadline` 最早的行程。
> 例如一個行程具有 10 ms timeslice,其 `eligible time` 為未來 20 ms,則 `virtual deadline` 為未來 30 ms。
具有較短 timeslice 的行程會有較小的 `virtual deadline` ,所以 EEVDF 會分配較短的 timeslice ,讓有 `latency requirements` 的行程優先執行,能夠快速回應事件。
### 重現 BORE 實驗
使用 `0001-linux6.1.y-bore2.2.8.patch` 補丁並重新編譯核心
```shell
$ patch -p1 -i 0001-linux6.1.y-bore2.2.8.patch
```
同時編譯另一個使用 EEVDF 排程器的核心。先使用 EEVDF 的補丁,接著再使用 EEVDF-BORE 的補丁
以 [`jitterdebugger`](https://github.com/igaw/jitterdebugger) 搭配 [`stress-ng`](https://wiki.ubuntu.com/Kernel/Reference/stress-ng) 測量, `jitterdebugger` 扮演的是 interactive task,而 `stress-ng` 扮演的則是背景工作負載。
> `jitterdebugger` 測量 wake up latency。
> `jitterdebugger` 在每個 CPU 上使用一個執行緒然後進行睡眠。在睡眠結束後,獲取當下 timestamp,也就是實際 wake up 時間,並與預期的 wake up 時間相減,即是 wake up latency
可以透過以下命令來切換要實驗的排程器
```shell
$ sudo sysctl -w kernel.sched_bore=3
```
若是基於 CFS 版本的 BORE
- 3 是 BORE
- 0 是 CFS
若是基於 EEVDF 版本的 BORE
- 1 是 BORE
- 0 是 EEVDF
產生在背景執行的工作負載以測試排程器並製圖。設定 stressor 個數為 48,測試 10 分鐘
```shell
$ sudo ./jitterdebugger -D 10m -c 'stress -c 48' -o result
$ ./jitterplot --output res.jpg hist result/results.json
```
- CFS 版本的 BORE
![](https://hackmd.io/_uploads/HyCrOIs8n.png)
- CFS
![](https://hackmd.io/_uploads/HkuU_LjU3.png)
- EEVDF 版本的 BORE
![](https://hackmd.io/_uploads/BkQwdUsL2.png)
- EEVDF
![](https://hackmd.io/_uploads/ryZud8j83.png)
上面為 `jitterdebugger` 的測量結果,橫軸為 scheduling latency,縱軸為次數。
由實驗結果可以看出 CFS 的 max scheduling latency 是所有排程器中最差的(cpu5 達到 1068)。而兩種版本的 BORE 多數的 max scheduling latency 都相當的低,表示 BORE 的確會優先處理需要快速回應的任務。而 avg scheduling latency 的表現其實所有排程器都差不多。
:::warning
TODO: 搭配上方排程器設計的描述,解讀實驗結果,特別是為何 EEVDF 與宣稱不同。
:notes: jserv
:::
## TODO: 理解 [schedgraph](https://gitlab.inria.fr/schedgraph/schedgraph) 原理並實驗
INRIA 的 Julia Lawall 教授在 FOSDEM 2023 有場精彩的演講〈[Graphing tools for scheduler tracing](https://fosdem.org/2023/schedule/event/sched_tracing/)〉(內附簡報檔案和錄影),談及 [schedgraph](https://gitlab.inria.fr/schedgraph/schedgraph) 這工具,預期產出:
* 在本頁面介紹 [schedgraph](https://gitlab.inria.fr/schedgraph/schedgraph) 原理
* 設計實驗 (對照《Demystifying the Linux CPU Scheduler》第 7 章),並以 [schedgraph](https://gitlab.inria.fr/schedgraph/schedgraph) 展現 CFS 排程行為
* 對照 [BORE](https://github.com/firelzrd/bore-scheduler) 和 CFS 行為,使用 [schedgraph](https://gitlab.inria.fr/schedgraph/schedgraph) 展現
### 介紹 [schedgraph](https://gitlab.inria.fr/schedgraph/schedgraph) 原理
> 參考資料 :
> * [schedgraph](https://gitlab.inria.fr/schedgraph/schedgraph)
> * [FOSDEM 2023 演講](https://fosdem.org/2023/schedule/event/sched_tracing/)
> * [Graphing Tools for Scheduler Tracing](https://inria.hal.science/hal-04001993/preview/RR-9498.pdf)
`trace-cmd` 是用於收集和視覺化追蹤執行的 `ftrace` 的前端工具。
我們主要使用的工具是 [`schedgraph`](https://gitlab.inria.fr/schedgraph/schedgraph),由 `dat2graph` 和 `running_waiting` 組成。這兩個工具都依賴於 `trace-cmd`。
我們使用 `dat2graph` 和 `running_waiting` 而不是 `trace-cmd` 或是它的圖形化介面 `kernelshark` ,是因為較難追蹤擁有大量處理器核系統的排程器活動。
首先介紹一些名詞 :
- Work conservation : 如果有處理器核空閒,則不應使處理器核過載。也就是當一個任務準備好執行時,排程器應該傾向於將任務放在一個空閒的處理器核上,而不是放在有任務已經在執行的處理器核上。
- Locality : NUMA 機器的影響很大,其中處理器核對本地記憶體的存取速度較快,而對其他 NUMA 節點上的記憶體存取速度較慢。 work conservation 與 locality 會發生衝突,將任務放在目前空閒但位於不同 NUMA 節點的處理器核上,可能會降低存取速度。
:::warning
注意用語:
* kernel : 核心
* (processor) core : 處理器核 (避免寫「核心」)
> 了解,接下來會注意用語。
:::
`dat2graph` 和 `running_waiting` 分別提供在處理器核上的任務活動的視覺化,了解大型多核機器上排程器的行為。兩種不同的方法提供了互補的資訊 :
- `dat2graph2` : 了解何時、在哪個處理器核執行。只有顯示正在執行的任務,沒有顯示正在等待的任務。
![](https://hackmd.io/_uploads/ryo9s1G83.png)
- x 軸是執行時間(秒)
- y 軸是處理器核,顏色代表 PID
從上圖可以看出並非所有任務都在相同的處理器核上執行,例如接近 3 秒和 15 秒時,任務可能會移動到其他處理器核上或暫停執行。
若任務移動頻繁,可能會喪失 Locality。
- `running_waiting` : 有多少任務在執行或是等待
![](https://hackmd.io/_uploads/HkBhskMUh.png)
- x 軸是執行時間(秒)
- y 軸是任務數
- 綠線表示正在執行的任務數量
- 紅線表示可執行任務的數量,即正在執行與正在等待的任務
- 黑色水平虛線表示機器的處理器核數
如果紅線可視,即高於綠線,則表示某些可執行任務正在等待,即出現過載。
如果紅線在黑色虛線上方,則任務多於處理器核數,則過載是不可避免的。但如果紅線可視,且低於黑色虛線,則表示存在 Work conservation 問題,代表一些處理器核過載、但有些處理器核是空閒的。也就是上圖中 15 到 20 秒之間。
### 設計實驗並以 [schedgraph](https://gitlab.inria.fr/schedgraph/schedgraph) 展現 CFS 排程行為
> 參考資料 :
> - [trace-cmd](https://github.com/rostedt/trace-cmd)
> - [Ocaml 安裝](https://ocaml.org/install)
> - [Ocaml 安裝手冊](https://ocaml.org/docs/up-and-running#first-steps-with-ocaml)
> - [jgraph](http://web.eecs.utk.edu/~jplank/plank/jgraph/jgraph.html)
#### 安裝必要工具
為了執行 [schedgraph](https://gitlab.inria.fr/schedgraph/schedgraph) 專案,首先需要安裝一些必要的工具。
安裝 trace-cmd 以及相關套件
```shell
$ sudo apt-get install build-essential git pkg-config -y
$ sudo apt-get install libtracefs-dev libtraceevent-dev -y
$ git clone https://git.kernel.org/pub/scm/libs/libtrace/libtraceevent.git
$ cd libtraceevent
$ make
$ sudo make install
$ git clone https://git.kernel.org/pub/scm/libs/libtrace/libtracefs.git
$ cd libtracefs
$ make
$ sudo make install
$ git clone https://github.com/rostedt/trace-cmd.git
$ cd trace-cmd
$ make
$ sudo make install
$ sudo make install_libs
```
[schedgraph](https://gitlab.inria.fr/schedgraph/schedgraph) 專案利用 OCaml 程式語言撰寫,所以我們需要安裝 OCaml 以及其套件管理器 opam
```shell
$ bash -c "sh <(curl -fsSL https://raw.githubusercontent.com/ocaml/opam/master/shell/install.sh)"
```
安裝完後對 opam 進行初始化,接著下載開發工具
```shell
$ opam init
$ opam install dune merlin ocaml-lsp-server odoc ocamlformat utop dune-release
```
安裝特定版本的 OCaml 並運行該版本
```shell
$ opam switch create 4.14.0
$ eval opam env
```
> 實驗過若下載 5.0.0 版本, schedgraph 目前不支援
確認版本是否正確
```shell
$ which ocaml
/home/howard/.opam/4.14.0/bin/ocaml
$ ocaml -version
The OCaml toplevel, version 4.14.0
```
安裝製圖工具
```shell
$ sudo apt-get install jgraph
$ sudo apt-get install ps2eps
$ sudo apt-get install texlive-font-utils
```
取得 [schedgraph](https://gitlab.inria.fr/schedgraph/schedgraph) 專案程式碼
```shell
$ git clone https://gitlab.inria.fr/schedgraph/schedgraph.git
$ cd schedgraph
$ make all
```
#### 將行程固定在特定的 CPU 中執行
修改 /etc/default/grub 內的 `GRUB_CMDLINE_LINUX_DEFAULT` ,加入 `isolcpus=2,3` 來指定編號 2,3 的處理器核不會被排程器賦予任務,修改完成後需 `sudo update-grub` 來更新設定,最後重新開機
```shell
$ sudo vi /etc/default/grub
GRUB_CMDLINE_LINUX_DEFAULT="quiet splash isolcpus=2,3"
$ sudo update-grub
$ sudo reboot
```
重新開機後檢查 CPU affinity
```shell
$ cat /sys/devices/system/cpu/isolated
2-3
```
#### 實驗
利用 `mktasks` 程式產生 workload ,並執行 `start.py` 來利用以下命令進行追蹤並產生對應的 `.dat` 檔案
```shell
$ sudo trace-cmd record -e sched -v -e sched_stat_runtime
```
- `record` : 記錄追蹤的事件
- `-e sched` : 追蹤所有排程相關的事件
- `-v -e sched_stat_runtime` : 忽略統計收集事件,因為它們數量多且與理解為什麼將任務放置在各個處理核上無關
接著利用 `dat2graph` 和 `running_waiting` 來產生追蹤事件的視覺化圖片
#### Difference between FIFO and RR
參數設定與書中第七章的設定一樣。首先分為兩組,每一組各有三個任務。執行的任務都是 CPU-bound 類型。第一組的任務利用 FIFO 排程,第二組的任務利用 RR 排程,並分別運行在處理核編號 3 跟 2 號。
使用下列命令來產生 `dat2graph` 圖片
```shell
$ ./dat2graph2 --save-tmp /path/to/fifo-rr-trace.dat
$ jgraph fifo-rr-trace.jgr | ps2eps -q -l -r 600 | epstopdf --filter > fifo-rr-trace.pdf
```
利用 `--save-tmp` 命令列引數來儲存產生的 jgraph 檔案,接著利用 jgraph 命令來將檔案轉換成 pdf 檔
![](https://hackmd.io/_uploads/HkhFl4DU2.png)
可以看得很清楚處理核編號 3 的任務是利用 FIFO 來做排程,而處理核編號 2 的任務是利用 RR 來做排程。但仔細看的話可以發現在執行結束時,有出現非常小的點。所以我利用以下命令來放大來看
```shell
$ ./dat2graph2 --color-by-command --min 1.136 --max 1.138 --save-tmp /path/to/fifo-rr-trace.dat
$ jgraph fifo-rr-trace_color_from_1.315_upto_1.318_color.jgr | ps2eps -q -l -r 600 | epstopdf --filter > fifo-rr-trace_color_zoomin.pdf
```
`--color-by-command` 會根據正在執行的任務名稱來上色
![](https://hackmd.io/_uploads/SyNffNwUn.png)
可以發現任務執行結束時, kworker 會執行。所以我利用 kernelshark 去細看資訊時發現當任務執行結束時,kworker 會去執行 swapper 。因為沒有其它可執行的任務。
使用下列命令來產生 `running_waiting` 圖片
```shell
$ ./running_waiting --save-tmp --range 0.0-1.4 /path/to/fifo-rr-prio-trace.dat
$ jgraph fifo-rr-prio-trace_rw_from_0.0_upto_1.4.jgr | ps2eps -q -l -r 600 | epstopdf --filter > fifo-rr-prio-trace_rw.pdf
```
![](https://hackmd.io/_uploads/Sy81pVvLn.png)
圖中可以看到有紅線,表示存在 Work conservation 問題,但是是正常的。因為我們只有在兩個處理核上執行任務,其它尚未被排程到的任務只能等待。
#### Priority of FIFO and RR
一樣分為兩組,每一組各有四個任務。執行的任務都是 CPU-bound 類型。第一組的任務利用 FIFO 排程,第二組的任務利用 RR 排程,且任務的優先權級分別為 97,98,98,99 ,並分別運行在處理核編號 3 跟 2 號。
觀察 `dat2graph` 圖片
![](https://hackmd.io/_uploads/B1JZfSwUh.png)
處理核編號 3 號的任務一樣是利用 FIFO 來做排程,會按照優先權級依序執行。而處理核編號 2 號的任務是利用 RR 來做排程,會先執行優先權級最高的任務,然後因為有兩個任務的優先權級相同,所以當它們的 timeslice 消耗完時會切換執行,最後執行優先權級最低的任務。
一樣可以發現在執行結束時,有出現非常小的點,將這個部份放大來看。
![](https://hackmd.io/_uploads/HJt5QSwLh.png)
一樣利用 kernelshark 來查看詳細資訊,因為沒有其它可執行的任務,所以 kworker 會去執行 swapper 。
觀察 `running_waiting` 圖片
![](https://hackmd.io/_uploads/BJcU4HDUn.png)
一樣存在 Work conservation 問題,因為尚未被排程到的任務只能等待。
#### Block the FIFO
:::info
實驗過 [schedgraph](https://gitlab.inria.fr/schedgraph/schedgraph) 必須將任務運行在多個處理核上,故讓處理器核編號 3 上也有執行任務,但任務內容為不做任何事。
:::
處理核編號 2 號有 I/O_block 跟 yield 類型的任務各兩個,皆是利用 FIFO 來做排程
觀察 `dat2graph` 圖片
![](https://hackmd.io/_uploads/H1CHj0aIh.png)
因為任務數量較多搭配以下 `dat2graph` 圖片來觀察
![](https://hackmd.io/_uploads/rJqq20aIh.png)
可以發現在處理核編號 2 號上是依序執行 I/O_block 跟 yield 類型的任務。當任務進行 I/O 操作或是呼叫 `sched_yield` 時,會釋放 CPU 資源並讓下一個任務執行
觀察 `running_waiting` 圖片
![](https://hackmd.io/_uploads/Sk8QRCpLh.png)
同樣存在 Work conservation 問題
### 對照 [BORE](https://github.com/firelzrd/bore-scheduler) , [EEVDF](https://lwn.net/Articles/925371/) 和 CFS 行為,使用 [schedgraph](https://gitlab.inria.fr/schedgraph/schedgraph) 展現
::: info
以下實驗皆有在處理器核編號 3 上執行任務,任務內容為不做任何事。僅為了能讓 [schedgraph](https://gitlab.inria.fr/schedgraph/schedgraph) 正常運作
而出現 unknown 是因為 trace-cmd 初始化的副作用所產生
:::
為了方便觀察 BORE , EEVDF 與 CFS 的排程行為,所以我設計以下三種實驗。分別是扮演使用 CPU 時間較長的 CPU-bound 類型的任務以及扮演使用 CPU 時間較少(會主動放棄 CPU )的任務,並都具有相同的優先權級
#### CPU-bound and yield
在處理器核編號 2 上執行的任務分別是 CPU-bound 與 yield 類型的任務
- CPU_bound 任務 : 在 main 函數中有一個 for 迴圈,每次迭代都會進行矩陣乘法,共迭代四次
- yield 任務 : 與 CPU-bound 任務相同,只是每次迭代進行矩陣乘法後,會呼叫 `sched_yield` 主動放棄處理器資源
#### BORE
![](https://hackmd.io/_uploads/BJInXCTIn.png)
#### EEVDF
![](https://hackmd.io/_uploads/BJjVyMg_n.png)
#### CFS
![](https://hackmd.io/_uploads/rJIXw06I3.png)
由實驗結果可以看出當每次輪到 yield 類型的任務使用 CPU 資源時, BORE 會給予更多的 timeslice (因為可以看到最後面 CPU-bound 任務使用一大段時間的 CPU 資源,表示 yield 類型的任務被給予更多的 timeslice),所以 yield 類型的任務會比 CPU-bound 類型的任務還要早執行完成。因為優先權級一樣,所以 EEVDF 會公平的分配 CPU 時間,但可以發現 EEVDF 非常頻繁的切換執行,因為 EEVDF 的 timeslice 較短。最後 CFS 則是非常公平的平均分配 timeslice 給兩種任務。
#### CPU-bound and sleep
在處理器核編號 2 上執行的任務分別是 CPU-bound 與 sleep 類型的任務
- CPU_bound 任務 : 迭代進行矩陣乘法
- sleep 任務 : 每次迭代進行矩陣乘法後,會呼叫 `usleep(1)` 讓任務去睡眠 1 微秒
#### BORE
![](https://hackmd.io/_uploads/BkHvE0aUh.png)
#### EEVDF
![](https://hackmd.io/_uploads/r12v1zgO3.png)
#### CFS
![](https://hackmd.io/_uploads/Hklc4C6I3.png)
由實驗結果可以看出每次輪到 sleep 類型的任務使用 CPU 資源時, BORE 同樣給予更多的 timeslice (可以看到最後面 CPU-bound 任務使用一大段時間的 CPU 資源)。同樣的 EEVDF 非常頻繁的切換執行,同時公平分配 CPU 時間。而 CFS 則是平均分配 timeslice 。
#### CPU-bound and I/O_block
在處理器核編號 2 上執行的任務分別是 CPU-bound 與 I/O_block 類型的任務
- CPU-bound 任務 : 迭代進行矩陣乘法
- I/O_block 任務 : 每次迭代進行矩陣乘法後,會進行 I/O 操作
#### BORE
![](https://hackmd.io/_uploads/HyoyHCaL2.png)
#### EEVDF
![](https://hackmd.io/_uploads/SJ0Fyfluh.png)
#### CFS
![](https://hackmd.io/_uploads/rJgpNRT82.png)
由實驗結果可以看出每次輪到 I/O_block 類型的任務使用 CPU 資源時, BORE 有給予更多的 timeslice ,所以執行過程中沒有出現 idle 。同樣的 EEVDF 非常頻繁的切換執行,且當 I/O_block 類型的任務完成寫檔後,又繼續與 CPU-bound 任務公平分配 CPU 時間,因為具有相同的優先權級。而 CFS 則是因為平均分配 timeslice ,導致有 idle 的情況發生。因為 CPU-bound 任務已經完成,但是 I/O_block 任務還在進行 I/O 操作且尚未完成。
::: info
結論 : BORE 會讓長時間使用 CPU 資源的任務賦予它更高的 vruntime ,所以該任務會較不容易被排程到。而使用 CPU 資源較少的任務會有較高的 burst score ,也就是會被賦予更多的 timeslice 並且會增加搶佔目前執行任務的積極度。
:::
:::warning
TODO: 針對 EEVDF 進行實驗,可能需要更新的 Linux 核心: [linux-cachyos](https://github.com/CachyOS/linux-cachyos)。BORE 的作者在 CachyOS 的 Telegram 很活躍。
:::
### [BORE 及 schedgraph 的英文描述](https://hackmd.io/@linhoward0522/HyHb3BsPh)
## TODO: 探討 CPU 排程器延遲的議題
研讀以下技術報告和論文:
* [Reducing CPU scheduler latency in Linux](https://www.diva-portal.org/smash/get/diva2:1630380/FULLTEXT01.pdf)
* [Reducing latency spikes by tuning the CPU scheduler](https://www.scylladb.com/2016/06/10/read-latency-and-scylla-jmx-process/)
* [Efficient Scheduler Live Update for Linux Kernel with Modularization](https://dl.acm.org/doi/abs/10.1145/3582016.3582054)
解讀 CPU 排程器的延遲和調整機制,分析如何設計實驗來理解實作的表現。
### 解讀 CPU 排程器的延遲和調整機制
> 參考資料 :
> - [perf 用法](https://jasonblog.github.io/note/linux_tools/perf_--_linuxxia_de_xi_tong_xing_neng_diao_you_gon2.html)
> - [perf bench](https://man7.org/linux/man-pages/man1/perf-bench.1.html)
> - [sysctl output changed from kernel 5.10 to 5.13](https://forum.endeavouros.com/t/sysctl-output-changed-from-kernel-5-10-to-5-13-why/17097)
> - [hackbench](https://lkml.iu.edu//hypermail/linux/kernel/0112.1/0702.html)
排程延遲 : 當一個行程可以運行卻不能有進展,因為排程器將 CPU 時間分配給其它不同的行程
首先在處理器核編號 2,3 號上使用 [stress](https://linux.die.net/man/1/stress) 來各產生 2 個 CPU-bound 任務,並且利用 taskset 將其指定到對應的處理器核編號上
```shell
$ taskset -c 2 stress -c 2
$ taskset -c 3 stress -c 2
```
接著我們利用 [perf](https://perf.wiki.kernel.org/index.php/Main_Page) 來觀察排程延遲的時間,並利用 `sudo perf sched latency` 來產生報告
```shell
$ sudo perf sched record -C 2,3 sleep 10
$ sudo perf sched latency
```
- `sched` 表示獲取排程相關的資訊
- `record` 紀錄資訊
- `-C` 指定收集該處理器核的資訊
- `sleep` 表示要收集幾秒鐘
以下是收集 10 秒鐘的結果
- CFS
```
-------------------------------------------------------------------------------------------------------------------------------------------
Task | Runtime ms | Switches | Avg delay ms | Max delay ms | Max delay start | Max delay end |
-------------------------------------------------------------------------------------------------------------------------------------------
stress:(4) | 20001.528 ms | 1255 | avg: 15.907 ms | max: 16.008 ms | max start: 101497.845449 s | max end: 101497.861457 s
migration/3:34 | 0.000 ms | 1 | avg: 0.000 ms | max: 0.000 ms | max start: 0.000000 s | max end: 0.000000 s
migration/2:28 | 0.000 ms | 1 | avg: 0.000 ms | max: 0.000 ms | max start: 0.000000 s | max end: 0.000000 s
-----------------------------------------------------------------------------------------------------------------
TOTAL: | 20001.528 ms | 1257 |
---------------------------------------------------
```
- BORE
```
-------------------------------------------------------------------------------------------------------------------------------------------
Task | Runtime ms | Switches | Avg delay ms | Max delay ms | Max delay start | Max delay end |
-------------------------------------------------------------------------------------------------------------------------------------------
stress:(4) | 20001.590 ms | 3308 | avg: 6.042 ms | max: 8.014 ms | max start: 101527.792998 s | max end: 101527.801012 s
migration/3:34 | 0.000 ms | 1 | avg: 0.000 ms | max: 0.000 ms | max start: 0.000000 s | max end: 0.000000 s
migration/2:28 | 0.000 ms | 1 | avg: 0.000 ms | max: 0.000 ms | max start: 0.000000 s | max end: 0.000000 s
-----------------------------------------------------------------------------------------------------------------
TOTAL: | 20001.590 ms | 3310 |
---------------------------------------------------
```
可以看出 CFS 的平均以及最大排程延遲時間都大於 BORE 的數值,因為 BORE 會比較快回應任務,所以 BORE 的切換次數遠高於 CFS
所以我想透過調整 CFS 的排程器參數,來實驗看看是否這些參數會影響排程延遲的時間
首先,進入 root 權限並列出所有 CFS 的排程器參數
```shell
$ cd /sys/kernel/debug/sched/
$ ls
debug latency_warn_ms numa_balancing
domains latency_warn_once preempt
features migration_cost_ns tunable_scaling
idle_min_granularity_ns min_granularity_ns verbose
latency_ns nr_migrate wakeup_granularity_
```
> 現在使用 `sudo sysctl -a | grep "sched" | grep -v "domain"` 命令僅能列出以下資訊 :
>```
>kernel.sched_autogroup_enabled = 1
>kernel.sched_cfs_bandwidth_slice_us = 5000
>kernel.sched_child_runs_first = 0
>kernel.sched_deadline_period_max_us = 4194304
>kernel.sched_deadline_period_min_us = 100
>kernel.sched_energy_aware = 1
>kernel.sched_rr_timeslice_ms = 100
>kernel.sched_rt_period_us = 1000000
>kernel.sched_rt_runtime_us = 950000
>kernel.sched_schedstats = 0
>kernel.sched_util_clamp_max = 1024
>kernel.sched_util_clamp_min = 1024
>kernel.sched_util_clamp_min_rt_default = 1024
>```
我使用 `sched_min_granularity_ns` , `sched_wakeup_granularity_ns` 這兩個參數來進行實驗
- `sched_min_granularity_ns` : 表示分配給每個任務 timeslice 的下限,也就是每個任務被搶佔之前能夠執行的最短時間。
目前預設值為 : 3000000 ns
- `sched_wakeup_granularity_ns` : 表示當另外一個任務睡醒時,會延遲當前正在執行任務的搶佔,目的是為了不要頻繁的排程。所以在設定的範圍內,非常接近當前任務 vruntime 數值的任務不會搶佔當前任務。
目前預設值為 : 4000000 ns
### 設計實驗
為了進行測量排程延遲的時間,我使用 [hackbench](https://lkml.iu.edu//hypermail/linux/kernel/0112.1/0702.html) 工具來進行實驗。 [hackbench](https://lkml.iu.edu//hypermail/linux/kernel/0112.1/0702.html) 是在核心中透過 IPC 的方式來測量延遲的工具。 [hackbench](https://lkml.iu.edu//hypermail/linux/kernel/0112.1/0702.html) 會建立幾對任務,同一組內的任務可以互相通訊。這些任務來回通訊,而延遲即是每對任務通訊的時間。
我寫了一隻測試程式,調整 `sched_min_granularity_ns` , `sched_wakeup_granularity_ns` 數值的同時,使用以下命令來測量排程延遲的時間
> [測試程式](https://github.com/linhoward0522/Latency_test)
```shell
$ sudo perf record -e sched:* perf bench sched messaging -p -t -g 20 -l 1000
$ sudo perf sched latency
```
- `-p` : 透過使用 pipe 來消除 socket communication 的開銷
- `-t` : 透過使用執行緒來消除建立行程的開銷
- `-g` : 指定組的數量。每一組裡面各有 20 個 sender 跟 receiver 執行緒。同組的任務互相通訊
- `-l` : 指定通訊次數
#### sched_min_granularity_ns
- 執行時間
![](https://hackmd.io/_uploads/SyC4IYtDh.png)
- 平均延遲時間
![](https://hackmd.io/_uploads/By2HLFKD2.png)
- 最大延遲時間
![](https://hackmd.io/_uploads/S1SIIttPn.png)
#### sched_wakeup_granularity_ns
- 執行時間
![](https://hackmd.io/_uploads/rkvv8KFP3.png)
- 平均延遲時間
![](https://hackmd.io/_uploads/SkUuUYYP2.png)
- 最大延遲時間
![](https://hackmd.io/_uploads/r1lFUKKD2.png)
:::info
由以上實驗可以觀察出,調整 `sched_min_granularity_ns` 數值似乎對於執行時間或是平均,最大延遲時間沒有太大的影響。反而是調整 `sched_wakeup_granularity_ns` 數值比較有影響。
當 `sched_wakeup_granularity_ns` 數值越高時,執行時間與最大延遲時間越低。而 `sched_wakeup_granularity_ns` 數值介於 5000000 ~ 20000000 ns 時平均延遲時間的表現比較好。因為透過減少了不必要的搶佔來提昇效能。
:::
#### sched_min_granularity_ns vs sched_wakeup_granularity_ns
- 執行時間
![](https://hackmd.io/_uploads/rkr4DFFDn.png)
- 平均延遲時間
![](https://hackmd.io/_uploads/HyIrDtYPh.png)
- 最大延遲時間
![](https://hackmd.io/_uploads/r1gUwFYw3.png)
:::info
同時調整 `sched_min_granularity_ns` , `sched_wakeup_granularity_ns`數值。對於執行時間來說,調整 `sched_wakeup_granularity_ns` 數值比較有影響。而對於平均延遲時間來說, `sched_wakeup_granularity_ns` 數值介於 5000000 ~ 15000000 ns 時較好,與調整 `sched_min_granularity_ns` 較無關連。最後是對於最大延遲時間, `sched_wakeup_granularity_ns` 數值越高時,最大延遲時間較好。同樣是因為透過減少不必要的搶佔來提昇效能。
:::