# 2024q1 Homework6 (integration) contributed by < [`Wufangni`](https://github.com/Wufangni) > ## 開發環境 ``` shell $ gcc --version gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0 Copyright (C) 2021 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. $ lscpu Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Address sizes: 39 bits physical, 48 bits virtual Byte Order: Little Endian CPU(s): 16 On-line CPU(s) list: 0-15 Vendor ID: GenuineIntel Model name: 12th Gen Intel(R) Core(TM) i5-12500H CPU family: 6 Model: 154 Thread(s) per core: 2 Core(s) per socket: 12 Socket(s): 1 Stepping: 3 CPU max MHz: 4500.0000 CPU min MHz: 400.0000 BogoMIPS: 6220.80 ``` ## 自我檢查清單 - [x] 研讀前述 Linux 效能分析 描述,在自己的實體電腦運作 GNU/Linux,做好必要的設定和準備工作 > Linux 核心 4.4 版以後 Ubuntu Linux 預設使用 `EFI_SECURE_BOOT_SIG_ENFORCE`,若核心模組沒有簽章則無法掛載至核心,因此需要預先關閉 UEFI Secure Boot 的功能 - 使用 `mokutil --sb-state` 查看 Secure Boot 是否停用成功。 ``` shell $ sudo mokutil --sb-state SecureBoot enabled SecureBoot validation is disabled in shim ``` - 檢查核心版本是否大於 5.15.0。 ``` shell $ uname -r 6.5.0-28-generic ``` :::danger 你現在執行的 Linux 核心版本是什麼? ::: - 安裝 linux-headers 套件及確認是否正確裝於開發環境。 ``` shell $ dpkg -L linux-headers-5.15.0-102-generic | grep "/lib/modules" /lib/modules/5.15.0-102-generic/build ``` - [X] 閱讀 〈[Linux 核心模組運作原理](https://hackmd.io/@sysprog/linux-kernel-module#fibdrv-%E5%8F%AF%E8%BC%B8%E5%87%BA-Fibonacci-%E6%95%B8%E5%88%97%E7%9A%84-Linux-%E6%A0%B8%E5%BF%83%E6%A8%A1%E7%B5%84)〉並對照 Linux 核心原始程式碼 (v6.1+),解釋 `insmod` 後,Linux 核心模組的符號 (symbol) 如何被 Linux 核心找到 (使用 List API)、`MODULE_LICENSE` 巨集指定的授權條款又對核心有什麼影響 (GPL 與否對於可用的符號列表有關),以及藉由 strace 追蹤 Linux 核心的掛載,涉及哪些系統呼叫和子系統? **練習 hello.c 測試** - 新增 hello 目錄,建立兩個檔案 `hello.c` 及 `Makefile`,編譯核心模組產生 `hello.ko` 。 ``` shell $ make -C /lib/modules/`uname -r`/build M=`pwd` modules ``` - 嘗試對核心掛載及卸載兩指令 ``` shell $ sudo insmod hello.ko $ sudo rmmod hello ``` - 使用 `dmesg` 顯示核心訊息 ``` shell [88338.431769] Hello, world [88361.955635] Goodbye, cruel world ``` **fibdrv: 可輸出 Fibonacci 數列的 Linux 核心模組** - 取得 `fibdrv` 程式碼進行 `make check` 編譯測試,觀察核心模組 `fibdrv.ko` 。 ``` shell $ modinfo fibdrv.ko filename: /home/fangni/fibdrv/fibdrv.ko version: 0.1 description: Fibonacci engine driver author: National Cheng Kung University, Taiwan license: Dual MIT/GPL srcversion: 6DA710BB9C77E697D6DBEEA depends: retpoline: Y name: fibdrv vermagic: 6.5.0-28-generic SMP preempt mod_unload modversions ``` 檔案 `fibdrv.c` 裡頭的 `MODULE_LICENSE`, `MODULE_AUTHOR`, `MODULE_DESCRIPTION`, `MODULE_VERSION` 會在編譯 `.ko` 檔後提供對應的資訊,在 [Linux 核心模組運作原理](https://hackmd.io/@sysprog/linux-kernel-module#fibdrv-%E5%8F%AF%E8%BC%B8%E5%87%BA-Fibonacci-%E6%95%B8%E5%88%97%E7%9A%84-Linux-%E6%A0%B8%E5%BF%83%E6%A8%A1%E7%B5%84) 中以 `MODULE_AUTHOR` 為例。 在 `fibdrv.c` 可看到對巨集的定義: ``` c MODULE_AUTHOR("National Cheng Kung University, Taiwan"); ``` 回朔至 [include/linux/module.h](https://elixir.bootlin.com/linux/v4.18/source/include/linux/module.h#L205) 找到 `MODULE_AUTHOR` 使用 `MODULE_INFO` 結構 ``` c #define MODULE_AUTHOR(_author) MODULE_INFO(author, _author) ``` 再從 [include/linux/moduleparam.h](https://elixir.bootlin.com/linux/v4.18/source/include/linux/moduleparam.h) 觀察, `__MODULE_INFO` 使用到 `__UNIQUE_ID` 。 ``` c #define __MODULE_INFO(tag, name, info) \ static const char __UNIQUE_ID(name)[] \ __used __attribute__((section(".modinfo"), unused, aligned(1))) \ = __stringify(tag) "=" info ``` 從 [include/linux/compiler-gcc.h](https://elixir.bootlin.com/linux/v4.18/source/include/linux/compiler-gcc.h#L208) 看到定義過程使用了 `__PASTE`。 ``` c #define __UNIQUE_ID(prefix) __PASTE(__PASTE(__UNIQUE_ID_, prefix), __COUNTER__) ``` 再對 `__UNIQUE_ID` 繼續做展開: ``` c static const char __PASTE(__PASTE(__UNIQUE_ID_, author), __COUNTER__)[] \ __used __attribute__((section(".modinfo"), unused, aligned(1))) \ = __stringify(author) "=" "National Cheng Kung University, Taiwan" ``` `__UNIQUE_ID` 使用傳入的兩參數產生一個不重複的名字,`__attribute__` 告訴編譯器文字存放的位置及其餘格式設定,`__stringify` 會將引數轉換成字串,因此 `MODULE_` 系列的巨集最後會轉變成唯一獨立的變數。 **藉由 strace 追蹤 Linux 核心的掛載** ``` shell $ sudo strace insmod fibdrv.ko execve("/usr/sbin/insmod", ["insmod", "fibdrv.ko"], 0x7fff78d52d88 /* 26 vars */) = 0 brk(NULL) = 0x55ad5cf7b000 arch_prctl(0x3001 /* ARCH_??? */, 0x7ffd4d65fde0) = -1 EINVAL (Invalid argument) mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7b99da089000 access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory) openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3 newfstatat(3, "", {st_mode=S_IFREG|0644, st_size=58227, ...}, AT_EMPTY_PATH) = 0 mmap(NULL, 58227, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7b99da07a000 close(3) = 0 ... close(3) = 0 getcwd("/home/fangni/fibdrv", 4096) = 20 newfstatat(AT_FDCWD, "/home/fangni/fibdrv/fibdrv.ko", {st_mode=S_IFREG|0664, st_size=274752, ...}, 0) = 0 openat(AT_FDCWD, "/home/fangni/fibdrv/fibdrv.ko", O_RDONLY|O_CLOEXEC) = 3 read(3, "\177ELF\2\1", 6) = 6 lseek(3, 0, SEEK_SET) = 0 newfstatat(3, "", {st_mode=S_IFREG|0664, st_size=274752, ...}, AT_EMPTY_PATH) = 0 mmap(NULL, 274752, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7b99d9f3a000 finit_module(3, "", 0) = 0 munmap(0x7b99d9f3a000, 274752) = 0 close(3) = 0 exit_group(0) = ? +++ exited with 0 +++ ``` 追蹤過程中可看到 `finit_module` 被系統執行,此函數用於動態加載 linux 核心模組,擴展系統功能性及添加新的驅動程序。 - [x] 當我們透過 `insmod` 去載入一個核心模組時,為何 `module_init` 所設定的函式得以執行呢?Linux 核心做了什麼事呢? 從 [include/linux/module.h](https://elixir.bootlin.com/linux/v4.18/source/include/linux/module.h#L129) 觀察 `module_init` 的定義方式: ``` c /* Each module must use one module_init(). */ #define module_init(initfn) \ static inline initcall_t __maybe_unused __inittest(void) \ { return initfn; } \ int init_module(void) __attribute__((alias(#initfn))); ``` - `static inline initcall_t __maybe_unused __inittest(void)` 標記函數為未使用並傳回名為 `initfn` 的初始化函數指針。 - 查閱 GCC 手冊 `__attribute__((alias("target")))` 用於給予函數或變量設置別名,讓其指向另一個函數,在這裡 `init_module` 被給予另一個別名 `initfn` ,使得加載模組時不會直接使用到 `init_module` ,而是調用 `module_init` 函數。 - 從註解中提到每個模組在加載過程中必須使用到 `module_init()` 進行初始化函數。 系統在執行 `finit_module` 後會執行 `load_module` ,從 [kernel/module.c](https://elixir.bootlin.com/linux/v4.18/source/kernel/module.c#L3785) 觀察 `load_module` 定義: ``` c static int load_module(struct load_info *info, const char __user *uargs, int flags) { struct module *mod; //... /* Figure out module layout, and allocate all the memory. */ mod = layout_and_allocate(info, flags); if (IS_ERR(mod)) { err = PTR_ERR(mod); goto free_copy; } // ... return do_init_module(mod); // ... } ``` 使用 `layout_and_allocate` 對模組進行記憶體配置,接著以 `do_init_module` 對模組進行初始化。 ``` c static noinline int do_init_module(struct module *mod) { // ... /* Start the module */ if (mod->init != NULL) ret = do_one_initcall(mod->init); // ... } ``` 再從 [init/main.c](https://elixir.bootlin.com/linux/v4.18/source/init/main.c#L874) 中追述 `do_one_initcall` 的定義,可看到`ret = fn()` 實際上針對函數 `fn` 進行初始化工作。 ``` c int __init_or_module do_one_initcall(initcall_t fn) { // ... do_trace_initcall_start(fn); ret = fn(); do_trace_initcall_finish(fn, ret); // ... } ``` - [x] 試著執行 `$ readelf -a fibdrv.ko`, 觀察裡頭的資訊和原始程式碼及 `modinfo` 的關聯,搭配上述提問,解釋像 `fibdrv.ko` 這樣的 ELF 執行檔案是如何「植入」到 Linux 核心 Executable and Linking Format(ELF) 可以表示成一種二進制文件格式,用於儲存 Executable File、Shared Library 和 Object File,可分成 ELF header、Section(s)、Section Header(s)三部份。 ``` shell $ readelf -a fibdrv.ko ELF Header: Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 Class: ELF64 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: REL (Relocatable file) Machine: Advanced Micro Devices X86-64 Version: 0x1 Entry point address: 0x0 Start of program headers: 0 (bytes into file) Start of section headers: 271424 (bytes into file) Flags: 0x0 Size of this header: 64 (bytes) Size of program headers: 0 (bytes) Number of program headers: 0 Size of section headers: 64 (bytes) Number of section headers: 52 Section header string table index: 51 ``` 從 `ELF header` 中可觀察到 `fibdrv.ko` 沒有 program headers 等訊息,因此 `fibdrv.ko` 不是能在 shell 呼叫的可執行檔,需要透過可執行檔 `insmod` 來將其植入核心,所需使用到 system call 將核心模組的資料複製到 kernel space。 - [x] 閱讀《[The Linux Kernel Module Programming Guide](https://sysprog21.github.io/lkmpg/)》(LKMPG) 並解釋 [simrupt](https://github.com/sysprog21/simrupt) 程式碼裡頭的 mutex lock 的使用方式,並探討能否改寫為 [lock-free](https://hackmd.io/@sysprog/concurrency-lockfree); `simrupt_work_func` 使用到 `kfifo buffer` 與 `circular buffer` 兩種儲存 data 的方式作為例子,先從 `kfifo` 觀察,為一種 First-In-First-Out (FIFO) 的結構體,在此定義一個 `rx_fifo` 的 kfifo 資料結構儲存傳遞到 userspace 的 data。 ``` c /* Data are stored into a kfifo buffer before passing them to the userspace */ static struct kfifo rx_fifo; ``` `kfifo` 生產資料使用到 `kfifo_in` 函數,此函數會先確認 buffer 剩餘空間大小是否裝的下生產的資料總長度,若不夠大則以剩餘空間長度為主複製資料到 buffer 中,足夠大則全部複製過去,最後更新 buffer 中插入資料的 index 並回傳插入資料的長度。 ``` c /* Insert a value into the kfifo buffer */ static void produce_data(unsigned char val) { /* Implement a kind of circular FIFO here (skip oldest element if kfifo * buffer is full). */ unsigned int len = kfifo_in(&rx_fifo, &val, sizeof(val)); if (unlikely(len < sizeof(val)) && printk_ratelimit()) pr_warn("%s: %zu bytes dropped\n", __func__, sizeof(val) - len); pr_debug("simrupt: %s: in %u/%u bytes\n", __func__, len, kfifo_len(&rx_fifo)); } ``` `Circular Buffer` 為一種固定大小的環狀記憶體緩衝區,`head index` 用於指向可插入資料的剩餘空間索引,`tail index` 則指向消費者即將取出資料的索引位置。 在 [include/linux/circ_buf.h](https://elixir.bootlin.com/linux/latest/source/include/linux/circ_buf.h) 可看到結構體的定義: ``` c struct circ_buf { char *buf; int head; int tail; }; /* Return count in buffer. */ #define CIRC_CNT(head,tail,size) (((head) - (tail)) & ((size)-1)) /* Return space available, 0..size-1. We always leave one free char * as a completely full buffer has head == tail, which is the same as * empty. */ #define CIRC_SPACE(head,tail,size) CIRC_CNT((tail),((head)+1),(size)) ``` `CIRC_CNT` 被消費者所使用到,用於計算 buffer 內含有的物體數量; `CIRC_SPACE` 則被生產者所使用,用於計算剩餘空間數量。 ``` c static int fast_buf_get(void) { struct circ_buf *ring = &fast_buf; /* prevent the compiler from merging or refetching accesses for tail */ unsigned long head = READ_ONCE(ring->head), tail = ring->tail; int ret; if (unlikely(!CIRC_CNT(head, tail, PAGE_SIZE))) return -ENOENT; /* read index before reading contents at that index */ smp_rmb(); /* extract item from the buffer */ ret = ring->buf[tail]; /* finish reading descriptor before incrementing tail */ smp_mb(); /* increment the tail pointer */ ring->tail = (tail + 1) & (PAGE_SIZE - 1); return ret; } ``` `fast_buf_get` 函數代表消費者,分別取得 buffer 的 `head` 及 `tail` 後先使用 `smp_rmb()` 作為記憶體緩衝區保護取出資料的過程,`ret = ring->buf[tail]` 取出該筆資料後執行 `smp_mb()` ,並對 `tail` 做更新,最後回傳取出的資料內容。 接著從 [linux/include/linux/workqueue.h](https://elixir.bootlin.com/linux/latest/source/include/linux/workqueue.h) 看到兩個 `mutex lock` 的定義: ``` c /* Mutex to serialize kfifo writers within the workqueue handler */ static DEFINE_MUTEX(producer_lock); /* Mutex to serialize fast_buf consumers: we can use a mutex because consumers * run in workqueue handler (kernel thread context). */ static DEFINE_MUTEX(consumer_lock); ``` `producer_lock` 作為生產者執行程式時的保護鎖,以 `mutex_lock` 及 `mutex_unlock` 方式防止其餘 thread 干擾包在其中的程式,而 `consumer_lock` 作為消費者的保護鎖,功能與 `producer_lock` 相似。 ``` c while (1) { /* Consume data from the circular buffer */ mutex_lock(&consumer_lock); val = fast_buf_get(); mutex_unlock(&consumer_lock); if (val < 0) break; /* Store data to the kfifo buffer */ mutex_lock(&producer_lock); produce_data(val); mutex_unlock(&producer_lock); } ``` 在 `simrupt_work_func` 函式中以 `val = fast_buf_get()` 執行消費者從 `circular buffer` 中取出資料,而 `produce_data(val)` 則是將剛才取出的資料放至 `kfifo buffer` 中,兩個過程分別都由 `mutex_lock` 及 `mutex_unlock` 保護,確保只有一個 thread 可對 `buffer` 進行存取。 在 `simrupt` 中單純使用 lock 作為共同資料存取的保護手段,但在多執行緒環境下此方法容易有阻塞問題,也就是說當其中一個 thread 再對共同記憶體進行存取過程時因為 lock 的性質,導致其餘欲使用到此空間的 thread 執行停滯,而若此時原進行存取的執行緒被系統做搶先或直接被 kill 則會造成所有 thread 都被迫暫停工作的情形,因此為了提昇多執行緒的效能達成並行成效,可使用 [lock-free](https://hackmd.io/@sysprog/concurrency-lockfree) 方法做改進。 lock-free 為一種實現並行操作的方法,避免使用 lock 減少執行緒間的競爭關系,它使得執行緒間不須等待其他人釋放 lock 就可以自由操作,主要依賴於 `Compare and Swap (CAS)` 實現對共用資源的並行存取操作。 ``` c int CAS(int *a, int old_val, int new_val) { if (*a == old_val) { *a = new_val; return old_val; } return *a; } ``` `CAS` 有三種須代入的參數,分別為用來比較的指標變數 `*a`、指標原先的舊數值 `old_val` 以及要做替換的新數值 `new_val`。 ``` c int old, new; do { old = *p; new = NEW_VALUE; } while(CAS(*p, old, new) != new); ``` 使用 `CAS` 包覆的迴圈作為 Critical Section,其空間為共用記憶體做存取,若執行到該空間的內容則 `CAS` 會去檢查 `p` 的值是否與原先 `old` 的值相同,一樣代表此空間沒有被其他執行緒使用過可以將 `p` 的值替換為 `new`,而若不相同則重新跑一次迴圈。 - [x] 研讀 [CMWQ](https://www.kernel.org/doc/html/latest/core-api/workqueue.html) (Concurrency Managed Workqueue) 文件,對照 [simrupt](https://github.com/sysprog21/simrupt) 專案的執行表現,留意到 worker-pools 類型可指定 "Bound" 來分配及限制特定 worker 執行於指定的 CPU,Linux 核心如何做到?CMWQ 關聯的 worker thread 又如何與 CPU 排程器互動? `workqueue` 作為 linux 核心對非同步的執行手段,其中 `work item` 代表欲執行的一種工作項目,創建時會先將其加入到等待佇列( workqueue )中,等待 linux 核心找到適合的 `work thread` 執行,以往 `workqueue` 的作法分成兩種方式: 1. **single threaded (ST) workqueue** 只依靠單一個 `work thread` 來處理系統上所有的 `work item`,因此若前面正執行的 `work item` 非常龐大,其餘所有的 work 都須等待該`work thread` 執行結束,雖然有可節省資源的優勢在(只需要少數的 `work thread` ),但會造成系統執行效率低落的問題。 2. **multi threaded (MT) workqueue** 每個 CPU 上會有一個 `work thread`,對於每個 `workqueue` 會有自己的一個 `thread pool` 且數目固定,對於分配到該 CPU 的 `work item` 有嚴格的綁定關係,不能隨意移到別的 `work thread` 上執行,需要多個 `work thread` 配合故十分消耗資源,雖然可緩解執行效率低落的現象,但在並行的效果有限,且不可避免的還是會有死結情況產生。 因此 CMWQ(Concurrent Multi-Queue Workqueue)方法被提出用於解決 ST 及 MT 效能和耗能上的問題,它打破一般對於 `workqueue` 與特定的 `thread pool` 之間的關聯性,建立若干個 `worker pool` (其等於 `thread pool` 的概念) 可共享給所有的 `workqueue` ,且可以根據 CPU 的負載情況動態調整 `worker pool` 大小釋放 `work thread` ,更好的實現並行,其主要實現的目標有: - **維持與原始 `workqueue` 的兼容性** 在與原本 `workqueue` 相容的情況下原始程式碼可以更容易的移植到 CMWQ,不需要進行過大的改動。 - **共享 per-CPU worker pools** CMWQ使得每個 `workqueue` 都可共用 per-CPU worker pools,也就是說不同的 `workqueue` 可共同使用一個 `work pool`, 減少資源浪費。 - **黑盒子式調整** `worker pool` 和並行等級共同封裝在系統內部,使用者只須使用系統提供的 API 進行提交和管理。 在 CMWQ 設計理念中分為兩種 `work pool` 類型,屬於Bound 類型的 `work pool` 會被限制與特定的 CPU 綁定,且每個 CPU 會有兩個 `work pool`,一個提供給普通的 work,一個則給予高等級的 work,藉由 `workqueue` 上的 `flag` 決定由哪一個來執行;屬於 Unbound 類型的 `work pool` 未與 CPU 做綁定,可動態釋放 `work thread` 減少資源消耗。 透過 `workqueue API` 生成的 `work item` 會根據 `workqueue` 參數和本身的屬性決定該交由哪個種類的 `work pool` 處理,而只要 CPU 上存在一個或個正在執行的 `worker thread`, `work pool` 就會停止執行新的 work 直到最後一個 `worker thread` 執行結束,確保不會有尚有 `work item` 但 `worker thread` 卻閒置的問題,且不會過度產生 `worker thread`。 - [ ] 探討 Timsort, Pattern Defeating Quicksort (pdqsort) 及 Linux 核心 [lib/sort.c](https://github.com/torvalds/linux/blob/master/lib/sort.c) 在排序過程中的平均比較次數,並提供對應的數學證明; - [ ] 解釋 xoroshiro128+ 的原理 (對照〈[Scrambled Linear Pseudorandom Number Generators](https://arxiv.org/pdf/1805.01407.pdf)〉論文),並利用 [ksort](https://github.com/sysprog21/ksort) 提供的 xoro 核心模組,比較 Linux 核心內建的 /dev/random 及 /dev/urandom 的速度,說明 xoroshiro128+ 是否有速度的優勢?其弱點又是什麼?