# Linux 核心專題: 排程器研究 > 執行人: YangYeh-PD > [專題解說影片](https://youtu.be/m6ps9Z9htzM) (2024.06.29) ## 任務簡介 1. 檢閱學員對於 Linux 核心 CPU 排程器的分析,確認其內容的正確,適度提出疑惑和參與討論。 2. 重現《Demystifying the Linux CPU Scheduler》第 7 章的實驗。 ## TODO: 閱讀《Demystifying the Linux CPU Scheduler》 :::info 問題紀錄 1. 我們知道在 Linux 核心要求 Source Program 在把任務交給 Scheduler 之前,一定要標示好任務的類型(比如緊急與否)。那麼把一個任務歸類在 Scheduling Class 的時候,到底是由 Source Program 本身定義出來的還是由 Scheduler 識別出來的? 2. 為何 `SCHED_IDLE` 屬於 `idle_sched_class`, 兩者差別在哪裡? > 在 yenslife 的排程器研究當中有提到,scheduling class 和 scheduling policies 的差異在於,前者用來區分優先權,後者則是排程的方法。 3. `sleep` 在早期 Linux 核心排程器有什麼用途? 為何這項行程與 ==interactivity== 有關聯? ::: ### 第 2 章: Linux 核心的 CPU 排程器 #### 一個 CPU Scheduler 需要考慮的特性 CPU 排程器在搶占式多工和非搶占式多工系統中都扮演著重要角色。 * 在搶占式多工系統中,排程器允許作業系統強行中斷正在運行的行程,並將 CPU 分配給其他行程。這確保所有行程都能獲得公平的 CPU 時間,並提高系統的響應性。 * 在非搶占式多工系統中,當一個行程被創建時,它會被放置在 **runqueue** 中。然後排程器會從中**選擇**一個行程。 ```graphviz digraph shuffle { rankdir = "LR" node [shape=box, style=filled]; // Define nodes with different colors scheduler1 [label="scheduler", fillcolor="white"]; head1 [shape=none, label="head", fillcolor=white]; tail1 [shape=none, label="tail", fillcolor=white]; node1 [label="task1", fillcolor="#ffbd19", color="black"]; node2 [label="task2", fillcolor="white", color="black"]; node3 [label="task3", fillcolor="white", color="black"]; node4 [label="task4", fillcolor="white", color="black"]; node5 [label="task5", fillcolor="white", color="black"]; // Connect the nodes scheduler1 -> node1; head1 -> node1; node1 -> node2; node2 -> node3; node3 -> node4; node4 -> node5; tail1 -> node5; } ``` ##### Interactive v.s. Throughput 為何排程器往往需要在 **interactiveness** 與 **throughput** 之間做取捨?如果想要提升作業系統的互動性 (interactive),那就要進行搶佔式多工。而這種搶佔式多工當中的 context switch 會造成 CPU 短時間閒置,因此就會降低 throught。 ##### Fairness and Liveness **Fairness** 意指具有相同優先級的任務應該獲得==相等的 CPU 時間==。然而,僅有 fairness 是不夠的。如果一個高優先級的任務持續佔用 CPU,其他任務將無法執行。因此,**Liveness** 的設計變得至關重要。Liveness 能確保==每個任務在一定時間內獲得 CPU 資源==。通過 Liveness,即使是需要完成的低優先級任務也能得到執行。 ##### Realtime (RT) Realtime 分為兩種類型:Soft real-time 以及 Hard real-time。 * 在 Soft real-time 中,如果一個進程在超過截止時間後仍未被處理,後果並不嚴重。最常見的例子是音頻和視頻不同步。 * 在 Hard real-time 中,如果一個進程在超過截止時間後仍未被處理,可能會導致嚴重後果,甚至可能導致系統崩潰。 ##### Power Efficiency Linux CPU 會根據不同的平台採用不同的節能方法。在桌機和筆記本電腦上,主要有兩種節能策略 * **優先處理低功耗任務**,如閒置任務或低優先級任務。 * **根據工作負載調整 CPU 頻率**。當工作負載較輕時,降低頻率以節省能源。 在伺服器中,由於系統需要連續運行,因此在 Throughput 和功耗之間尋求平衡是至關重要的。 * 優先處理 **CPU 密集型任務**,以提高 cache(包括時間和空間方面)和處理器之間的效率。 ##### Working Pattern 所以排程器如何知道哪些任務需要優先執行呢?Linux 核心直接要求 Source Program 在將任務交給排程器進行調度之前指定優先級。Linux 核心使用 **Scheduling Classes** 的概念來實現任務調度。現在的核心版本中,有五種不同的調度類別。 ```graphviz digraph shuffle { rankdir = "LR" node [shape=box, style=filled]; // Define nodes with different colors node1 [label="Stop", fillcolor="#ffbd19", color="black"]; node2 [label="Deadline", fillcolor="white", color="black"]; node3 [label="Real-Time", fillcolor="white", color="black"]; node4 [label="Fair", fillcolor="white", color="black"]; node5 [label="Idle", fillcolor="grey", color="black"]; // Connect the nodes node1 -> node2; node2 -> node3; node3 -> node4; node4 -> node5; } ``` Scheduling classes | Scheduling Policies | |:------------------:|:---------------------------------------:| | `stop_sched_class` | | | `dl_sched_class` | `SCHED_DEADLINE` | | `rt_sched_class` | `SCHED_FIFO`&`SCHED_RR` | | `fair_sched_class` |`SCHED_NORMAL`&`SCHED_BATCH`&`SCHED_IDLE`| | `idle_sched_class` | | * **Stop** 是用來停止 CPU 任務的,僅適用於對稱多處理器(SMP)系統。它包括一種機制,可以停止在特定 CPU 上運行的行程並將其遷移到另一個 CPU。 * **Deadline** 類型的任務必須在規定的截止時間內完成。使用此策略的任務會定義一個截止時間,在該時間之前任務必須完成執行。 * 在 `rt_sched_class` 當中,有兩種 scheduling policy,分別是 `SCHED_FIFO` 和 `SCHED_RR`,這兩者非常相似。 * 在 `SCHED_FIFO` 中,FIFO 代表 First in First Out。因此任務按照到達的順序執行。然而,如果有更高優先級的任務需要執行,它將被優先執行。 * `SCHED_RR`(round robin)與 FIFO 相似,但區別在於 RR 有 timeslice。它使用輪轉的方法循環調度所有具有相同優先級的任務。每個任務只能運行一個最大固定時間片。這會讓排程更具有 Liveness 的特性。 * `SCHED_NORMAL` 和 `SCHED_OTHER` 是用於==普通任務==的 scheduling policy,會使用 CFS(Completely Fair Scheduler)排程。`SCHED_BATCH` 與 `SCHED_NORMAL` 相似,但它會更少地搶占 CPU 的資源。 * `SCHED_IDLE` 用於非常低優先級的任務,幾乎任何任務都可以搶占使用此策略的任務。 #### 早期的 Linux 核心排程器 在 Linux 核心的 v0.01 版本中,所有任務都通過一個 ==array== 表示。這個 array 不僅是所有任務的列表,也是 runqueue。這個 array 的長度為 `NR_TASKS`,即 64。空條目表示為 `NULL`。 ```graphviz graph G{ 1[shape=none,margin=0,label=< <table BORDER="0" CELLBORDER="1" CELLSPACING="0" CELLPADDING="4"> <tr> <td bgcolor="skyblue"> runnable 10 </td> <td bgcolor="skyblue"> runnable 15 </td> <td bgcolor="grey"> NULL </td> <td bgcolor="skyblue"> runnable 5 </td> <td bgcolor="lightcoral"> sleep 12 </td> </tr> </table> >]; } ``` 其演算法很簡單 1. 逆向走訪整個 array,調度第一個可運行且具有最大 timeslice 的行程。 2. 如果沒有這樣的進程,那就會重置所有任務的 timeslice,並將當前 timeslice 的一半==添加到休眠任務的 timeslice 中==。 ```graphviz graph G{ 1[shape=none,margin=0,label=< <table BORDER="0" CELLBORDER="1" CELLSPACING="0" CELLPADDING="4"> <tr> <td bgcolor="skyblue"> runnable 10 </td> <td bgcolor="skyblue"> <b>runnable 15</b> </td> <td bgcolor="grey"> NULL </td> <td bgcolor="skyblue"> runnable 5 </td> <td bgcolor="lightcoral"> sleep 12 </td> </tr> </table> >]; } ``` ```graphviz graph G{ 1[shape=none,margin=0,label=< <table BORDER="0" CELLBORDER="1" CELLSPACING="0" CELLPADDING="4"> <tr> <td bgcolor="skyblue"> <b>runnable 10</b> </td> <td bgcolor="skyblue"> runnable 0 </td> <td bgcolor="grey"> NULL </td> <td bgcolor="skyblue"> runnable 5 </td> <td bgcolor="lightcoral"> sleep 12 </td> </tr> </table> >]; } ``` ```graphviz graph G{ 1[shape=none,margin=0,label=< <table BORDER="0" CELLBORDER="1" CELLSPACING="0" CELLPADDING="4"> <tr> <td bgcolor="skyblue"> runnable 0 </td> <td bgcolor="skyblue"> runnable 0 </td> <td bgcolor="grey"> NULL </td> <td bgcolor="skyblue"> <b>runnable 5</b> </td> <td bgcolor="lightcoral"> sleep 12 </td> </tr> </table> >]; } ``` ```graphviz graph G{ 1[shape=none,margin=0,label=< <table BORDER="0" CELLBORDER="1" CELLSPACING="0" CELLPADDING="4"> <tr> <td bgcolor="skyblue"> runnable 0 </td> <td bgcolor="skyblue"> runnable 0 </td> <td bgcolor="grey"> NULL </td> <td bgcolor="skyblue"> runnable 0 </td> <td bgcolor="lightcoral"> sleep 12 </td> </tr> </table> >]; } ``` ```graphviz graph G{ 1[shape=none,margin=0,label=< <table BORDER="0" CELLBORDER="1" CELLSPACING="0" CELLPADDING="4"> <tr> <td bgcolor="skyblue"> runnable 15 </td> <td bgcolor="skyblue"> runnable 15 </td> <td bgcolor="grey"> NULL </td> <td bgcolor="skyblue"> runnable 15 </td> <td bgcolor="lightcoral"> sleep 20 </td> </tr> </table> >]; } ``` 這個過程會不斷進行,直到任務完成時,它就會離開 array,並將該元素設置為 `NULL`。 #### O(n) 排程器 在這個階段,所有任務都存在一個==連結串列==當中。當 Linux 需要選擇一個任務進行執行時,它會走訪每個任務,並為每個任務給予一個 goodness score。得分最高的任務將會獲得 CPU 資源。當任務的執行時間耗盡時,它就會被搶占,然後 CPU 會運行另一個任務。 ```graphviz digraph shuffle { rankdir = "LR" node [shape=box, style=filled]; node0 [shape=none, label="", fillcolor="white", color="black"]; // Define nodes with different colors node1 [label="task1", fillcolor="white", color="black"]; node2 [label="task2", fillcolor="grey", color="black"]; node3 [label="task3", fillcolor="white", color="black"]; node4 [label="task4", fillcolor="white", color="black"]; node5 [label="task5", fillcolor="white", color="black"]; // Connect the nodes node0 -> node2; node1 -> node2; node2 -> node3; node3 -> node4; node4 -> node5; } ``` ## TODO: 檢閱學員對於 Linux 核心 CPU 排程器的分析 > 探討以下: > * Kuanch, devarajabc $\to$ [開發紀錄](https://hackmd.io/@sysprog/rkJd7TFX0) > * yenslife $\to$ [開發紀錄](https://hackmd.io/@sysprog/rysYj43ER) > * ShawnXuanc $\to$ [開發紀錄](https://hackmd.io/@sysprog/SyTH65LUC) > > 在上方開發紀錄提出你的疑惑並參與討論。 ### Linux 核心專題: CPU 排程器研究 (Kuanch, devarajabc) * 針對《Demystifying the Linux CPU Scheduler》第 2 章排程器的 Response Time (Interactive) 與 Throughput 做補充。 > 為何 Response Time 與 Throughput 需要做取捨? > 如果想要提升作業系統的互動性 (interactive),那麼每當使用者輸入指令的時候,CPU 都需要立刻暫行當下執行的任務,並且切換到其他任務回饋給使用者。這是搶占式多工 (preemptive multitasking) 的例子。在 《Demystifying the Linux CPU Scheduler》34 頁有提到 > >>Every time the scheduler determines to context switch, the CPU becomes idle and are unoccupied by any task. > > 因此,會降低 throughput 的原因是 context switch 當中造成 CPU 短時間閒置,任務被打斷是表面原因。 * 提出建議,比較 FIFO 與 RR 兩者 Scheduling Policy 的差異,並探討其 Fairness 與 Liveness。 > Fairness 意思是相同優先權的任務會享有==相同的 CPU 時間==。 > Liveness 意思是每個任務,無論優先權高低,最終都會得到 CPU 的資源。 > > 由於兩者 Scheduling Policy 都是優先權高先執行,享有 CPU 的資源比較多,因此在 Fairness 上,兩者表現一樣。 > > 而由於 RR 每隔一個 timeslice 就會切換一次任務,就能夠讓比較低優先權的任務更有機會執行, Liveness 的特性比較好。而因為 FIFO 在執行完一個任務後才會接續執行下一個任務,因此 Liveness 的特性與 RR 相比,就沒那麼好。 * 在探討 Task Migration 的時候有提到因為在 UMA 的設計裡每個 CPU Core 存取每個記憶體的時間是一樣的,因此遷移成本可能遠小於 NUMA。 但我持不同的觀點。 > NUMA 雖然會提高 migration 的成本,但因為在 NUMA 可以涵蓋到大量的處理器,每個處理器會比其他處理器更接近某些記憶體,讓記憶體存取速度比其他部分更快速,因此在高效能運算系統也很常見。因此UMA + NUMA 的混和版本是否會犧牲掉這點,也應該考慮。 ### Linux 核心專題: CPU 排程器研究 (yenslife) * 回答同學在 Stop-task 上 task migration 的疑問。 >如果今天系統負載較低,它會關閉任意 secondary CPU,以降低能源消耗。在關閉該 CPU 之前,一定會執行 Stop-Task 類別的任務。如果你關閉的那顆 CPU 上面剛好在執行其他任務,那 stop-task 就需要把上面的任務 migrate 到其他 CPU 完成,因此需要任務遷移。 ## TODO: CPU 排程器實驗重現 重現《Demystifying the Linux CPU Scheduler》第 7 章的實驗。 * Same Nice v.s. Different Nice * FIFO v.s. RR Scheduling Policy ### 開發環境 ``` $ gcc --version gcc (Ubuntu 13.2.0-4ubuntu3) 13.2.0 $ uname -mrs Linux 6.5.0-41-generic 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): 8 On-line CPU(s) list: 0-7 Vendor ID: GenuineIntel Model name: Intel(R) Core(TM) i5-8250U CPU @ 1.60GHz CPU family: 6 Model: 142 Thread(s) per core: 2 Core(s) per socket: 4 Socket(s): 1 Stepping: 10 CPU(s) scaling MHz: 36% CPU max MHz: 3400.0000 CPU min MHz: 400.0000 BogoMIPS: 3600.00 Virtualization features: Virtualization: VT-x Caches (sum of all): L1d: 128 KiB (4 instances) L1i: 128 KiB (4 instances) L2: 1 MiB (4 instances) L3: 6 MiB (1 instance) NUMA: NUMA node(s): 1 NUMA node0 CPU(s): 0-7 ``` ### 事前準備工作 1. **更新 Linux 至版本 6.1 或更高版本。** 在 `Linhoward0522` 的排程器研究中,有相關的[操作說明](https://hackmd.io/@sysprog/BJh9FdlS2#%E6%9B%B4%E6%96%B0%E8%87%B3-Linux-v61)。 2. **下載 Scheduler Analyzer** 需要前往 [sched-analyzer](https://github.com/qais-yousef/sched-analyzer) 下載。在生成 trace file 以後,我們可以將其放到 [Perfetto](https://perfetto.dev/docs/) 從而視覺化排程器的行為。 ### FIFO v.s. RR Scheduling Policy 本實驗旨在展示 FIFO 和 RR 調度策略之間的差異。這兩種策略都屬於 real-time 的 scheduling class。為了詳細介紹這兩種 scheduling policy,我會進行三個實驗。 1. **展示 FIFO 和 RR 在相同優先權任務下的排程狀況** 我們預期在 FIFO 策略下,每個任務只有在前一個任務終止後才會被選擇執行;而在 RR 策略下,每個任務在前一個任務超過給定的 timeslice 以後都有機會執行。我將會使用以下 configuration file。 ```c { "proc": [ { "comm": "cpu_bound", "cpus": "3", "name": "fifo", "policy": "fifo", "policy_detail": "99", "spawn_cnt": 3 }, { "comm": "cpu_bound", "cpus": "2", "name": "rr", "policy": "rr", "policy_detail": "99", "spawn_cnt": 3 } ] } ``` 2. **展示 FIFO 和 RR 在不同優先權任務下的排程狀況** 因為所使用的任務優先權必須不同,因此須給每個不同優先權的任務寫 configuration file。 ```c { "proc": [ { "comm": "cpu_bound", "cpus": "3", "name": "low priority (fifo)", "policy": "fifo", "policy_detail": "97" }, { "comm": "cpu_bound", "cpus": "3", "name": "medium priority (fifo)", "policy": "fifo", "policy_detail": "98", "spawn_cnt": 2 }, { "comm": "cpu_bound", "cpus": "3", "name": "high priority (fifo)", "policy": "fifo", "policy_detail": "99" }, { "comm": "cpu_bound", "cpus": "2", "name": "low priority (rr)", "policy": "rr", "policy_detail": "97" }, { "comm": "cpu_bound", "cpus": "2", "name": "medium priority (rr)", "policy": "rr", "policy_detail": "98", "spawn_cnt": 2 }, { "comm": "cpu_bound", "cpus": "2", "name": "high priority (rr)", "policy": "rr", "policy_detail": "99" } ] } ```