Jaychao2099
  • NEW!
    NEW!  Connect Ideas Across Notes
    Save time and share insights. With Paragraph Citation, you can quote others’ work with source info built in. If someone cites your note, you’ll see a card showing where it’s used—bringing notes closer together.
    Got it
      • Create new note
      • Create a note from template
        • Sharing URL Link copied
        • /edit
        • View mode
          • Edit mode
          • View mode
          • Book mode
          • Slide mode
          Edit mode View mode Book mode Slide mode
        • Customize slides
        • Note Permission
        • Read
          • Only me
          • Signed-in users
          • Everyone
          Only me Signed-in users Everyone
        • Write
          • Only me
          • Signed-in users
          • Everyone
          Only me Signed-in users Everyone
        • Engagement control Commenting, Suggest edit, Emoji Reply
      • Invite by email
        Invitee

        This note has no invitees

      • Publish Note

        Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note No publishing access yet

        Your note will be visible on your profile and discoverable by anyone.
        Your note is now live.
        This note is visible on your profile and discoverable online.
        Everyone on the web can find and read all notes of this public team.

        Your account was recently created. Publishing will be available soon, allowing you to share notes on your public page and in search results.

        Your team account was recently created. Publishing will be available soon, allowing you to share notes on your public page and in search results.

        Explore these features while you wait
        Complete general settings
        Bookmark and like published notes
        Write a few more notes
        Complete general settings
        Write a few more notes
        See published notes
        Unpublish note
        Please check the box to agree to the Community Guidelines.
        View profile
      • Commenting
        Permission
        Disabled Forbidden Owners Signed-in users Everyone
      • Enable
      • Permission
        • Forbidden
        • Owners
        • Signed-in users
        • Everyone
      • Suggest edit
        Permission
        Disabled Forbidden Owners Signed-in users Everyone
      • Enable
      • Permission
        • Forbidden
        • Owners
        • Signed-in users
      • Emoji Reply
      • Enable
      • Versions and GitHub Sync
      • Note settings
      • Note Insights New
      • Engagement control
      • Make a copy
      • Transfer ownership
      • Delete this note
      • Save as template
      • Insert from template
      • Import from
        • Dropbox
        • Google Drive
        • Gist
        • Clipboard
      • Export to
        • Dropbox
        • Google Drive
        • Gist
      • Download
        • Markdown
        • HTML
        • Raw HTML
    Menu Note settings Note Insights Versions and GitHub Sync Sharing URL Create Help
    Create Create new note Create a note from template
    Menu
    Options
    Engagement control Make a copy Transfer ownership Delete this note
    Import from
    Dropbox Google Drive Gist Clipboard
    Export to
    Dropbox Google Drive Gist
    Download
    Markdown HTML Raw HTML
    Back
    Sharing URL Link copied
    /edit
    View mode
    • Edit mode
    • View mode
    • Book mode
    • Slide mode
    Edit mode View mode Book mode Slide mode
    Customize slides
    Note Permission
    Read
    Only me
    • Only me
    • Signed-in users
    • Everyone
    Only me Signed-in users Everyone
    Write
    Only me
    • Only me
    • Signed-in users
    • Everyone
    Only me Signed-in users Everyone
    Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note No publishing access yet

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.

    Your account was recently created. Publishing will be available soon, allowing you to share notes on your public page and in search results.

    Your team account was recently created. Publishing will be available soon, allowing you to share notes on your public page and in search results.

    Explore these features while you wait
    Complete general settings
    Bookmark and like published notes
    Write a few more notes
    Complete general settings
    Write a few more notes
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       Owned this note    Owned this note      
    Published Linked with GitHub
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    # 第七講:不只挑選任務的排程器 > 本筆記僅為個人紀錄,相關教材之 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)

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password
    or
    Sign in via Google Sign in via Facebook Sign in via X(Twitter) Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    By signing in, you agree to our terms of service.

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully