# 第七講:不只挑選任務的排程器 > 本筆記僅為個人紀錄,相關教材之 Copyright 為[jserv](http://wiki.csie.ncku.edu.tw/User/jserv)及其他相關作者所有 * 直播: * ==[Linux 核心設計:不只挑選任務的排程器(上) - 2023/3/13](https://youtu.be/X2T7U6AEWrY)== * ==[Linux 核心設計:不只挑選任務的排程器(中) - 2023/3/13](https://youtu.be/S2MP4hqNXX0)== * ==[Linux 核心設計:不只挑選任務的排程器(下) - 2022/4/19](https://youtu.be/-wn-ttOvQl0)== * 詳細共筆:[Linux 核心設計:不只挑選任務的排程器](https://hackmd.io/@sysprog/linux-scheduler) * 主要參考資料: * [Evolution of the x86 context switch in Linux](https://www.maizure.org/projects/evolution_x86_context_switch_linux/) * [CPU Scheduling of the Linux Kernel (2022)](https://docs.google.com/presentation/d/1qDSFOfhGF-AX3O8CNzoAkQr_5ohr0nMVDlNfPnM9hOI/edit?usp=sharing) --- 本課整理了 Linux 核心中 CPU 排程器 (CPU Scheduler) 的相關概念、演化以及面臨的挑戰。排程器不僅僅是挑選下一個任務執行,它需要考慮公平性 (fairness)、反應時間 (interactiveness)、多核處理器的負載平衡 (load balancing)、進階電源管理以及即時處理 (real-time processing) 等多個複雜議題。 --- ## 排程器的目標與概念 排程器 (Scheduler) 在計算機科學中,是將待辦任務分配給可用資源以完成工作的方法。以數學觀點來看,==**排程是個函數,將任務集合映射到資源集合**==。 ### Linux CPU 排程器: 其核心任務是 **將一系列的行程 (Process),即各種工作負載,映射到一個個 CPU 核心上執行**。這些任務包括常駐系統的服務、各式代理程式 (Agent)、初始化腳本 (initscripts) 及週期性任務等。 #### 排程器的挑戰: 作為一個特殊的函數,排程器需要考量多方面因素,包括: * **公平性 (Fairness)**:確保各個任務能獲得合理的執行機會。 * **優先權 (Priority)**:允許重要性較高的任務優先執行。 * **搶佔 (Preemption)**:允許高優先權任務中斷低優先權任務的執行。 * **反應時間 (Interactiveness/Response Time)**:對於 time sharing,應能快速回應使用者的操作。 * **吞吐量 (Throughput)**:單位時間內完成的工作總量應最大化。 * **效率 (Efficiency)**:最小化排程器本身的開銷。 * **可擴展能力 (Scalability)**:系統從單核擴展到多核,乃至於 NUMA (Non-Uniform Memory Access) 架構時,排程器依然能有效運作。 * **電源管理 (Power Management)**:在行動裝置和伺服器環境中,有效降低功耗,如[EAS, Energy Aware Scheduling](https://developer.arm.com/open-source/energy-aware-scheduling)。 * **即時性 (Real-time Capability)**:對於即時任務,必須在限定的時間內完成。 #### 排程器的任務分類 不同應用場景有不同需求,因此任務可分類為: * **普通行程 (Normal process)** * **即時行程 (Real-time process)**:對反應時間的快與精準要求最高。 * **互動式行程 (Interactive process)**:與使用者互動,對排程延遲敏感。 * **批次處理行程 (Batch process)**:背景執行,更注重吞吐量。 #### Linux 排程的設計式與權衡 (Patterns / Trade-offs) 設計排程器時,需考慮以下問題: * **任務組織**: * **共享單一執行佇列 (RunQueue)**:所有資源共享一個任務佇列,排程時需透過鎖定 (locking) 保證互斥。 * **每個資源獨立的執行佇列**:減少鎖定開銷,但需處理資源閒置時從其他佇列「偷取」任務的問題。 * **資源利用**: * **執行到完成 (Run to completion)**:適用於計算密集且在意延遲的場景 (如網路設備的訊息處理)。 * **時間分片 (Time slicing)**:適用於 I/O 密集或在意系統總體使用率的場景。 * **RunQueue 資料結構**:各有優劣 * FIFO 佇列 (Linked-List) * 紅黑樹 (Red-Black Tree) * 點陣圖 (bitmap) + FIFO 佇列 ... * **演算法**: * 迭代走訪挑選任務 ($O(N)$) * 對任意任務進行循環分配 (Round Robin) * 加權循環分配 (Weighted Round Robin) ... ### 資訊系統中的其他排程器: 1. **Erlang scheduler**:負責 Erlang 行程到作業系統 Thread 之間的映射。 2. **Nginx**:負責將 HTTP/HTTPS 請求映射到後端的行程或應用程式。 3. **記憶體管理器 (Memory manager)**:負責虛擬記憶體到實體記憶體之間的映射。 4. **Hypervisor**:負責虛擬機器 (VM) 到實體硬體資源之間的映射。 5. **Amazon Elastic Compute Cloud (EC2)**:負責 VM 到 Hypervisor 之間的映射。 6. **Apache Spark**:負責 Map/Reduce 任務到計算節點之間的映射。 #### 這些映射帶來的好處包括: * 資源更高效的利用 * 降低任務與資源間的耦合度 (Indirection) * 更好的服務品質 (Quality of Service, QoS)。 :::success 正如 David Wheeler 所言: > "All problems in computer science can be solved by another level of indirection" (電腦科學中的所有問題都可以透過增加一個間接層來解決)。 ::: 許多系統都面臨任務數量遠大於資源數量的挑戰,這種情況稱為「超額認購」(Oversubscribed)。排程器透過引入間接層,降低資源兩端的耦合度,從而有效地支援超額認購。 --- ## CPU 排程對系統的影響 排程器本身的運作會消耗系統資源,這是其主要代價。理想情況下,這個代價應越小越好。 ```graphviz digraph G { size="5!"; // 使用無形狀或純文字節點以呈現 HTML 標籤 node [shape=none]; // 定義橫向長條:採用 HTML 表格,每個 <TD> 設定背景色與寬度 bar [ label=<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0"> <TR> <TD BGCOLOR="#D1E9E9" WIDTH="90" HEIGHT="20"></TD> <TD PORT="p1" BGCOLOR="orange" WIDTH="40" HEIGHT="20"></TD> <TD PORT="p0" BGCOLOR="#D1E9E9" WIDTH="110" HEIGHT="20"></TD> <TD BGCOLOR="orange" WIDTH="30" HEIGHT="20"></TD> <TD BGCOLOR="#D1E9E9" WIDTH="90" HEIGHT="20"></TD> <TD BGCOLOR="orange" WIDTH="40" HEIGHT="20"></TD> <TD BGCOLOR="#D1E9E9" WIDTH="105" HEIGHT="20"></TD> </TR> </TABLE>> ]; // 在第一個橘色段上方放置文字節點,文字顏色設為橘色 lbl [ label="Scheduling time, as little as possible", fontcolor="orange", fontsize="20", fontname="Arial" ]; lbl2 [ label="Task running time", fontcolor="black", fontsize="20", fontname="Arial" ]; // 讓 lbl 在 bar 的上方 (透過 rank 來控制垂直位置) { rank=source; lbl; } { rank=sink; lbl2; } // 從文字節點連一條箭頭到 bar 的第一個橘色段 lbl2:w -> bar:p0 [color="black", arrowhead="vee"]; lbl:w -> bar:p1 [color="orange", arrowhead="vee"]; } ``` ### 測量排程器成本 - 實驗 要測量 Linux 進行一次重新排程 (reschedule) 的成本,可以使用 GNU/Linux 下的 [perf 工具](http://wiki.csie.ncku.edu.tw/embedded/perf-tutorial)。 1. 使用 `perf bench sched pipe` 命令可以測量兩個行程之間 **透過管道 (pipe) 進行百萬次操作的總時間**,從而推算出單次操作的成本。 ```shell $ perf bench sched pipe # Running 'sched/pipe' benchmark: # Executed 1000000 pipe operations between two processes Total time:33.412 [sec] 33.412879 usecs/op 29928 ops/sec ``` > [!Note]測試結果 > 在 16 核 Intel i7-12650H 上 (WSL2),每次操作約耗時 33.41 $\mu s$。 2. 為了消除多核心環境下行程分佈帶來的影響,可以使用 `taskset` 工具將 process **固定在特定的 CPU 核心上執行**: ```shell $ taskset -c 0 perf bench sched pipe # Running 'sched/pipe' benchmark: # Executed 1000000 pipe operations between two processes Total time:2.408 [sec] 2.408468 usecs/op 415201 ops/sec ``` > [!Note] 固定在單一核心後 > 每次操作的成本降至約 2.4 $\mu s$。顯示了 process 在不同核心間遷移的成本。 這些測試雖然不夠嚴謹 (未完全排除其他系統行程的影響),但能提供排程器處理速度的數量級概念。**排程器切分出的時間片段 (timeslice) 應高於這個數量級才有意義**。與其說是排程效能,不如說是上下文交換 (Context Switch) 的表現,其中也包含了切換頁表 (page table) 的成本。 > 相比之下,使用者層級的排程器 (如 Golang 的 goroutine 排程器) 由於沒有核心層級的複雜性 (如優先權、搶佔),其延遲可以低得多。 --- ## 行程、執行緒、群組、會話 (Process, Thread, Group, Session) Linux 核心在處理 process 和 thread 時,其界線相對模糊,主要是為了最大化程式碼的重用性。理解 Linux 中任務的組織方式對於理解排程器至關重要。 ### 多層級結構 * **執行緒 (Thread)**: * **獨立**:執行上下文 (Execution Context) 和執行緒 ID (Thread ID)。 * **共享**:大部分其他資源,特別是 Address Space、Signal Handlers 和 Process ID (PID)。 * **執行緒群組 (Thread Group)** * Thread Group 共享的識別碼是 **Thread Group Leader (TGL) 的 PID**,即群組中第一個 Thread 的 PID。 * Thread 只能透過退出 (exiting) 來離開一個 Process。 * **行程 (Process)**:擁有獨立的 Address Space。 * **行程群組 (Process Group)**: * 每個 **行程描述子 (Process Descriptor,即 `task_struct`)** 包含一個行程群組 ID (Process Group ID, PGID) 欄位。 * Process Group Leader (PGL) 的 PID 與 PGID 相同。PGL 同時也必須是TGL。 * Process 可以在不同的 Process Group 之間移動。 * **登入會話 (Login Session)**: * 包含了一個 Process 的所有後代 (descendants),該 Process 啟動了在特定終端上的工作會話,**通常是為使用者建立的第一個命令殼層 (Shell) Process**。 * 一個 Process 群組中的所有 Process 必須屬於同一個登入會話。 * 會話領導者 (Session Leader) **可以** 在會話中其他 Process 結束前退出,如 `./process &` 背景執行後,關閉終端。 <center> <img src="https://static.lwn.net/images/2014/cgroups-procs.png" alt="image" width="40%" /> </center> > (圖中藍色部分代表 struct pid) > 圖片來源:[Control groups, part 6:A look under the hood, 2014](https://lwn.net/Articles/606925/) <!-- ![Thread groups, process groups, sessions](https://notes.shichao.io/apue/figure_9.6.png) --> Linux 核心使用 `task_struct` 結構來描述任務 (task),此處的任務可以是 process 或 thread。 ### 相關結構 * **PID 類型**: `include/linux/pid_types.h` ```c=5 enum pid_type { PIDTYPE_PID, // 行程 ID PIDTYPE_TGID, // 執行緒群組 ID PIDTYPE_PGID, // 行程群組 ID PIDTYPE_SID, // 會話 ID PIDTYPE_MAX, // 以上共 4 種類型 }; ``` * **struct task_struct**:核心中描述一個任務 ( Process 或 Thread) 的主要資料結構。 `include/linux/sched.h` ```c=813 struct task_struct { struct task_struct *group_leader; // thread group leader ... pid_t pid; /* 全域 PID */ pid_t tgid; /* 全域 TGID */ ... struct pid *thread_pid; struct hlist_node pid_links[PIDTYPE_MAX]; // 舊版核心使用 struct list_head thread_node; // 串聯 thread group 內部的頭 ... struct signal_struct *signal; // 指向 signal ... } ``` ### 行程識別碼 (Process Identifier, PID) Linux 核心對 PID 的處理具有 **層次性** 和 **命名空間 (Namespace)** 的概念。 * **PID 命名空間 (PID Namespace)**: 為了支援容器化 (如 Docker),Linux 引入了 PID Namespace。每個 Namespace 擁有獨立的 PID 集合。PID Namespace 是層級式的,父 Namespace 可以看見子 Namespace 中的任務,反之則不行。一個任務在不同的 Namespace 中可以有不同的 PID。 * 新的 PID Namespace 透過呼叫 `clone()` 系統呼叫並帶上 `CLONE_NEWPID` 旗標來建立。 <center> <img src="https://hackmd.io/_uploads/SycI-zN7ee.png" alt="image" width="60%" /> </center> * **全域 ID (Global ID) vs. 區域 ID (Local ID)**: * **全域 ID**:在 **核心本身** 及 **初始全域 Namespace** 中有效的識別碼,保證在整個系統中唯一。全域 PID 和 TGID 直接儲存在 `task_struct` 的 `pid` 和 `tgid` 欄位中。 * **區域 ID**:屬於特定 Namespace,並非全域有效。 * **struct pid**:核心內部對 PID 的表示。它指向單獨的 task、process group 和 session。 `include/linux/pid.h` ```c=55 struct pid { refcount_t count; // 引用計數 unsigned int level; // 包含該 ns 的層級 = 該 proces 在多少個 ns 中可見 ... /* lists of tasks that use this pid */ struct hlist_head tasks[PIDTYPE_MAX]; // hash table,將使用此 pid 的任務連結起來 ... struct rcu_head rcu; struct upid numbers[]; // 每層級一個 upid 實例陣列 }; ``` * **PID 雜湊表 (PID Hash Table)**:用於快速根據 PID 數值查找 `struct pid`。 不同的 PID 可能會雜湊到同一個槽位,透過 linked-list 解決碰撞。 <center> <img src="https://hackmd.io/_uploads/HywbZM4Xge.png" alt="image" width="50%" /> </center> * **struct upid**:用於取得 `struct pid` 在特定 Namespace 中的 ID。 ```c=50 struct upid { // 用於獲取`struct pid`在特定 ns 中的 ID int nr; struct pid_namespace *ns; }; ``` * **find_pid_ns()**:根據 `nr` (Namespace 中的 ID) 和 `*ns` (Namespace 指標) 查找 `struct pid`。 ```c=120 extern struct pid *find_pid_ns(int nr, struct pid_namespace *ns); ``` * **IDR 機制 (ID Radix Tree)**:用於整數 ID 管理,可將整數 ID 映射到任意指標,適用於稀疏 ID 的場景。 > 延伸閱讀:[idr – integer ID management (2004)](https://lwn.net/Articles/103209/) 下圖展示一個 PID 命名空間的層級結構範例: ![PID namespace hierarchy example](https://hackmd.io/_uploads/Sk3r2Bmmxl.png) > 圖片來源:[How containers use PID namespaces to provide process isolation, Red Hat (2020)](https://www.youtube.com/watch?v=J17rXQ5XkDE) * **PID 最大值**:可以透過讀取 `/proc/sys/kernel/pid_max` 來查看系統允許的最大 PID 值。 * **Bitmap Pages**:用於表示 PID 空間。 > PID 的分配和釋放是無鎖 (lockless) 的。 * **signal_struct**:儲存與 signal 相關的資訊,包含 Process 擁有的 Thread 數量 `include/linux/sched/signal.h` ```c=94 struct signal_struct { int nr_threads; // Process 擁有的 Thread 數量 struct list_head thread_head; /* PID/PID hash table linkage. */ ... struct pid *pids[PIDTYPE_MAX]; // 根據 `PIDTYPE` 儲存不同類型的 PID ... }; ``` * **常用的涵式**: `include/linux/pid.h` ```c=212 static inline struct pid *task_pid(struct task_struct *task) { return task->thread_pid; } ``` `include/linux/sched/signal.h` ```c=685 static inline struct pid *task_tgid(struct task_struct *task) { return task->signal->pids[PIDTYPE_TGID]; } static inline struct pid *task_pgrp(struct task_struct *task) { return task->signal->pids[PIDTYPE_PGID]; } static inline struct pid *task_session(struct task_struct *task) { return task->signal->pids[PIDTYPE_SID]; } static inline int get_nr_threads(struct task_struct *task) { return task->signal->nr_threads; } ``` * **pidfd**:一個參照到 process 的 **檔案描述符 (file descriptor)**。 `include/uapi/linux/pidfd.h` * **引入目的**: * 避免 **PID 重用 (PID reuse)** 可能導致的安全問題:當一個 process 退出後,其 PID 可能會被一個完全不相關的新 Process 使用。 * 支援 **需要建立不可見輔助 Process** 的共享函式庫。 * 讓 process-management app 能將特定 process 的處理委派給非父行程。 > e.g. For waiting, signalling (examples are the Android low-memory killer daemon and systemd). * **參照對象**:process 的 TGL 的 `pid` 結構。 * **特性**:對於擁有者是穩定且私有的。 > 相關閱讀:[Adding the pidfd abstraction to the kernel (2019)](https://lwn.net/Articles/801319/) ### 指向目前 Process 的指標:current 巨集 核心經常透過指向 process descriptor (`task_struct`) 的指標來參照 Process。**為了快速存取**,核心使用了一個特殊的、架構相依的巨集 `current`。 * 在某些架構中,指向目前 Process 的指標,儲存在暫存器中。如果沒有足夠的暫存器,則使用 stack pointer (如`esp` in x86) 來定位。 > 核心模式下的 process,`esp`指向該特定 Process 的 kernel mode stack 的頂部。 * **實作**:通常是透過 `get_current()` 內聯函式完成,該函式讀取特定於 CPU 的 `current_task` 變數。 * **範例**:`arch/x86/include/asm/current.h` ```c=15 DECLARE_PER_CPU(struct task_struct *, current_task); static __always_inline struct task_struct *get_current(void) { return this_cpu_read_stable(current_task); } #define current get_current() ``` * `current` 和 `current_thread_info` 巨集常作為 `task_struct` 欄位的前綴出現在核心程式碼中。 --- ## 上下文交換 (Context Switch) 當 `schedule()` 函式被呼叫時 (可能由於處理 interrupt 或 syscall),可能需要進行 Context Switch,即改變目前 CPU 上執行的 Process。 ### 切換內容 1. **硬體上下文 (Hardware Context)**: Process 恢復執行前必須載入到 CPU 暫存器中的資料集合,如程式計數器 (Program Counter)、通用暫存器等。 > 在 Linux 中,一部分 Hardware Context 儲存在 process descriptor (`task_struct`) 中,其餘部分則保存在該 Process 的核心模式堆疊 (Kernel Mode Stack) 中。 2. **分頁全域目錄 (Page Global Directory, PGD)**:切換 PGD 以安裝新的位址空間。 3. **核心模式堆疊 (Kernel Mode Stack)** Linux 使用 **軟體** 來執行 Process 交換,且 Process 交換只發生在核心模式。使用者模式下的所有暫存器內容在執行 Process 交換前已經保存在 Kernel Mode Stack 中。 ### x86 架構的 Context Switch:從 TSS 到現代 Linux #### 任務狀態區段 (TSS) 的角色 * **硬體基礎**:早期 x86 架構設計了任務狀態區段 (Task State Segment, TSS),用於硬體層面儲存任務的上下文。每個 TSS 由 `tss_struct` 結構描述。 `arch/x86/include/asm/processor.h` ```c=400 struct tss_struct { /* * The fixed hardware portion... */ struct x86_hw_tss x86_tss; struct x86_io_bitmap io_bitmap; }; ``` * **Linux 的用法**: * 與早期為每個任務維護一個 TSS 不同,Linux 為系統中 **每個 CPU 維護一個 TSS** (`init_tss` 陣列)。 * 這個 TSS 主要用於指定當前 CPU 上核心模式操作的 **stack pointer (RSP0)** 和 **I/O 權限點陣圖** 等。 * 核心在每次行程切換時 **更新 TSS 的某些欄位**。TSS 反映了當前行程在 CPU 上的特權,但當行程未執行時,無需為其維護 TSS。取而代之,核心將被切換的 Hardware Context (如暫存器狀態) 儲存在該 process descriptor (`task_struct`) 內的`thread_struct`欄位中。 `include/linux/sched.h` ```c=813 struct task_struct { ... struct thread_struct thread; ... }; ``` `arch/x86/include/asm/processor.h` ```c=448 struct thread_struct { /* Cached TLS descriptors:*/ struct desc_struct tls_array[GDT_ENTRY_TLS_ENTRIES]; unsigned long sp; unsigned short es; unsigned short ds; unsigned short fsindex; unsigned short gsindex; ... /* IO permissions:*/ struct io_bitmap *io_bitmap; ... }; ``` #### switch_to:Context Switch 的核心 * **本質**:交換 **stack pointer (ESP/RSP)** 和 **instruction pointer (EIP/RIP)**,從而將 CPU 的控制權從一個 process 轉移到另一個 process。 ![image](https://www.maizure.org/projects/evolution_x86_context_switch_linux/context_switch_flow.png) * **呼叫**:通常由 `context_switch()` 函式呼叫 `switch_to(prev, next, last)`: * **`prev`**:指向 **將被暫停** process 的描述符。 * **`next`**:指向 **將要執行** process 的描述符。 * **`last`**:一個巧妙的參數。當 Process A 切換到 B,然後經過一系列切換 (B, C, ..., X),最終由 X 切換回 A。`last` 參數 **確保了恢復執行的 A 能準確知道是從 X 切換回來的**。 :::success * **原理**:在恢復執行的 Process A 中,`last` 變數所得到的值,其實是來自於那個「喚醒它」的 Process X 在呼叫 `switch_to` 時所傳入的 `prev` 參數。 > 在 x86 架構上,`switch_to` 巨集會將它的第一個輸入參數 (`prev`) 放在回傳值專用的暫存器 (`eax`) 中。因此,當 X 執行 `switch_to(prev=X, next=A)` 後,恢復執行的 A 就會從「回傳值」中得到 X 的指標。 * **目的**: * 讓 A 能夠 **安全地為 X 進行收尾工作** (如呼叫 `finish_task_switch(last)`,清理一個已經結束的 zombie process 所遺留的資源)。 * 解決了控制流直接「跳轉 (如 goto)」到函式中間,無法使用標準函式回傳值,使 zombie 無法自己釋放自己腳下的地毯的問題。 ::: ![image](https://hackmd.io/_uploads/Bk0Zxd4Qel.png) > 圖片來源:[UnderStanding The Linux Kernel](https://www.cs.utexas.edu/~rossbach/cs380p/papers/ulk3.pdf) > `switch_to`實際上做的就是 prev = switch_to(prev,next) #### Context Switch 的演化歷程 * **早期 (80x86,硬體交換)**: * 依賴 CPU 的 Hardware Context Switch 機制。透過 `ljmp` 指令跳轉到目標任務的 TSS 描述子,CPU 自動完成狀態保存與載入。 ![](https://www.maizure.org/projects/evolution_x86_context_switch_linux/early_linux_context_switch_flow.png) * **Linux 1.x (概念驗證)(1994)**: * `switch_to` 參數調整,引入中斷控制 (`cli`/`sti`) 增強原子性,但仍主要依賴硬體 TSS 交換。 ![](https://www.maizure.org/projects/evolution_x86_context_switch_linux/linux1_context_switch_flow.png) * **Linux 2.2 (轉向軟體交換 - 重大變革)(1999)**: * `switch_to` 巨集不再直接觸發硬體 TSS 交換,而是改為: 1. **軟體** 保存當前 process 的 ESP (stack pointer) 和 EIP (instruction pointer) 到其 `thread_struct`。 2. 更新 `thread_struct` 中新 process 的 ESP 和 EIP。 3. 呼叫 C 函式 `__switch_to()` 來完成剩餘工作: * FPU 狀態保存/恢復 * 更新 GDT (Global Descriptor Table) 中 TSS 的忙碌位元 * 載入任務暫存器 (TR) * 處理 segment 暫存器 (FS/GS) * 重載 LDT (Local Descriptor Table) (若需要) * 切換 Page Table (CR3) * 恢復 除錯暫存器 ![](https://www.maizure.org/projects/evolution_x86_context_switch_linux/linux22_context_switch_flow.png) * **Linux 2.4 (細化)(2001)**: * 與 2.2 版相似,但不再頻繁更新 TR,而是直接修改 CPU 當前活動 TSS 中的資料 (如 RSP0)。 * stack / instruction pointer 由 `prev->thread.esp` 和 `next->thread.esp` 管理。 * **Linux 2.6 (Linux 走向主流,x86_64 時代)(2003)**: * **i386**:變化不大。 * **x86_64**:`switch_to` 巨集使用輔助巨集 (`SAVE_CONTEXT`, `RESTORE_CONTEXT`) 保存/恢復 RSP,然後呼叫 `__switch_to` C 函式。 * **`__switch_to()` C 函式**: * 處理 FPU/SIMD 狀態 * 載入核心 stack pointer (更新 TSS 中的 SP0) * 更新 Page Table * 載入 TLS (Thread-Local Storage) 描述子到 GDT * 透過 MSR (Model-Specific Register) 設定 FS/GS 基底 * 處理 除錯暫存器 和 I/O bitmap ![](https://www.maizure.org/projects/evolution_x86_context_switch_linux/linux26_x8664_context_switch_flow.png) * **Linux 3.0 - 4.x (持續演進與結構化)**: * x86_64 的 `switch_to` 巨集引入堆疊保護 (`__switch_canary`)。 * **`switch_to()`** 結構更清晰: * **`prepare_switch_to()` 巨集**:確保核心 stack 就緒 (尤其對 VMAP_STACK)。 * **`__switch_to_asm` 組合語言函式**: * 執行 callee 保存暫存器的推入/彈出 * RSP 交換 * 處理堆疊保護和 Retpoline (針對 Spectre) 等底層操作 * 最後跳轉到 `__switch_to` C 函式。 * **`__switch_to` C 函式**:增加 **FPU 預載入** 優化、**虛擬化環境支援**。 ![](https://www.maizure.org/projects/evolution_x86_context_switch_linux/linux30_context_switch_flow.png) * **Linux 4.16 ~ 至今 (成熟與優化)**: * **核心機制穩定**:`__switch_to_asm` 組語函式 和 `__switch_to` C 函式,在後續版本的分工幾乎保持一致。 * **`switch_to` 巨集**: * 棄用 `prepare_switch_to()` 巨集。 * **`__switch_to_asm` (組合語言)**:持續精煉暫存器操作效率,深化旁路攻擊緩解措施。 `arch/x86/include/asm/switch_to.h` ```c=49 #define switch_to(prev, next, last) \ do { \ ((last) = __switch_to_asm((prev), (next))); \ } while (0) ``` `arch/x86/entry/entry_64.S` ```asm=177 SYM_FUNC_START(__switch_to_asm) ANNOTATE_NOENDBR /* * Save callee-saved registers * This must match the order in inactive_task_frame */ pushq %rbp ... jmp __switch_to SYM_FUNC_END(__switch_to_asm) ``` * **`__switch_to` (C 函式)**: * 精細的 FPU/SIMD 狀態管理 (如 AVX512, AMX)。 * 保存/載入 FS/GS 基底 (TLS)。 * 更新 current_task 和 TSS 中的 RSP0。 * 處理 __switch_to_xtra() (除錯、I/O bitmap)。 * 整合硬體新特性 (如 Intel RDT)、虛擬化優化、電源管理考量。 `arch/x86/kernel/process_64.c` ```c=610 __switch_to(struct task_struct *prev_p, struct task_struct *next_p) { struct thread_struct *prev = &prev_p->thread; struct thread_struct *next = &next_p->thread; ... return prev_p; } ``` * **整體趨勢**: * **安全性**:持續應對新硬體漏洞。 * **效能**:在多核環境下降低交換開銷。 * **可維護性**:程式碼結構清晰。 ![](https://www.maizure.org/projects/evolution_x86_context_switch_linux/linux41467_context_switch_flow.png) > 參考資料:[Evolution of the x86 context switch in Linux](https://www.maizure.org/projects/evolution_x86_context_switch_linux/) > 延伸閱讀:[不同架構下的 switch_to](https://hackmd.io/@Jaychao2099/linux-switch_to) --- ## 排程類別 (Scheduling Classes) 與 `task_struct` 中的相關欄位 Linux 核心使用排程類別來區分不同類型任務的排程策略。優先權高的類別會先被排程器考慮。 排程類別按優先權降序排列: 1. **`SCHED_STOP` (Stop Task)**: * **特性**:最高優先權,會停止其他所有任務。 * **用途**:SMP 的任務遷移 (Task Migration)、CPU 熱插拔 (CPU Hotplug)、`ftrace` 等。 3. **`SCHED_DEADLINE` (Deadline)**: * **特性**:基於 EDF (Earliest Deadline First) 演算法。 * **用途**:週期性的即時任務,如影音編解碼。 5. **`SCHED_FIFO` (First-In, First-Out) / `SCHED_RR` (Round-Robin)**: * **特性**:標準的 Real-Time 排程策略。 * **FIFO**:一直執行直到它阻塞、退出或被更高優先權的任務搶佔。 * **RR** :執行一個 Timeslice 後,如果仍可執行,會被放到其 priority queue 的末尾。 6. **`SCHED_NORMAL` (CFS) / `SCHED_BATCH` (Batch)**: * **NORMAL**:預設排程策略,由 **CFS (Completely Fair Scheduler)** 實現。 * **BATCH**:適用於 **CPU 密集型** 的批次處理任務,會降低其被搶佔的頻率,以利於快取使用和減少 context switch,但會犧牲互動性。 7. **`SCHED_IDLE` (Idle Task)**: * **特性**:最低優先權。 * **用途**:系統閒置時執行的任務,如讓 CPU 進入低功耗狀態。 ```mermaid flowchart LR Stop-Task --> Deadline Deadline --> Real-Time Real-Time --> Fair Fair --> Idle-Task ``` ### `task_struct` 中與排程相關的欄位 `include/linux/sched.h` ```c=615 struct sched_rt_entity { ... unsigned int time_slice; // 時間片段 for RR ... } ``` ```c=813 struct task_struct { ... int prio; // 動態優先權。對於普通 Process,基於 static_prio 和 bonus 計算 int static_prio; // 靜態優先權,根據 nice 值計算,用於計算 timeslice int normal_prio; // 一般優先權,記錄 prio 在 priority boost (如 取得 mutex 時被提昇) 前的值 unsigned int rt_priority; // Real-time Process 的優先權,不依賴 nice 值 struct sched_entity se; // CFS 排程實體 struct sched_rt_entity rt; // Real-time 排程實體 ... const struct sched_class *sched_class; // 指向所屬排程類別的實作函式 // (如 pick_next_task, enqueue_task 等) ... struct task_group *sched_task_group; // 指向任務所屬的 cgroup ... unsigned int policy; // Process 的排程模式 (SCHED_NORMAL, SCHED_FIFO ...) ... } ``` * `static_prio`:建立 child process 時,parent process 的靜態優先權會均分給父子。 * `prio`:(高) 100 ~ 139 (低)。Bonus = 0 ~ 10。$$\text{dynamic priority} = \max(100, \min(\text{static priority} - \text{bonus} + 5, 139))$$ * `rt_priority`:(高) 0 ~ 99 (低) <center> <img src="https://hackmd.io/_uploads/HyhvWBB7le.png" alt="image" width="30%" /> </center> ```c=2060 static inline int test_tsk_thread_flag(struct task_struct *tsk, int flag) { return test_ti_thread_flag(task_thread_info(tsk), flag); } ... static inline int test_tsk_need_resched(struct task_struct *tsk) { return unlikely(test_tsk_thread_flag(tsk,TIF_NEED_RESCHED)); } ... static __always_inline bool need_resched(void) { return unlikely(tif_need_resched()); } ``` * `TIF_NEED_RESCHED`: Process 旗標 (`thread_info->flags`) 中的一個位元,表示 **應盡快重新排程**。由 `need_resched()` 函式提供其值。 `linux/jiffies.h` ```c=85 extern u64 __cacheline_aligned_in_smp jiffies_64; extern unsigned long volatile __cacheline_aligned_in_smp __jiffy_arch_data jiffies; ``` * `jiffies`:系統時間計數器,表示 **開機以來發生的 clock interrupt 次數**。 ### 優先權與 Nice 值 * Linux 內部將 `nice` 值映射到一個 0-139 的優先權範圍: * 0-99:保留給即時 Process。數值越小,優先權越高。 * 100-139:給普通 Process。數值越小,優先權越高 (對應 `nice` 值 -20 到 +19)。 * `static_prio` 可以透過 `sys_nice()` 或 `sys_setpriority()` 系統呼叫改變。 * `task_prio()` 函式返回在 `/proc` 中看到的任務優先權值。 * `task_nice()` 函式根據給定的 `static_prio` 計算 nice 值。 * `NICE_TO_PRIO()` 巨集根據給定的 nice 值計算 `static_prio`。 `kernel/sched/syscalls.c` ```c=191 int task_prio(const struct task_struct *p) { return p->prio - MAX_RT_PRIO; } ``` `include/linux/sched.h` ```c=1898 extern int task_prio(const struct task_struct *p); ... static inline int task_nice(const struct task_struct *p) { return PRIO_TO_NICE((p)->static_prio); } ``` `include/linux/sched/prio.h` ```h=5 #define MAX_NICE 19 #define MIN_NICE -20 #define NICE_WIDTH (MAX_NICE - MIN_NICE + 1) // 40 #define MAX_RT_PRIO 100 // 即時優先權 0-99 #define MAX_DL_PRIO 0 #define MAX_PRIO (MAX_RT_PRIO + NICE_WIDTH) // 140 #define DEFAULT_PRIO (MAX_RT_PRIO + NICE_WIDTH / 2) // 120 (對應 nice 0) // 轉換 nice 值 (-20 ~ 19) 變成 內部優先權 (100 ~ 139) #define NICE_TO_PRIO(nice) ((nice) + DEFAULT_PRIO) #define PRIO_TO_NICE(prio) ((prio) - DEFAULT_PRIO) ``` ### 排程器主要函式 * **`schedule()`**:主要的排程程序,選擇下一個要執行的 Process 並將處理器交給它。 * **調用時機**:當核心希望 Process 睡眠或 Process 需要被 preempt 時。 * **作用**:如果新的 Process (`next`) 不同於先前執行的 Process (`prev`),則進行 context switch。 `kernel/sched/core.c` ```c=6645 static void __sched notrace __schedule(int sched_mode) { struct task_struct *prev, *next; ... is_switch = prev != next; if (likely(is_switch)) { rq->nr_switches++; RCU_INIT_POINTER(rq->curr, next); ++*switch_count; ... rq = context_switch(rq, prev, next, &rf); ... } ... } ``` ```c=6841 static __always_inline void __schedule_loop(int sched_mode) { do { preempt_disable(); __schedule(sched_mode); /* <----- */ sched_preempt_enable_no_resched(); } while (need_resched()); } asmlinkage __visible void __sched schedule(void) { struct task_struct *tsk = current; ... if (!task_is_running(tsk)) sched_submit_work(tsk); __schedule_loop(SM_NONE); /* <----- */ sched_update_worker(tsk); } EXPORT_SYMBOL(schedule); ``` * **`scheduler_tick()`**:每個時鐘中斷 (tick) 調用一次,用於重新計算優先權和時間片段,並觸發重新排程 (如果需要)。 > (已棄用,新版核心邏輯已整合調整) --- ## CPU 排程器的演化 Linux CPU 排程器經歷了多次重大演變,以適應不斷變化的硬體和應用需求。 ### 主要開發者 * **Ingo Molnár**:匈牙利籍,Red Hat 員工,Linux 核心排程器的主要開發者之一,$O(1)$ 排程器和 CFS 的作者。也曾開發過核心內 Web 伺服器 Tux。 * **Peter Zijlstra**:Intel 員工,排程器、鎖定原語和效能監控等多個核心子系統的共同維護者。 * **Con Kolivas (CK)**:澳大利亞麻醉科醫師,業餘核心開發者,專注於桌面環境的反應性和良好效能。開發了 RSDL/SD、BFS、MuQSS 等排程器。也曾領導團隊 3D 列印 COVID-19 檢測設備。 ### 發展時間線 * **1991 (Linux 初始版本)**:最早的 Linux 行程排程器。只有一個 Process 佇列,選擇新 Process 時遍歷整個佇列。 * **1999 (Linux 2.2)**:引入排程類別 (即時 vs. 非即時),支援 `SCHED_RR` 和 `SCHED_FIFO`。 * **2001 (Linux 2.4)**:實作了後來被歸類為 **$O(N)$ 排程器**。這個排程器在選擇下一個任務時,最壞情況下需要遍歷所有活動任務,因此其時間複雜度與任務數量成正比。 * **2003 (Linux 2.6.0 - 2.6.22)**:引入了 **$O(1)$ 排程器**。其設計目標是在常數時間內完成任務的選擇,與系統中活動任務的數量無關。它為每個 CPU 維護獨立的 RunQueue,並使用 bitmap 來快速定位最高優先權的非空佇列。 * **2007 (Linux 2.6.23)**:引入了 **CFS (Completely Fair Scheduler)**,取代了 $O(1)$ 排程器,成為預設排程器至今。CFS 的目標是為每個任務提供公平的 CPU 時間分配,其時間複雜度約為 $O(\log N)$ ,主要來自紅黑樹的操作。 * **2014 (Linux 3.14)**:引入了 **`SCHED_DEADLINE`**,基於 EDF (Earliest Deadline First) 和 CBS (Constant Bandwidth Server) 演算法,為即時任務提供更強的截止時間保證。 * **2019 (Linux 5.0)**:引入了 **EAS (Energy-Aware Scheduling)**,使排程器能夠感知 CPU 的功耗/效能特性,以優化能源消耗,特別適用於大小核 (big.LITTLE) 架構。 ### 衡量排程器的指標 * 應當 **最大化** 的指標: * **CPU 利用率 (CPU utilization)**:CPU 執行 **非閒置任務** 的時間比例。 * **吞吐量 (Throughput)**:單位時間內完成的 Process 數量。 * 應當 **最小化** 的指標: * **周轉時間 (Turnaround time)**: Process **從提交到完成** 的總時間。 * **等待時間 (Waiting time)**: Process **在 WaitingQueue 中等待執行** 的總時間。 * **排程器延遲 (Scheduler latency)**:從 **喚醒訊號發送到 Thread** 到排程器 **實際執行該 Thread** 的時間。包括分派延遲 (dispatch latency):處理 Context Switch、切換到 user mode 並跳轉到起始位置的時間。 ### $O(N)$ 排程器 (Linux 2.4 及更早版本) * **設計**:使用單一全域執行佇列 (RunQueue),所有 CPU 核心競爭同一個佇列中的任務。 * **問題**: * **可擴展性差**:在 SMP 系統中,全域佇列需要鎖 (lock) 保護,導致嚴重的競爭和效能瓶頸。 * **時間複雜度 $O(N)$**:挑選下一個任務或插入任務時,最壞情況下需要遍歷整個佇列,排程時間不確定,隨系統負載變化大。 ```graphviz graph Fig2_4 { layout=neato; /* 改用 neato 引擎 */ overlap=true; /* 避免自動調整節點重疊 */ size="3!"; rankdir=TB; splines=polyline; // 或 splines=ortho /* 節點外觀設定 */ node [shape=box, style=filled, fontname="Helvetica"]; /* RunQueue 方塊 */ RunQueue [pos="0,0!", z=1, shape=rectangle, width=0.5, height=3, label="Run\nQueue", fillcolor=white, style="solid"]; /* 四顆 CPU */ CPU1 [fillcolor="#4fd5eb", label="CPU 0", pos="-1.5,-2.5!", fontcolor=yellow]; CPU2 [fillcolor="#51a7f4", label="CPU 1", pos="-0.5,-2.5!", fontcolor=yellow]; CPU3 [fillcolor="#4f5def", label="CPU 2", pos="0.5,-2.5!", fontcolor=yellow]; CPU4 [fillcolor="#250368", label="CPU 3", pos="1.5,-2.5!", fontcolor=yellow]; /* 在 RunQueue 上方要加的小方塊 */ Top1 [pos="-0.4,1.2!", z=3, shape=rectangle, width=0.5, height=0.2, label="", fillcolor="#4fd5eb"]; Top2 [pos="-0.4,0.9!", z=3, shape=rectangle, width=0.5, height=0.2, label="", fillcolor="#250368"]; Top3 [pos="0.4,-0.9!", z=3, shape=rectangle, width=0.5, height=0.2, label="", fillcolor="#4f5def"]; Top4 [pos="-0.4,-0.6!", z=3, shape=rectangle, width=0.5, height=0.2, label="", fillcolor="#4fd5eb"]; Top5 [pos="-0.4,-1.2!", z=3, shape=rectangle, width=0.5, height=0.2, label="", fillcolor="#250368"]; Top6 [pos="0.4,0.9!", z=3, shape=rectangle, width=0.5, height=0.2, label="", fillcolor="#51a7f4"]; /* 從 RunQueue 的南側同一個點,連出到各 CPU 的北側 */ RunQueue:s -- CPU1:n [style=dashed]; RunQueue:s -- CPU2:n [style=dashed]; RunQueue:s -- CPU3:n [style=dashed]; RunQueue:s -- CPU4:n [style=dashed]; } ``` ### $O(1)$ 排程器 (Linux 2.6.0 - 2.6.22) * **目標**:解決 $O(N)$ 排程器的可擴展性和不確定性問題,使得關鍵排程操作 (如挑選任務、插入任務) 的時間複雜度達到常數級別 $O(1)$。 * **設計**: * **Per-CPU Runqueue**:每個 CPU 核心擁有各自的 RunQueue,大幅減少 SMP 的鎖競爭。每個 RunQueue 包含 **兩個優先權陣列 (Priority Array)** * **優先權陣列 (Priority Array)**:包含 140 個佇列頭的陣列,每個佇列對應一個優先權等級。同一優先權的行程以 FIFO 方式組織。 * `active` 陣列:包含 **尚未用完 Timeslice** 的 Process。 * `expired` 陣列:包含 **已用完 Timeslice** 的 Process。 <center> <img src="https://i.imgur.com/EeB2VmP.png" alt="image" width="80%" /> </center> * **Bitmap**:使用一個 bitmap 來標記哪些 Priority Queue 中有待執行的行程。 * **任務選擇**:透過在 bitmap 中查找第一個被設定的位元 (Find First Set, FFS) 來定位最高優先權的非空佇列,時間複雜度為 $O(1)$ (依賴硬體指令)。 ![](https://i.imgur.com/nM1OlJA.png) * **佇列切換**:當 `active` 陣列中的 **所有任務都耗盡 Timeslice 後**,`active` 和 `expired` 陣列指標互換,時間複雜度為 $O(1)$。 ```graphviz digraph RunQueueDiagram { rankdir=BT; size="6!"; splines=polyline; node [shape=record, style=filled, fillcolor=white, fontname="Helvetica"]; edge [arrowhead=none]; // CPU nodes CPU0 [label="CPU 0", shape=rectangle, fillcolor="#4fd5eb", fontcolor=yellow]; CPU1 [label="CPU 1", shape=rectangle, fillcolor="#51a7f4", fontcolor=yellow]; CPU2 [label="CPU 2", shape=rectangle, fillcolor="#4f5def", fontcolor=yellow]; CPU3 [label="CPU 3", shape=rectangle, fillcolor="#250368", fontcolor=yellow]; // RunQueue nodes with two Priority Arrays each RQ0 [shape=plaintext, margin=0, label=< <TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0"> <TR><TD BGCOLOR="#84e878">0</TD></TR> <TR><TD BGCOLOR="#84e878">1</TD></TR> <TR><TD BGCOLOR="#84e878"><BR/><BR/><BR/></TD></TR> <TR><TD BGCOLOR="#84e878">99</TD></TR> <TR><TD BGCOLOR="#e95646">100</TD></TR> <TR><TD BGCOLOR="#e95646"><BR/><BR/></TD></TR> <TR><TD BGCOLOR="#e95646">139</TD></TR> </TABLE> >]; RQ1 [shape=plaintext, margin=0, label=< <TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0"> <TR><TD BGCOLOR="#b2e892">0</TD></TR> <TR><TD BGCOLOR="#b2e892">1</TD></TR> <TR><TD BGCOLOR="#b2e892"><BR/><BR/><BR/></TD></TR> <TR><TD BGCOLOR="#b2e892">99</TD></TR> <TR><TD BGCOLOR="#fca699">100</TD></TR> <TR><TD BGCOLOR="#fca699"><BR/><BR/></TD></TR> <TR><TD BGCOLOR="#fca699">139</TD></TR> </TABLE> >]; RQ2 [shape=plaintext, margin=0, label=< <TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0"> <TR><TD BGCOLOR="#84e878">0</TD></TR> <TR><TD BGCOLOR="#84e878">1</TD></TR> <TR><TD BGCOLOR="#84e878"><BR/><BR/><BR/></TD></TR> <TR><TD BGCOLOR="#84e878">99</TD></TR> <TR><TD BGCOLOR="#e95646">100</TD></TR> <TR><TD BGCOLOR="#e95646"><BR/><BR/></TD></TR> <TR><TD BGCOLOR="#e95646">139</TD></TR> </TABLE> >]; RQ3 [shape=plaintext, margin=0, label=< <TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0"> <TR><TD BGCOLOR="#b2e892">0</TD></TR> <TR><TD BGCOLOR="#b2e892">1</TD></TR> <TR><TD BGCOLOR="#b2e892"><BR/><BR/><BR/></TD></TR> <TR><TD BGCOLOR="#b2e892">99</TD></TR> <TR><TD BGCOLOR="#fca699">100</TD></TR> <TR><TD BGCOLOR="#fca699"><BR/><BR/></TD></TR> <TR><TD BGCOLOR="#fca699">139</TD></TR> </TABLE> >]; RQ4 [shape=plaintext, margin=0, label=< <TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0"> <TR><TD BGCOLOR="#84e878">0</TD></TR> <TR><TD BGCOLOR="#84e878">1</TD></TR> <TR><TD BGCOLOR="#84e878"><BR/><BR/><BR/></TD></TR> <TR><TD BGCOLOR="#84e878">99</TD></TR> <TR><TD BGCOLOR="#e95646">100</TD></TR> <TR><TD BGCOLOR="#e95646"><BR/><BR/></TD></TR> <TR><TD BGCOLOR="#e95646">139</TD></TR> </TABLE> >]; RQ5 [shape=plaintext, margin=0, label=< <TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0"> <TR><TD BGCOLOR="#b2e892">0</TD></TR> <TR><TD BGCOLOR="#b2e892">1</TD></TR> <TR><TD BGCOLOR="#b2e892"><BR/><BR/><BR/></TD></TR> <TR><TD BGCOLOR="#b2e892">99</TD></TR> <TR><TD BGCOLOR="#fca699">100</TD></TR> <TR><TD BGCOLOR="#fca699"><BR/><BR/></TD></TR> <TR><TD BGCOLOR="#fca699">139</TD></TR> </TABLE> >]; RQ6 [shape=plaintext, margin=0, label=< <TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0"> <TR><TD BGCOLOR="#84e878">0</TD></TR> <TR><TD BGCOLOR="#84e878">1</TD></TR> <TR><TD BGCOLOR="#84e878"><BR/><BR/><BR/></TD></TR> <TR><TD BGCOLOR="#84e878">99</TD></TR> <TR><TD BGCOLOR="#e95646">100</TD></TR> <TR><TD BGCOLOR="#e95646"><BR/><BR/></TD></TR> <TR><TD BGCOLOR="#e95646">139</TD></TR> </TABLE> >]; RQ7 [shape=plaintext, margin=0, label=< <TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0"> <TR><TD BGCOLOR="#b2e892">0</TD></TR> <TR><TD BGCOLOR="#b2e892">1</TD></TR> <TR><TD BGCOLOR="#b2e892"><BR/><BR/><BR/></TD></TR> <TR><TD BGCOLOR="#b2e892">99</TD></TR> <TR><TD BGCOLOR="#fca699">100</TD></TR> <TR><TD BGCOLOR="#fca699"><BR/><BR/></TD></TR> <TR><TD BGCOLOR="#fca699">139</TD></TR> </TABLE> >]; // RunQueues at the top { rank = max; RQ0; RQ1; RQ2; RQ3; RQ4; RQ5; RQ6; RQ7; } // CPUs at the bottom { rank = min; CPU0; CPU1; CPU2; CPU3; } // Connections: each CPU connects to its two RunQueues CPU0:n -> RQ0:s; CPU0:n -> RQ1:s; CPU1:n -> RQ2:s; CPU1:n -> RQ3:s; CPU2:n -> RQ4:s; CPU2:n -> RQ5:s; CPU3:n -> RQ6:s; CPU3:n -> RQ7:s; } ``` #### $O(1)$ 排程器的特性與問題 * **優點**: * **常數時間操作**:排程操作 (任務選擇、佇列切換) 時間複雜度為 $O(1)$。 * **SMP 可擴展性**:Per-CPU Runqueue 提高了多處理器環境下的效能。 * **CPU 親和性 (Affinity)**:傾向於將行程保留在同一個 CPU 上執行,以利用快取。 * **互動式行程支援**:透過啟發式演算法 (heuristics) 和獎勵 (bonus) 機制提升互動式行程的反應速度。 * **公平性**:宣稱 Process 不會餓死 (starved)。 * **負載平衡 (Load Balancing)**:當某些 CPU 過於繁忙或空閒時,會在 CPU 之間遷移任務。 * **時間片段分配問題 (Priorities and Timeslices)**: * 根據 static_prio 分配的 Timeslice 長度差異巨大,且非線性。(分配不均) * 優先權 100 的任務比優先權 101 的任務多獲得 2.5% 的 CPU 時間。 * 優先權 119 的任務比優先權 120 的任務多獲得 4 倍 CPU 時間。 * 優先權 138 的任務比優先權 139 的任務多獲得 2 倍 CPU 時間。 ![image](https://hackmd.io/_uploads/Hyuc3TB7ee.png) > 相同優先權差異,CPU 時間比例可能差異很大。 * **啟發式規則 (Heuristics) 與獎勵 (Bonus)**: * **Heuristics**:為了改善互動性,引入了複雜規則來判斷 Process 是 I/O-bound 還是 CPU-bound。 * **Bonus**:**睡眠較多** 的 Process (等待 I/O) 被視為互動式,會獲得優先權獎勵 (bonus),範圍為 -5 到 +5。 * **矛盾現象 (Paradox)**: * **互動式 Process**:需要較短時間但頻繁執行,可能因為獎勵而獲得較長的 Timeslice。 * **批次處理 Process**:能有效利用 cpu,反而可能獲得較短的 Timeslice。 * **"會哭的小孩有糖吃"**:過多的 Heuristics 規則導致系統行為難以預測,前景任務容易霸佔 CPU,而背景任務 (如[音樂播放](https://marc.info/?l=linux-kernel&m=104759311126102&w=2)) 可能因得不到足夠 CPU 時間而卡頓。 * **獎勵過度問題**:如果某些 Process 獲得過大的互動性獎勵,它們可能會主導處理器,導致其他 Process 飢餓。 > S. Takeuchi, 2007 的測試顯示,在極端情況下少數 Process 佔用了 98% 的處理器時間。 * $O(1)$ 排程器雖然核心操作是 $O(1)$,但大量的 Heuristics 計算和 Bonus 調整使得整體行為複雜、難以預測和調整、且不夠公平,有時甚至產生反效果。 * **對桌面應用不友好**:過於注重伺服器端的吞吐量和可擴展性,有時犧牲了桌面應用的反應速度和流暢性。Con Kolivas 等開發者對此提出了批評。 <center> <img src="https://hackmd.io/_uploads/rk9oBCHQel.png" alt="image" width="50%" /> </center> > 圖片來源:[xkcd:Supported Features](https://xkcd.com/619/) > 諷刺開發者專注於某些技術指標 (如支援超多核心),而忽略了使用者實際體驗 (如 Flash 影片播放流暢度)。$O(1)$ 排程器在追求 $O(1)$ 複雜度和可擴展性的同時,犧牲了部分桌面應用的公平性和體驗。 ### RSDL (Rotating Staircase Deadline) 排程器 由 Con Kolivas 在 2007 年針對 $O(1)$ 排程器的問題提出的改進方案。 * **設計思想**:基於 **多級回饋佇列 (Multi-level Feedback Queue)** 的概念: 1. 為每個優先權等級分配一個預設 Timeslice。Process 在其 static priority queue 中執行。 2. 用完 Timeslice 後會被放到下一級 (更低) 優先權的佇列中 ("走下樓梯")。 3. **minor rotation (微小輪轉)**:每個優先權級別有執行時間限制。時限到時,該級別所有 Process (無論剩餘 Timeslice 多少) 都會被降級。 4. **Major rotation (主輪轉)**:當一個優先權時代 (era) 的所有行程都執行完畢或用盡 Timeslice,進入 `expired` 陣列,等待 Major rotation。 <center> <img src="https://static.lwn.net/images/ns/kernel/RSDL1.png" alt="image" width="50%" /> </center> * **特點**: * **公平性**:避免了複雜的 heuristics,對待行程更為公平。 * **簡單性**:演算法相對簡單,易於理解和調整。 * **桌面應用優化**:旨在提升桌面應用的反應速度和流暢性。 * **結果**:Linus Torvalds 曾對 RSDL 表示讚賞,並考慮將其合併。但最終 Ingo Molnár 快速開發並提出了 CFS,吸取了 RSDL 的一些思想,但採用了不同的實現方式。RSDL 未被合併到主線核心。 ### CFS (Completely Fair Scheduler) 由 Ingo Molnár 在 2007 年提出,取代了 $O(1)$ 排程器,成為 Linux 至今的預設排程器。 * **核心目標**:實現處理器共享 (Processor Sharing),模擬一個「理想、精確的多工 CPU」。在理想情況下,如果系統中有 N 個任務,每個任務應獲得 1/N 的 CPU 時間。 * **虛擬執行時間 (Virtual Runtime, vruntime)**: * 每個 Process 維護一個 `vruntime` 值。(不直接分配 Timeslice) * 任務的 **實際執行時間** 會根據其優先權 (nice 值) 進行加權,**轉換為 `vruntime`**: * 高優先權任務的 `vruntime` 增長較慢。 * 低優先權任務的 `vruntime` 增長較快。 * CFS 總是 **選擇 `vruntime` 最小的任務** 來執行。 * 當一個任務執行完畢或被搶佔時,其 `vruntime` 會更新。 * **資料結構**:使用 [紅黑樹 (Red-Black Tree)](https://hackmd.io/@sysprog/linux-rbtree) 來儲存可執行任務,樹中的節點按 `vruntime` 排序。 * **時間複雜度**:整體時間複雜度被認為是 $O(\log N)$。 * 選取 `vruntime` 最小的任務 (樹的最左節點) = $O(1)$。 * 插入 / 刪除任務 = $O(\log N)$。 ![image](https://hackmd.io/_uploads/H1jc-q8Xex.png) * **公平性實現**: * 透過 `vruntime` 和權重 (weight) 機制,確保了不同優先權的 Process 能按比例獲得 CPU 時間。 * 權重與 nice 值相關,nice 值越低 (優先權越高),權重越大 (增長越慢),保證了 CPU 時間的幾何級數分配。$$\text{prio_to_weight[nice]} \approx \text{prio_to_weight[nice+1]} \times 1.25$$ * 這種機制確保了所有行程的 `vruntime` 趨於平衡,從而實現了 CPU 時間的公平分配。 <center> <img src="https://hackmd.io/_uploads/H13Ay2UXll.png" alt="image" width="80%" /> </center> * **排程實體 (Scheduler Entities, `struct sched_entity`)**: * CFS 排程的對象是 **排程實體**,而非直接的 `task_struct`。一個 sched_entity 可以代表一個任務,也可以代表一組任務,這種階層式的實體與資源結構稱為 **cgroups**。 `/include/linux/sched.h` ```c=569 struct sched_entity { /* For load-balancing: */ struct load_weight load; struct rb_node run_node; ... u64 vruntime; ... struct sched_entity *parent; /* rq on which this entity is (to be) queued: */ struct cfs_rq *cfs_rq; /* rq "owned" by this entity/group: */ struct cfs_rq *my_q; ... } ``` ```c=813 struct task_struct { ... struct sched_entity se; // 用來向 CFS 註冊排程 struct task_group *sched_task_group; // 指向所屬的 task_group ... } ``` * 透過定義 **`task_group` 結構**,描述由多個任務組成的群組: * `task_group` 中也包含 `sched_entity` 實體,因此其也可以做為一個 CFS 所管理的對象,被加入到一個 runqueue 之中。 * 並且,`task_group` 中對於自己群組中的任務也存在自己的 runqueue 中。換句話說,系統中的 runqueue 其實是階層式樹狀結構的。 `kernel/sched/sched.h` ```c=431 struct task_group { ... /* schedulable entities of this group on each CPU */ struct sched_entity **se; /* runqueue "owned" by this group on each CPU */ struct cfs_rq **cfs_rq; ... } ``` <center> <img src="https://i.imgur.com/gOgkZ2I.png" alt="image" width="90%" /> </center> * **目標延遲 (Target Latency) 和最小粒度 (Minimum Granularity)**: CFS 的執行時間取決於 Process 的 `vruntime` 何時不再是最小的。不過,為了避免過於頻繁的 context switch,CFS 仍然有一個最小執行粒度 (minimum granularity) 的概念。 * **`sched_latency_ns`**:一個排程週期的 **目標延遲**,預設 20ms。表示 **所有執行中任務應在此時間內至少執行一次**。 * **`sched_min_granularity_ns`**:任務執行的 **最小 Timeslice**,預設 4ms。 * **時間片段 (`ideal_slice`) 動態計算**:$$\color{red}{\text{epoch}} = \max\{(\text{sched_latency_ns}),\ (\text{sched_min_granularity_ns} \times \text{nr_running})\}$$$$\text{ideal_slice} = \color{red}{\text{epoch}} \times \frac{\text{weight}}{\text{sum_of_weights}}$$ * **排程夥伴 (Scheduler Buddies)**:CFS 中用於調度的一些輔助指標,如: * `next` (最近喚醒的任務) * `last` (最近被換出的任務) * `skip` (呼叫 `sched_yield()` 的任務)。 * **優點**: * **高度公平**:相對於 $O(1)$ 排程器,CFS 在 CPU 時間分配上更為公平。 * **簡潔的設計**:核心概念 (`vruntime` 和紅黑樹) 相對簡單,避免了大量 Heuristics 和 Bouns。 * **良好的可調整性**:可以透過調整 nice 值和一些 sysctl 參數來影響排程行為。 * **與 $O(1)$ 的比較**:CFS 放棄了 $O(1)$ 排程器對嚴格常數時間操作的追求,但通過更公平的機制和合理的資料結構選擇,達到了更好的整體效果,尤其是在互動性和桌面體驗方面。 * **後續討論**: * Con Kolivas 指責 CFS 借鑒了他的 `plugsched` (一個允許輕鬆更換排程器的模組化框架) 思想,儘管之前這一想法曾被批評。 * Linus Torvalds 則認為伺服器和桌面的排程需求差異不大,好的演算法應該能透過自動調整 (auto-tune) 來適應。 > 延伸閱讀:[Linux 核心設計: Scheduler(2): 概述 CFS Scheduler](https://hackmd.io/@sysprog/B18MhT00t) ### BFS (Brain F*ck Scheduler) 由 Con Kolivas 在 2009 年重出江湖 (CFS 出現後),認為 CFS 仍然過於複雜且對桌面應用不夠友好,於是再次提出新的排程器。 * **設計哲學**:極度簡潔,專為 **桌面** 和 **低延遲場景** 設計,不追求極致的伺服器吞吐量或可擴展性。 * **核心特點**: * **單一執行佇列 (Single Runqueue)**:回歸到類似 $O(N)$ 的設計。有鎖競爭現象,但 CK 認為對於核心數不多的桌面系統,這不是問題。 * **$O(N)$ 查找**:任務選擇是 $O(N)$ 的,因為它遍歷一個簡單的列表。CK 認為對於核心數不多的桌面系統,這不是問題。 * **最早有效虛擬截止時間優先 (Earliest Effective Virtual Deadline First, EEVDF)**:這是 BFS 的核心演算法思想,用於計算虛擬截止時間:$$\text{deadline} = \text{jiffies} + (\text{prio_ratio} * \text{rr_interval})$$ * `rr_interval`:時間片段,預設 6ms。 * `prio_ratio`:類似 CFS 中的權重,幾何依賴於優先權。 * 排程器選擇具有最早 deadline (已過期或最接近) 的任務執行。 * **無複雜 Heuristics**:避免了 CFS 和 $O(1)$ 中的複雜權重計算和動態調整。 ![image](https://hackmd.io/_uploads/BJ44M68Qxx.png) * **爭議與效能比較 (CFS vs. BFS)**: * [Ingo Molnár 的測試 (2009)](http://marc.info/?l=linux-kernel&m=125227082723350&w=2):在多核心伺服器環境下,BFS 在核心編譯、管道延遲、訊息傳遞、OLTP 等方面均不如 CFS。 * [Con Kolivas 的回應](https://lwn.net/Articles/351504/):Ingo 的測試場景不符合普通桌面PC的使用情況。BFS 的目標是提升互動性和反應速度。 * [Groves, Knockel, Schulte 的測試 (2009)](http://cs.unm.edu/~eschulte/classes/cs587/data/bfs-v-cfs_groves-knockel-schulte.pdf): * BFS 在最小化**延遲**方面優於 CFS (適用於互動式任務)。 * CFS 在最小化**周轉時間**方面優於 BFS (適用於批次處理任務)。 * [Graysky 的測試 (2012)](http://repo-ck.com/bench/cpu_schedulers_compared.pdf):在少量核心 (1-4核) 的 CPU 上,CK 的補丁 (基於 BFS) 在壓縮、編譯、影片編碼等桌面常見任務上,通常比原版 CFS 有小幅 (1-8%) 的效能提升。但在更多核心 (如16核) 的情況下,BFS 的優勢減小或消失。 * **結果**:BFS 未被合併到主線核心。 ### SCHED_DEADLINE 在 Linux 3.14 中加入,是一個針對 **即時系統** 的排程策略。 * **目標**:提供比傳統 `SCHED_FIFO` 和 `SCHED_RR` 更強的即時保證,特別是對於有明確截止時間 (deadline) 要求的週期性任務。 * **核心演算法**: * **最早截止時間優先 (Earliest Deadline First, EDF)**: * **使用**:SCHED_DEADLINE 的主要演算法,屬於 Dynamic Priority 排程。 * **作法**:總是執行 **絕對截止時間 (absolute deadline) 最早** 的任務。 * **特性**:其理論上的 CPU 可排程利用率上限為 100%。$$U = \sum_{i=1}^{n}\frac{\text{runtime}_i}{\text{period}_i} \leq 1$$ * **速率單調排程 (Rate Monotonic Scheduling, RMS)**: * **使用**:另一種常見的即時排程演算法,但屬於 Static Priority。 * **作法**:**週期越短,優先權越高**。 * **特性**:其可排程的利用率上限較低,在某些情況下,即使總利用率未滿,也可能排程失敗 $(\text{Dhall's Effect, }U > 1)$。 * **持續頻寬伺服器 (Constant Bandwidth Server, CBS)**: * **作法**:為每個任務預留一定的 CPU 預算 (即 runtime): * 當任務執行時,預算會被消耗。 * 如果預算耗盡,任務會被節流 (throttled) 直到下一個週期開始。 * **temporal isolation (時間隔離)**:防止 Process 超出分配的預算,進而影響其他任務的截止時間。 * **每個任務定義三個參數**:任務保證在每個 `period` 內獲得 `runtime` 的執行時間,並且此 `runtime` 必須在從週期開始計算的 `deadline` 內完成。$\text{runtime} \leq \text{deadline} \leq \text{period}$。 * `runtime` (執行時間) * `deadline` (相對截止時間) * `period` (週期) ![deadline範例](https://hackmd.io/_uploads/ByTbLaLmel.png) * **系統呼叫**:透過 `sched_setattr()` 和 `sched_getattr()` 這兩個系統呼叫來設定和取得任務的排程屬性。 `include/linux/syscalls.h` ```c=914 sys_sched_setattr(pid_t pid, struct sched_attr *attr, unsigned int flags); sys_sched_getattr(pid_t pid, struct sched_attr *attr, unsigned int size, unsigned int flags); ``` `include/uapi/linux/sched/types.h` ```c=98 struct sched_attr { __u32 size; /* Size of this structure */ __u32 sched_policy; /* 排程方法: SCHED_DEADLINE */ __u64 sched_flags; /* SCHED_NORMAL, SCHED_BATCH */ __s32 sched_nice; /* SCHED_FIFO, SCHED_RR */ __u32 sched_priority; /* SCHED_DEADLINE */ __u64 sched_runtime; /* 重點 */ __u64 sched_deadline; /* 重點 */ __u64 sched_period; /* 重點 */ /* Utilization hints */ __u32 sched_util_min; __u32 sched_util_max; }; ``` * **優勢**: * 能更好地處理複雜的即時任務集,提供可預測的排程行為。 * 支援跨 **CPU 的負載平衡 (push/pull)** 和 **截止時間繼承 (deadline inheritance)**。 * **應用場景**:需要嚴格截止時間保證的即時系統,如影片播放、工業控制、高頻交易等。 * **挑戰與解決方案**: * **Dhall's Effect**:在多核心處理器上,使用全域 EDF 排程時,即使總體 CPU 利用率未滿,也可能無法滿足所有即時任務的截止時間。這通常發生在 **少量執行時間很長的任務** 與 **大量執行時間很短的任務** 混合時。(太多短任務先做了,剩餘時間不夠給長任務) * **解決方案一**:透過 **CPU cgroup** 將任務 **綁定到特定的 CPU 核心上** (Partitioned EDF)。 * **解決方案二**:讓任務在不同核心間遷移 (Global EDF) 來應對。 * **解決方案三**:永遠不對 $U>1$ 的 deadline 作即時保證。 * **WCET 計算困難**:準確計算任務的「最壞情況執行時間 (Worst-Case Execution Time)」非常困難。若估算不足,任務可能在完成前就被節流。 * **解決方案**:提出了 **GRUB (Greedy Reclaim of Unused Bandwidth)** 機制,允許任務在需要時使用系統中閒置的 CPU 頻寬,增加彈性。 * **非對稱 CPU 架構**:如 ARM big.LITTLE,負載平衡和頻寬分配仍需完善。 * **限制**:使用 `SCHED_DEADLINE` 的任務 **不能 fork** 子行程。 > SCHED_DEADLINE 的運作基礎是一個「資源預留合約」,即系統保證在每個 period 內給予該任務 runtime 的 CPU 時間。如果這個任務 fork 了一個子行程,排程器會面臨一個難題:這個預留的 CPU 資源 (頻寬) 應該如何分配給父子兩個行程?無論哪種方式,都會讓原本清晰的排程模型變得複雜且難以預測,從而破壞其提供的「即時保證」。這也意味著 **SCHED_DEADLINE 更適合用於功能單一、固定的執行緒,而不是會動態產生子行程的複雜應用**。 * **優先權**:高於 `SCHED_FIFO/RR` 及 `SCHED_NORMAL` (CFS)。 > 參考資料:[Using SCHED_DEADLINE](https://elinux.org/images/f/fe/Using_SCHED_DEADLINE.pdf) > 延伸閱讀: > * [Deadline scheduling](https://lwn.net/Articles/743740/) > * [SCHED_DEADLINE: a status update - ARM](https://events.static.linuxfound.org/sites/events/files/slides/SCHED_DEADLINE-20160404.pdf) ### MuQSS (Multiple Queue Skiplist Scheduler) Con Kolivas 在 BFS 之後又再出江湖 (約 2016 年),旨在解決 BFS 在多核擴展性上的一些問題。發音為 "mux"。 * **核心改進**: * **多執行佇列 (Multiple Runqueues)**:每個 CPU 一個 RunQueue,類似 $O(1)$ 和 CFS,以改善 BFS 在超過 16 核時的鎖競爭問題,提升可擴展性。 * **[跳躍串列 (Skip List)](https://en.wikipedia.org/wiki/Skip_list)**:用 Skip List 取代 CFS 的紅黑樹或 BFS 的簡單列表來組織任務。Skip List 是一種基於 **機率** 的資料結構,平均查找、插入、刪除的時間複雜度為 $O(\log N)$,但最壞情況為 $O(N)$。實作比紅黑樹更簡單。 ![](https://upload.wikimedia.org/wikipedia/commons/2/2c/Skip_list_add_element-en.gif) * **虛擬截止時間 (Virtual Deadline)**:概念類似 BFS 的 EEVDF 思想,但使用奈秒級解析度的單調計數器 (`niffies`) 而非 `jiffies`。 * **非阻塞 `trylock`**:從 RunQueue 彈出任務時,嘗試使用非阻塞鎖。如果失敗,則轉向其他佇列上第二近 deadline 的任務。 * **結果**:CK 在 2021 年左右因投入 COVID-19 設備開發等原因停止了 MuQSS 的維護。 --- ## CPU 排程器近期發展 ### CPU 閒置迴圈 (CPU Idle Loop) (Linux 4.17) 針對 CPU 空閒時的電源管理。 * **目標**:在 CPU 閒置時,更智能地決定 CPU 進入 **合適的低功耗狀態 (idle-state)**,同時在需要時能快速喚醒,以平衡節能和喚醒延遲。 * **調控器 (Governor)**:排程器呼叫 Governor 來預測 CPU 應進入哪種深度的 idle state。 * menu governor:預設的,基於歷史閒置時間和預期延遲。 * ladder governor:另一種策略。 * teo (timer events oriented) governor:考慮計時器事件。 * **停擺時鐘 (Tickless Idle / `NO_HZ`)**:在 CPU 深度閒置時,可以停止週期性的時鐘中斷 (timer tick),以進一步節能。喚醒由非週期性事件觸發。 * **議題**:預測 CPU 將空閒多長時間是關鍵,錯誤的預測可能導致頻繁進出 idle state,反而增加開銷。 <center> <img src="https://static.lwn.net/images/2018/kr-rw-graph.jpg" alt="image" width="70%" /> </center> > CPU Idle Loop 重構前後功耗對比。綠線為舊迴圈,紅線為新迴圈,顯示新迴圈 **功耗更低且更可預測**。 > 參考資料: > * [CPU Idle Loop Rework](https://www.youtube.com/watch?v=QSOo5N97T0A) (Rafael J. Wysocki, Intel, 2018) > * [What's a CPU to do when it has nothing to do?](https://lwn.net/Articles/767630/) ### 熱壓力 (Thermal Pressure) 當 CPU 過熱時,核心的散熱調節器 (thermal governor) 會限制 CPU 的最大頻率以冷卻。傳統排程器可能未意識到 CPU 容量的變化,導致分配過多工作,效能下降。 * **目標**:讓排程器感知散熱事件,從而更合理地分配任務。 * **解決方案**:增加一個介面,讓排程器了解熱事件,從而更佳地分配任務。有兩種方法協同合作: * **Thermal Pressure Approach**:引進 Thermal Pressure API,允許排程器收到熱事件的通知。更適用於非對稱 CPU 配置。 * **EAS (energy-aware scheduling)** > 參考資料:[Telling the scheduler about thermal pressure](https://lwn.net/Articles/788380/) (Marta Rybczyńska, 2019) ### EAS (Energy Aware Scheduling) 在 Linux 5.0 中正式加入。 * **目標**:使排程器完全感知 CPU 的功耗/效能特性,智能地分配任務,以平衡效能和功耗。特別適用於 ARM big.LITTLE 這類異質多核 (Heterogeneous Multi-Processing, HMP) 架構。 * **核心機制**: * **能源模型 (Energy Model)**:數據描述不同 CPU 核心在不同頻率下的功耗和效能特性。 * **任務放置**:排程器根據 **任務的特性** (如計算密集度、延遲敏感度) 和 **能源模型**,將任務放置到最合適的 CPU 核心上。(例如,輕量級任務可能放在小核,重量級任務放在大核) * **顯著影響**:在保持或輕微影響效能 (約 3-5%) 的情況下,可降低 8-11% 的能耗。廠商 (如[聯發科的 CorePilot](https://www.mediatek.tw/features/corepilot-evolution)) 基於 EAS 進行客製化可獲得更大提升。 * **挑戰**: * 準確預測任務負載和行為。 * 在大小核之間遷移任務的成本。 * 與 CPU Frequency Scaling (DVFS) 和 CPU Idle 機制的協同工作。 * **相關技術**: * **DVFS (Dynamic Voltage and Frequency Scaling)**:動態電壓和頻率調整。功耗與電壓平方和頻率成正比 ($P \approx CV^2f$)。 * **CPU Frequency Governor (`cpufreq_schedutil`)**:利用排程器提供的 CPU 利用率資訊 (如 PELT - Per-Entity Load Tracking) 來調整 CPU 頻率。PELT 使用 EWMA (指數移動平均) 來追蹤最近的負載。 * **PSI (Pressure Stall Information)**:核心中的一個機制,用於量化 CPU、記憶體、I/O 等資源的壓力,幫助排程器和調控器做出更明智的決策。 > 延伸閱讀:[Linux 核心設計: Scheduler(8): Energy Aware Scheduling](https://hackmd.io/@sysprog/Skelo1WY6) ### 非對稱 CPU 架構排程 (Arm big.LITTLE) [big.LITTLE 架構](https://en.wikipedia.org/wiki/ARM_big.LITTLE),將高效能但功耗較高的大核 (big cores) 與低功耗但效能較低的小核 (LITTLE cores) 集成在同一晶片上。 * **智能分配任務**:高強度任務到大核,低強度或背景任務到小核。錯誤的分配會導致效能不佳或功耗過高。 * **任務遷移 (migration) 成本**:從小核到大核的切換存在延遲。需要預測任務類型和負載 (可能用到機器學習技術)。 * **ISA 兼容性問題**:部分 ARM 設計中,某些 CPU 核可能只支援 64 位元任務,而另一些則同時支援 32 位元和 64 位元任務。排程器必須考慮到這一點,避免將 32 位元任務分配到僅支援 64 位元的核上。 ![](https://upload.wikimedia.org/wikipedia/commons/8/87/In_Kernel_Switcher.jpg) > 延伸閱讀: > * [Scheduling for asymmetric Arm systems](https://lwn.net/Articles/838339/) > * [成大wiki - ARMv8](https://wiki.csie.ncku.edu.tw/embedded/ARMv8) ### 核心排程 (Core Scheduling) 針對 SMT (Simultaneous Multithreading,如 Intel Hyper-Threading) 帶來的安全問題。在 Linux 5.14 納入主線。 * **SMT 的問題**: * SMT 允許一個物理核心同時執行多個硬體 Thread,它們共享某些核心資源 (如執行單元、cache)。這可能導致旁路攻擊,一個惡意 thread 可能竊取同一核心上其他 thread 的敏感資訊 (如 L1TF, TLBleed 等漏洞)。 * OpenBSD 等注重安全的系統曾建議完全禁用 SMT,但這會導致顯著的效能損失 (約 8-30%)。![image](https://hackmd.io/_uploads/By33dosNWg.png) * **Core Scheduling 的目標**:在啟用 SMT 的同時,減輕這些安全風險,同時將效能影響降至最低。 * **核心機制**: * **信任邊界 (Trust Boundaries)**:透過 `core_cookie` 來定義。具有相同 `core_cookie` 的行程被認為是相互信任的,可以同時在同一個物理核心的 SMT thread 上執行。 * **不信任的行程 (具有不同 `core_cookie`)** 則不會被同時排程到同一個物理核心的 SMT thread 上。 * **效能影響**:啟用 Core Scheduling 會帶來一定的效能開銷 (約 4%),但遠小於完全禁用 SMT 的開銷。 > 參考資料:[Core scheduling](https://lwn.net/Articles/780703/) (Jonathan Corbet, February 2019)。 --- ## 總結與展望 Linux CPU 排程器從最初簡單的任務挑選機制,演化至今已成為一個極其複雜且精密的系統,需要平衡公平性、反應時間、吞吐量、功耗、即時性以及安全性等多方面需求。其發展歷程反映了硬體技術的進步 (從單核到多核、NUMA、SMT、異質多核) 以及應用場景的多樣化 (從個人電腦到伺服器、資料中心、嵌入式設備、即時系統)。 * **演化趨勢**:從簡單的 $O(N)$ 到追求確定性的 $O(1)$,再到強調公平和簡潔設計哲學的 CFS,以及針對特定需求的 `SCHED_DEADLINE` 和 EAS。 * **持續的挑戰**: * **啟發式規則 vs. 通用性**:過多的 Heuristics 規則可能導致系統行為難以預測且只適用於特定場景 ($O(1)$ 的教訓)。 * **效能 vs. 功耗 vs. 安全**:三者之間往往存在此消彼長的關係,需要在不同層面進行權衡。 * **桌面 vs. 伺服器**:不同應用場景對排程器的側重點不同,單一排程器如何適應所有場景是一個持續的議題。Con Kolivas 的工作在這方面提供了很多有價值的探索。 * **未來方向**: * **異質計算環境**:更好地利用 CPU、GPU、NPU 等不同處理單元。 * **細粒度的資源控制**:為容器、雲端運算、虛擬化環境、微服務等場景提供更精確的資源隔離和 QoS 保證。 * 持續應對新的硬體安全漏洞帶來的挑戰。 * **機器學習輔助**:更智能地預測任務行為和系統狀態,以優化排程決策。 Linux 核心排程器的演化是一個充滿挑戰和創新的過程,也是整個資訊科技發展的縮影;這個故事遠未結束,隨著技術推陳出新,排程器仍將持續進化。 要深入掌握其核心設計與演化脈絡,開發者除了需精通排程算法與資料結構,還必須了解處理器架構、記憶體管理、中斷處理以及各種系統級的權衡;並利用 `perf`、`ftrace` 等工具進行實際的效能分析與行為追蹤,才能在「看到漏洞並修復它」(seeing a hole and fixing it) 的精神指引下,參與並推動排程器的改進。 > 延伸閱讀: > * [Linux 核心設計: Scheduler(1): O(1) Scheduler](https://hackmd.io/@sysprog/S1opp7-mP) > * [Linux 核心設計: Scheduler(2): 概述 CFS Scheduler](https://hackmd.io/@sysprog/B18MhT00t) > * [Linux 核心設計: Scheduler(3): 深入剖析 CFS Scheduler](https://hackmd.io/@sysprog/BJ9m_qs-5) > * [Linux 核心設計: Scheduler(4): PELT](https://hackmd.io/@sysprog/Bk4y_5o-9) > * [Linux 核心設計: Scheduler(5): EEVDF 排程器/1](https://hackmd.io/@sysprog/SyG4t5u1a) > * [Linux 核心設計: Scheduler(5): EEVDF 排程器/2](https://hackmd.io/@sysprog/HkyEtNkjA) > * [Linux 核心設計: Scheduler(6): 排程器測試工具](https://hackmd.io/@sysprog/H1Eh3clIp) > * [Linux 核心設計: Scheduler(7): sched_ext](https://hackmd.io/@sysprog/r1uSVAWwp) > * [Linux 核心設計: Scheduler(8): Energy Aware Scheduling](https://hackmd.io/@sysprog/Skelo1WY6) --- 回[主目錄](https://hackmd.io/@Jaychao2099/Linux-kernel)