contributed by < Kuanch >
Linux Kenrel
Buildroot
QEMU
GDB
本專題專注於排程器研究,包含相關材料閱讀以及排程器測試分析工具:
下載原始碼並編譯:
建議將 debug 設定打開,此外我因在使用 linux gdb debug 指令 lx-
等遭遇問題,我亦重新將 gcc g++ gdb 等都升級版本:
在編譯階段,由於我們同時需要設定開啟 debug option 供 GDB 使用,也要提供 kernel module 介面,供載入 VM 時使用,故編譯選項的設定較預設複雜。
首先根據 How to add a Linux kernel driver module as a Buildroot package? 設定 .config
,並根據 Linux Kernel Debugging 手動編輯 .config
加入 BR2_ENABLE_DEBUG
,至此我們完成 buildroot configuration。
或使用 make menuconfig
介面編輯。
由於 Buildroot 會一併將 Linux 核心以及相關 toolchain 都編譯出來,我們亦需要提供 linux kernel compiling configuration;輸入 make linux-menuconfig
後,由於我們使用了 BR2_ENABLE_DEBUG
,其會確認一連串 debug 相關設定:
注意此處,若選項未在 .config
未被設定,則會被要求選擇;我原先忽略了設定 BR2_OPTIMIZE_2
而非 BR2_OPTIMIZE_G
,導致使用 GDB 時變數會被最佳化;一樣可透過編輯 .config
修改。
之後出現 Linux Kernel configuration menu,更改
前四個必定要打開供後續 GDB debugging 之用,除了使用 menuconfig,知道 symbol 的話也可以直接編輯 board/qemu/x86_64/linux.config
。
接著將編譯 Buildroot 的各項工具 make -j8
,編譯完成後執行:
即可到下一步開啟 remote gdb 連接 VM。
注意,當鍵入 apropos lx-
並未顯示任何訊息表示 GDB scripts 未被成功設置,首先確認 Linux Kernel configuration 中的第三點已經被打開;並檢查 ~/.gdbinit
以及 /root/.gdbinit
有寫入腳本位置,參考
由於 qemu-system-x86_64 ... -s
已經將 gdbserver:1234 打開,故我們可以進入 gdb 後接入 server,並設定中斷點
或使用以下指令連接 gdbserver
我們可以使用 GDB script 搭配 gdb 中斷點監測核心的運行狀況,如
由於 Buildroot 早已不支援編譯器運行在其環境中,我們必須自行加入 patch 以在其中使用 gcc 編譯;參考 Buildroot and compiler on target。
上文有些內容沒有清楚說明,以下記錄我的作法。
package/gcc/Config.in
package/gcc/gcc-target/gcc-target.mk
package/Config.in
ln -s package/gcc/gcc.hash gcc-target/gcc-target.hash
Makefile
.config
你也可直接應用其提供的 patchset。
動態追蹤 (Dynamic Tracing) 相對於靜態追蹤,它致力於在系統運行時讀取系統的各項狀態,就像是一個隨時變化內容的資料庫;由於有許多系統是不允許隨時下線維護或者其靜態分析問題是困難或無意義的,動態追蹤的技術對於系統分析故十分重要。
eBPF 的前身 BPF (cBPF) 用於分析網路封包,並運行於 Linux 核心之中,避免頻繁地在 Kernel mode 與 user model 切換,BPF 顯然是一種動態追蹤技術,但其仍不時需要將資料從核心層級複製到使用者層級;而 eBPF 在其基礎之上已不滿足單純的分析系統狀態,其本質上是運行於 Linux 核心中的虛擬機器,透過 BPF Compiler Collection (BCC),使用 LLVM toolschain 編譯 C code 為 eBPF bytecode 運行於 Linux 核心中。
(?)上圖是 eBPF 在不同 Linux 核心版本所支援可追蹤的事件,注意到在 Linux 4.7 之後,就整合了追蹤 scheduler 事件的功能;由於我們的目標是要令 Linux 核心運行我們所寫的 CPU scheduler,eBPF 無疑成為最好的選擇之一。
一個主要的疑問是,究竟是 Kernel 直接執行 eBPF bytecode,也就是植入,還是存在中間層,Kernel 實際僅是提供環境運行 eBPF 程式?根據 scx README.md:
sched_ext is a Linux kernel feature which enables implementing kernel thread schedulers in BPF and dynamically loading them.
以及
Above, we switch the whole system to use scx_simple by running the binary, suspend it with ctrl-z to confirm that it's loaded, and then switch back to the kernel default scheduler by terminating the process with ctrl-c.
這暗示了使用 eBPF 技術的 sched_ext 是使 Kernel 直接運行 customized scheduler 的。
我們可以使用 Linux Kernel 提供的 GDB scripts 更方便的知道核心的各項資訊,譬如 lx-
注意,若鍵入 apropos lx-
後並沒有顯示可用的 lx-
等 GDB scripts,請檢查是否開啟 Linux Kernel configuration menu 的第三項,並按 gdb 提示編輯 ~/.gdbinit
及 /root/.gdbinit
,其他相關疑難雜症請參考
Debugging kernel and modules via gdb
Makefile : no rule to make target '/constants.py.in' needed by '/constants.py'. Stop
Using +gdb+ in Buildroot
CPU scheduler 的調用十分複雜且難以調適以符合第三方應用,直接修改核心程式碼難度非常高,且第三方的自定義擴充幾乎不可能被以注重泛用性、可攜帶性的 Linux 核心採用,使得 CPU scheduler 難以發展;於是 sched_ext 應運而生,透過 eBPF 我們能夠較便利的「植入」自定義排程器,並對排程器進行分析、調適、實驗。
隨後我們會詳細分析 sched_ext 實際如何運作客製化排程器。
安裝流程可以參考 Linux 核心設計: 開發與測試環境
確定安裝 clang
lld
lldb
ld.lld
且連結 bin file
upgrade clang and pahole
apt-get install dwarves
安裝在編譯 tools/sched_ext
可能會遭遇以下問題,因其安裝版本小於 1.25
:升級 dwarves
版本解決問題:
注意此時需要重新編譯 sched_ext 之 Linux 核心。
在準備編譯 dwarves cmake 時遭遇 missing: LIBDWARF_LIBRARIES LIBDWARF_INCLUDE_DIRS
sudo apt-get install libdw-dev
參考使用 pip3 安裝 virtme-ng 時遭遇 error: Error: setup script specifies an absolute path
BUILD_VIRTME_NG_INIT=1 pip3 install --verbose -r requirements.txt .
遭遇問題setup.py
解決該問題scx_simple.c
worksscx_simple.c
是 sched_ext 提供的簡單範例,其 main()
實作如下:
事實上,我們完全可以不理解 BPF 機制,僅需實作幾項關鍵函式,如 ops.select_cpu()
ops.enqueue()
ops.dispatch()
等,參考 Scheduling Cycle,scx_simple.bpf.c
提供以下範例
我們使用以下命令列模擬 Linux 核心
並連結 gdbserver
start_kernel()
的 GDB 初探start_kernel
是 Linux 核心的進入點,在這裡會執行許多初始化操作,參考從 start_kernel 到第一個任務: Linux Scheduler 閱讀筆記 (2)。
除此之外,當我們在start_kernel
設置中斷點導致中斷,此時鍵入 lx-ps
已經有 PID 0 ,並事實上 start_kernel
是由其執行,並創造第一個 user process 為 PID 1,用於初始化各項子系統。
而 PID 0 在初始化系統後,將成為 CPUIdle 模組的一部分,用於確保排程器總是有任務可排程,可用於減少 CPU 陷入睡眠的時間、減少功耗;亦可參考 Linux 核心設計: CPUIdle(1): 子系統架構。
PID=1 通常是 init 或 systemd,即第一個 user-space process,所有的 Linux 行程均衍生 (fork) 於此,但我們不免好奇: 「PID = 0 在哪?」
要解釋這議題,需要追溯到 UNIX 的歷史,首先 Solaris 是 Sun Microsystems (現為 Oracle) 發展的作業系統,血統純正,繼承絕大多數來自 AT&T UNIX 的設計,Linux 充其量只能說是「泛 UNIX 家族」,並未直接繼承 UNIX,而是取鏡 UNIX 的經典設計。因此在面對「PID = 0 為何者?」的問題時,我們應當區隔 UNIX/Solaris 和 Linux。
在早期 UNIX 設計中,PID = 0 為 swapper process,一如字面上的意思,swapper 會置換整個 process 的內容,包含核心模式的資料結構,到儲存空間 (通常為磁帶或硬碟),也會反過來將儲存空間的內容還原為 process,這在 PDP-7 和 PDP-11 主機上,是必要的機制,其 MMU 不具備今日完整的虛擬記憶體能力。
這種置換形式的記憶體管理,讓 UNIX 效率不彰,於是在 UNIX System V R2V5 和 4.BSD (1980 年代) 就將 swapper process 更換為 demand-paged 的虛擬記憶體管理機制,後者是 BSD 開發團隊向 Mach 微核心學習並改造。 PID = 0 是系統第一個 process,而 PID = 1 則是第一個 user process,由於 UNIX/BSD 朝向 demand paging 演化,swapper process 就失去原本的作用,UNIX System V 則乾脆將 "swapper" 更名為 "sched",表示實際是 scheduler() 函式。
發展於 1990 年代的 Linux 核心就沒有上述「包袱」,但基於歷史因素,Linux 也有 PID =0 的 process,稱為 idle process,對應 cpu_idle() 函式,本身只是無窮迴圈,無其他作用,其存在只是確保排程器總是有任務可排程。
我們在GNU/Linux 執行 ps -f 1 命令,可發現以下輸出:
UID PID PPID CMD root 1 0 /sbin/init
顯然 PID =1 的 init 或 systemd 行程,其 parent PID 為 0,即 idle process,也維持「從零開始」的優良傳統。
在 AT&T, BSDi, 加州大學柏克萊分校的三方官司訴訟後,BSD 家族 (FreeBSD, NetBSD, OpenBSD, DragonflyBSD 等等) 浴火重生,趁著系統改寫 (以符合授權規範) 之際,改變實際 PID = 0 的動作,可能是系統初始化或者如 Linux 一般的 idle。
此外,我們也可得知,這個當下被中斷的正是 PID 0 行程,正在執行 /init
的路上:
我們可以發現,目前 kernel stack 被使用了 3f20
約是 16KB,似乎比想像中大一些,根據 Kernel stacks on x86-64 bit,每一個 thread (task_struct) 約占用 8KB:
x86_64 page size (PAGE_SIZE) is 4K.
Like all other architectures, x86_64 has a kernel stack for every active thread. These thread stacks are THREAD_SIZE (2*PAGE_SIZE) big.
而事實上該行程僅佔 6,784 bytes:
我們透過以下方法看看目前 function call 使用 stack 的狀況:
以及
透過觀察以上資料,我們可以如下解讀:
0xffffffff82000000
- 0xffffffff82003f20
是 start_kernel()
所使用的 stack framex86_64_start_reservations
使用了 0xffffffff82003f20
+ 16 bytesx86_64_start_kernel
使用了 0xffffffff82003f30
+ 16 bytessecondary_startup_64
使用了 0xffffffff82003f50
+ 16 bytes在這之後,我們讓 GDB 繼續初始化流程,直到登入 Linux。
登入 Linux 後,在 GDB 介面按下 ctrl + c,會顯示我們目前正身在 default_idle()
中:
此時鍵入 where
,其 function call hierachy 如下:
我們也看看 default_idle()
是什麼:
故我們了解到,目前可能並沒有任何任務需要進行,詳細的 CPUIdle 機制我們暫且略過不提。
接下來我們想知道當我們啟動一個程式,他是如何被排入行程且被執行;由於 buildroot 不支援 compiler on target,故我們需要自行編輯相關 toolchain 以在 buildroot 內部使用 GCC,詳情參考我的 Buildroot compiler on target。
首先我們轉寫一隻簡單的 C 程式,為了方便,我們執行一個簡單的無限迴圈,並控制接收到 SIGNINT
時的行為,當按下 ctrl + c,我們應當會在 /var/log/syslog
(或是 /var/log/messages
) 見到相應訊息:
執行 ./infiteloop
後,在 gdb 中斷並輸入 lx-ps
可以發現其正在運作
由於此時並沒有任務可以執行,當我們在 kernel/sched/core.c
中的 schedule()
設中斷點,隨即可以攔截到該程式
我們接下來將中斷點設定到我們熟悉的 pick_next_task_fair
如同前一節我們追中 User Program 在核心中的行為,此次我們關注 Kernel Module 是如何在核心中被排程以及運行的,和 User Program 的差別為和?
相同地,我們撰寫一隻簡單的 Kernel Module 如下,注意此處仍需要使用先前提到過的 simrupt buildroot external toolchain 將本 linux module 載入 buildroot:
我們透過 cat /dev/cpureport
來取得類似於當前行程的簡單資訊,並觀察其調用如下