執行人: otteryc
專題解說影片
研究 sched_ext / scx,撰寫簡易的使用者層級 CPU 排程器並予以驗證。
儘量列出第一手材料,包含文件和演講
注意用語:
sched_ext 是基於 BPF 的使用者層級 CPU 排程器,其目的在於簡化排程器的開發及實驗。在過去的 15 年內,排程的重要性發生了戲劇性的改變,現今的處理器不但有異質多核計算的情境,還有 CCX 、 NUMA 等等的架構問題在設計排程器時需要被考慮。
Linux v6.10-rc5 已發布,重新統計程式碼行數
兩份 release candidate 之間,沒有排程器相關的修改,參見 Diffstat
但是,截止 2024/6 (v6.10-rc3) 2024/6 (v6.10-rc5),在 kernel/sched
資料夾 目錄中有 28505 行的程式碼,直接從 Linux 核心的 CFS ( v6.6) 或 EEVDF ( v6.6) 排程器著手進行研究,其所需要的時間成本顯然過高,造成晚近排程器研究門檻上升。此外,即使直接更改 CFS 或 EEVDF 排程器的程式碼,每次更改都需 編譯核心 安裝核心 重啟電腦 的流程始能生效。因為 Linux 屬於 Monolithic kernel ,雖然有 modules 的機制,但是仍然無法媲美 microkernel 中 service 的能力 ,僅能作為核心的延伸功能(Demysifying The Linux Kernel Scheduler,第 13 頁)。
澄清關於 microkernel 的 server 描述,特別是排程器的設計。
Microkernel 旨在保障核心的安全性以及可靠性,排程器也是 Hurd 及 Mach 等 microkernel 實作中核心提供的基礎功能;此外, Solaris 作為 Monolithic Kernel , Scheduling Classes 也可以作為可載入的核心模組,前面關於 microkernel 中 service 的敘述並不準確。
此外, sched_ext 利用 eBPF 強大的連結時期靜態檢查機制,避免排程器的實作使得核心發生錯誤,這個專案更進一步的實作了核心內保護機制,防止被錯誤實作的排程器無限期的排斥單一行程的執行。得益於 eBPF 的 API , sched_ext 在進行排程決策的程式碼可以執行在使用者層級,這使得執行及終止排程器的方法可以被顯著的簡化,僅需將 sched_ext 行程結束即可。
預計在 Linux v6.11 之後, sched_ext 將被收錄到開發主幹,參見 Re: [PATCHSET v6] sched: Implement BPF extensible scheduler class。
需要能在 sched_ext 分支運作,針對 Linux v6.9+
首先,由於 sched_ext 專案已獲 Linus Torvalds 接受, GitHub 上的 sched-ext/sched_ext 已於 2024/6/19 起不再維護,我們需要從 kernel.org 獲得最新的 sched_ext 在 kernel 內的 infrastructure
接著,我們需要進行編譯設定,在這份專題研究中,考量到 sched_ext
專案具有核心內保護機制的特性,我們直接在 Ubuntu 24.04 機器上進行,而不透過虛擬環境進行:
在進行設定時,請參考 Linux 核心設計: Scheduler(7): sched_ext 中的敘述,打開 BPF subsystem, General Setup Extensible Scheduling Class, Kernel hacking Compile-time checks and compiler options 的選項,在完成設定後,可以用以下的命令檢查:
設定完之後,我們就可以開始編譯、安裝 Linux 核心並重新開機,就有了可以執行 sched_ext 排程器的環境
重新開機後,我們可以試著執行 scx_rustland
專案:
預期看到以下輸出:
接下來,我們以最簡單的 FIFO 排程策略為例,以 Top-Down 方式扼要說明如何藉助 sched_ext 專案,以 Rust 語言撰寫的使用者層級 CPU 排程器:
在排程器初始化階段, scx_rlfifo
會先建立一個 BpfScheduler
在初始化階段完成之後,除非收到 SIGINT 訊號或 BPF 排程器結束, 使用者層級 CPU 排程器將停留在無窮迴圈中。並在排程決定完成之後,讓出 (yield) CPU 資源,等待 BPF Scheduler 喚醒,進行下一個 Scheduling cycle
而作為最簡單的排程策略並得益於 scx 專案提供的高度抽象介面, scx_rlfifo 僅僅使用了約 150 行程式碼就完成了一個能正常運作的使用者層級 CPU 排程器,其排程實作如下:
簡而言之,每當使用者層級 CPU 排程器被喚醒,會從 queued
(BPF_MAP_TYPE_RINGBUF
, 資料型態為 queued_task_ctx
) 中讀取需要被排程的工作,並將之放入 dispatched
(BPF_MAP_TYPE_USER_RINGBUF, 資料型態為 dispatched_task_ctx
)之中,供核心讀取。
本專題將以一個簡單的 Two-level Queue Scheduling 為例,說明如何在 sched_ext/scx 的助力下,設計一套簡易的排程器
設計概念:
統一術語,使用 timeslice
由於 QueuedTask
結構體中,並不包含 Linux 核心定義的 task_struct
,沒有辦法直接得到單一行程的優先程度。
不過,作為使用者層級 CPU 排程器,基於 scx 專案所開發的排程器有強大的 crate.io 套件庫支援,可以藉由 pid 從 procfs 獲得 NICE 值。
使用的方法也十分容易:
根據恐龍書中的描述:
The performance of the RR algorithm depends heavily on the size of the time quantum. At one extreme, if the time quantum is extremely large, the RR policy is the same as the FCFS policy.
這樣會讓實作變得簡單:我們只需要在 FIFO () 的情況的 time slice 調整的足夠大,並且在 RR 時允許搶佔即可。
將下方內容整合到對應的環境設定指引中。
在進行 Rust 語言的開發時,如果沒有顯式的在專案中註明使用 2021 edition 的 Rust 語言,會預設使用 2015 edition,造成 use crate 語法的差異,務必留意。
在專題截止前夕,趕進度的臺灣時間凌晨 1:00 , libbpf 的 GitHub repo fetch 了來自 kernel tree 的 commit,其中包含新增 bpf_map_skeleton
的結構體成員 link
。
這會導致 scx 專案編譯失敗,在經過一夜的惡補 meson 的編譯流程以及除錯之後,在早上終於發現問題,並提交 Pull Request(#396),並於是日下午獲得接受。
而後,發現套用 be998aa 雖然能夠成功編譯,卻在其他開發者測試時會造成 Segmentation Fault , PR#396 後於當天晚上由 PR#400 revert,並更改 bpftool 版本。
本課程的「核心」都指作業系統核心,避免誤用該詞彙到其他場景。下方可說:「其關鍵問題在於」
其核心 原因在於, bpftool 的 GitHub repo 曾經使用 force-push ,造成 meson script 所指向的 commit ID 消失,進而自動使用了最新版本的 bpftool ,以及 meson script 所指定的 libbpf 版本,最終發生了兩者版本上的不一致。把 libbpf commit ID 同步更新到最新之後,雖然能夠成功編譯,但是 scx 專案所寫入 ring buffer 的資料型態仍然是舊的 bpf_map_skeleton
,致使 segmentation fault 的發生。
如果在 git clone 完最新的 repo 並且 checkout 到正確的 tag 之後,仍然出現以下錯誤,
請先別急著懷疑是不是真的是核心版本過於老舊,很有可能是根本沒打開 sched_ext 的核心編譯選項。