--- tags : linux kernel, jserv, lab0-c --- # 2023q1 Homework3 (fibdrv) contributed by < [`WangHanChi`](https://github.com/WangHanChi) > >[作業要求](https://hackmd.io/@sysprog/linux2023-fibdrv/%2F%40sysprog%2Flinux2023-fibdrv-b#-%E6%92%B0%E5%AF%AB-Linux-%E6%A0%B8%E5%BF%83%E6%A8%A1%E7%B5%84) :::info - [x] 研讀上述 Linux 效能分析的提示 描述,在自己的實體電腦運作 GNU/Linux,做好必要的設定和準備工作 - [x] 研讀上述費氏數列相關材料 (包含論文),摘錄關鍵手法,並思考 clz / ctz 一類的指令對 Fibonacci 數運算的幫助。請列出關鍵程式碼並解說 - [x] 複習 C 語言 數值系統 和 bitwise operation,思考 Fibonacci 數快速計算演算法的實作中如何減少乘法運算的成本; - [x] 學習指出針對大數運算的加速運算和縮減記憶體操作成本的舉措,紀錄你的認知和疑惑 - [ ] 注意到 fibdrv.c 存在著 DEFINE_MUTEX, mutex_trylock, mutex_init, mutex_unlock, mutex_destroy 等字樣,什麼場景中會需要呢?撰寫多執行緒的 userspace 程式來測試,觀察 Linux 核心模組若沒用到 mutex,到底會發生什麼問題。嘗試撰寫使用 POSIX Thread 的程式碼來確認 - [ ] 逐步縮減 Fibonacci 的執行時間,過程中要充分量化 - [ ] 嘗試研讀 sysprog21/bignum 的實作,理解其中的技巧並與你的實作進行效能比較,探討個中值得取鏡之處。 - [ ] 當 $Fib(n)$ 隨著 n 越來越大時,由 Linux 核心傳遞字串形式的十進位就成為效能瓶頸 ::: :::spoiler 開發環境 ```shell $ gcc --version gcc (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0 Copyright (C) 2021 Free Software Foundation, Inc. $ lscpu | less Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Address sizes: 48 bits physical, 48 bits virtual Byte Order: Little Endian CPU(s): 12 On-line CPU(s) list: 0-11 Vendor ID: AuthenticAMD Model name: AMD Ryzen 5 5600X 6-Core Processor CPU family: 25 Model: 33 Thread(s) per core: 2 Core(s) per socket: 6 Socket(s): 1 Stepping: 0 Frequency boost: enabled CPU max MHz: 4650.2920 CPU min MHz: 2200.0000 BogoMIPS: 7385.75 ``` ::: ## 研讀上述 Linux 效能分析的提示與描述 Linux kernel 與 Ubuntu 的版本如下: ```shell $ uname -a Linux hanchi 5.19.0-35-generic #36~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Fri Feb 17 15:17:25 UTC 2 x86_64 x86_64 x86_64 GNU/Linux ``` ### 為何不使用虛擬機來執行這份作業 >本段落參考自 [Virtual Machines: A Closer Look at Pros and Cons](https://cynexlink.com/latest-articles/virtual-machines-pros-cons/) 與 [Containers vs Virtual Machines (VMs)](https://www.eginnovations.com/blog/containers-vs-vms/) 與 [什麼是虛擬機器 (VM)?](https://www.oracle.com/tw/cloud/compute/virtual-machines/what-is-virtual-machine/) 如果不想要幫電腦安裝雙系統又想要使用 linux 的話,通常會有兩種用法 1. 使用 **虛擬機 virtual machine** 2. 使用 **容器 container** 以下將分開介紹 ![](https://i.imgur.com/11l1Y0f.png) - 虛擬機 (VM) - 常用的軟體為 [VirtualBOX](https://www.virtualbox.org/wiki/Downloads) 或是 [Parallels Desktop](https://www.parallels.com/hk/pd/general/?gclid=Cj0KCQjwk7ugBhDIARIsAGuvgPagBDebItn5OAsvfi8SygsB7P59Y-2G0sueWtaAAmlwVZZQRhsQDYAaAo_dEALw_wcB) - 由上圖來看,虛擬機通常有自己的作業系統和應用程式,獨立於其**主機**(通常是物理伺服器)運行。所以,可在同一台伺服器上創建多個作業系統環境 - 優點 - 虛擬機可以提供不同於硬體主機的指令集架構或 ISA。ISA 用作軟件和硬件之間的接口 - 如果虛擬機上的作業系統崩潰了,也不會影響到原本的系統 - 運行虛擬機具有安全優勢,假如要訪問一些可能有毒的線上影音學習網站(?),電腦可能會被病毒感染而中毒,這時如果使用了虛擬機就可以杜絕自己原本的系統中毒 - 缺點 - 虛擬機的效率低於真實機器,因為它們間接訪問硬體。在主機作業系統之上運行 VM 軟體意味著它必須請求從物理設備訪問硬碟和記憶體,==這也是為何老師希望我們直接安裝雙系統的原因== - 容器 - 通常使用 [docker](https://www.docker.com/) 這個軟體 - 一樣由上圖來看,容器通常與主機系統共享許多資源,因此它們需要安裝的東西更少就能夠運行。虛擬機相比,容器通常佔用更少的空間,消耗更少的 RAM 和 CPU 時間。出於這個原因,與使用虛擬機相比,使用容器通常可以在單個服務器上容納更多應用程式。 - 優點 - 使用 Dockerfile 可以輕鬆且快速構建容器鏡像。 - 與上述一樣,可以直接訪問硬體設備,所以效能的折損相比虛擬機還要少,所以==這是老師還可以接受我們使用容器的原因== - 資源消耗較低 - 缺點 - 因為可以直接訪問硬體,因此他的安全性不如虛擬機好 ## 研讀上述費氏數列相關材料 ### 研讀 Part 1: Introduction [原文連結](http://derekmolloy.ie/writing-a-linux-kernel-module-part-1-introduction/) #### Prepare the System 這邊的步驟與老師筆記的步驟相同 ```sehll $ sudo apt install linux-headers-`uname -r` $ dpkg -L linux-headers-5.19.0-35-generic | grep "/lib/modules" ``` 得到以下結果 ```shell $ dpkg -L linux-headers-5.19.0-35-generic | grep "/lib/modules" /lib/modules /lib/modules/5.19.0-35-generic /lib/modules/5.19.0-35-generic/build ``` 接著進入 `/usr/src/linux-headers-5.19.0-35-generic/`,可以發現目錄夾下的目錄也跟文章中的一致 ```shell $ ls arch Documentation init Kconfig mm scripts ubuntu block drivers io_uring kernel Module.symvers security usr certs fs ipc lib net sound virt crypto include Kbuild Makefile samples tools ``` 接著使用一個簡單的 Hello, world 來測試 LKM :::spoiler 原始碼 ```c= /** * @file hello.c * @author Derek Molloy * @date 4 April 2015 * @version 0.1 * @brief An introductory "Hello World!" loadable kernel module (LKM) that can display a message * in the /var/log/kern.log file when the module is loaded and removed. The module can accept an * argument when it is loaded -- the name, which appears in the kernel log files. * @see http://www.derekmolloy.ie/ for a full description and follow-up descriptions. */ #include <linux/init.h> // Macros used to mark up functions e.g., __init __exit #include <linux/module.h> // Core header for loading LKMs into the kernel #include <linux/kernel.h> // Contains types, macros, functions for the kernel MODULE_LICENSE("GPL"); ///< The license type -- this affects runtime behavior MODULE_AUTHOR("Derek Molloy"); ///< The author -- visible when you use modinfo MODULE_DESCRIPTION("A simple Linux driver for the BBB."); ///< The description -- see modinfo MODULE_VERSION("0.1"); ///< The version of the module static char *name = "world"; ///< An example LKM argument -- default value is "world" module_param(name, charp, S_IRUGO); ///< Param desc. charp = char ptr, S_IRUGO can be read/not changed MODULE_PARM_DESC(name, "The name to display in /var/log/kern.log"); ///< parameter description /** @brief The LKM initialization function * The static keyword restricts the visibility of the function to within this C file. The __init * macro means that for a built-in driver (not a LKM) the function is only used at initialization * time and that it can be discarded and its memory freed up after that point. * @return returns 0 if successful */ static int __init helloBBB_init(void){ printk(KERN_INFO "EBB: Hello %s from the BBB LKM!\n", name); return 0; } /** @brief The LKM cleanup function * Similar to the initialization function, it is static. The __exit macro notifies that if this * code is used for a built-in driver (not a LKM) that this function is not required. */ static void __exit helloBBB_exit(void){ printk(KERN_INFO "EBB: Goodbye %s from the BBB LKM!\n", name); } /** @brief A module must use the module_init() module_exit() macros from linux/init.h, which * identify the initialization function at insertion time and the cleanup function (as * listed above) */ module_init(helloBBB_init); module_exit(helloBBB_exit); ``` ::: 程式碼中有許多可以討論的地方: - 第 16 行 ```c MODULE_LICENSE("GPL"); ``` - 這個巨集是要我們填入所使用的指定授權方式,若是沒有填的話,就會顯示警告訊息,但是不會拒絕載入程式,有定義過得 License 可以在 [linux/module.h](https://github.com/torvalds/linux/blob/master/include/linux/module.h#L185) 中找到 - 第 21 行 ```c static char *name = "world"; ``` - 定義 name 指向一個 static 的字串 - 第 22 行 ```c module_param(name, charp, S_IRUGO); ``` - 這是使用了一個巨集 `module_param(name, type, permissions)`,主要在載入 module 時帶參數進去,三個參數分別是變數名稱、變數的資料型別,以及其對應 `sysfs` 檔案的權限,詳細的 moduleparam 可以 [linux/moduleparam.h](https://github.com/torvalds/linux/blob/master/include/linux/moduleparam.h) 中找到。而這邊所設定的權限 `S_IRUGO` 代表的是允許用戶/組/其他人進行讀取訪問 - 第 31 和 40 行 ```c static int __init helloBBB_init(void) static void __exit helloBBB_exit(void) ``` - 這個函式名稱可以自行設定成名稱,只是最後要把函式名稱放進這兩個巨集 `module_init`, `module_exit` (48, 49) 行 - 第 32 行 ```c printk(KERN_INFO "EBB: Hello %s from the BBB LKM!\n", name); ``` - 這個 `printk` 功能與一般的 printf 相似,只是它額外需要傳遞 **log levels** 進去,log levels 定義在 [linux/kern_levels.h](https://github.com/torvalds/linux/blob/master/include/linux/kern_levels.h),而 printk 的定義在 [linux/printk.h](https://github.com/torvalds/linux/blob/master/include/linux/printk.h) - 老師在 lkmpg 裡面是使用 pr_info,其功能與 `printk(KERN_INFO .....)` 一致,詳情可以參考[這裡](https://github.com/torvalds/linux/blob/master/include/linux/printk.h#L527) :::spoiler pr 系列完整版定義 ```c #define pr_debug(fmt, ...) \ dynamic_pr_debug(fmt, ##__VA_ARGS__) #elif defined(DEBUG) #define pr_debug(fmt, ...) \ printk(KERN_DEBUG pr_fmt(fmt), ##__VA_ARGS__) #else #define pr_debug(fmt, ...) \ no_printk(KERN_DEBUG pr_fmt(fmt), ##__VA_ARGS__) #endif #ifdef CONFIG_PRINTK #define printk_once(fmt, ...) \ DO_ONCE_LITE(printk, fmt, ##__VA_ARGS__) #define printk_deferred_once(fmt, ...) \ DO_ONCE_LITE(printk_deferred, fmt, ##__VA_ARGS__) #else #define printk_once(fmt, ...) \ no_printk(fmt, ##__VA_ARGS__) #define printk_deferred_once(fmt, ...) \ no_printk(fmt, ##__VA_ARGS__) #endif #define pr_emerg_once(fmt, ...) \ printk_once(KERN_EMERG pr_fmt(fmt), ##__VA_ARGS__) #define pr_alert_once(fmt, ...) \ printk_once(KERN_ALERT pr_fmt(fmt), ##__VA_ARGS__) #define pr_crit_once(fmt, ...) \ printk_once(KERN_CRIT pr_fmt(fmt), ##__VA_ARGS__) #define pr_err_once(fmt, ...) \ printk_once(KERN_ERR pr_fmt(fmt), ##__VA_ARGS__) #define pr_warn_once(fmt, ...) \ printk_once(KERN_WARNING pr_fmt(fmt), ##__VA_ARGS__) #define pr_notice_once(fmt, ...) \ printk_once(KERN_NOTICE pr_fmt(fmt), ##__VA_ARGS__) #define pr_info_once(fmt, ...) \ printk_once(KERN_INFO pr_fmt(fmt), ##__VA_ARGS__) ``` ::: #### Building the Module Code ```makefile obj-m+=hello.o all: make -C /lib/modules/$(shell uname -r)/build/ M=$(PWD) modules clean: make -C /lib/modules/$(shell uname -r)/build/ M=$(PWD) clean ``` 這個 makefile 的第一行 `obj-m+=hello.o` 定義了要構件的模組 `obj-m` 定義一個可加載模組目標 如果 `make` 成功的話,就可以在資料夾下面看到多了一個名為 hello 的可加載的模組,其副檔名為 **.ko** #### Testing the LKM 使用以下命令來查看所有核心模組的權限 ```shell $ ls -l *.ko ``` 接著使用以下命令來插入 (insert) 模組 ```shell $ sudo insmod hello.ko ``` 接著輸入以下命令可以看到剛做的 module 在上面 ```shell $ lsmod ``` ``` $ lsmod Module Size Used by hello 16384 0 cmac 16384 3 ... ``` 也可以使用以下命令來查看模組資訊 ```shell $ modinfo hello.ko ``` 最後要移除模組的話,可以使用以下命令 ```shell $ sudo rmmod hello.ko ``` 最後可以觀察 kernel log 來看看是否真的有紀錄 ```shell $ cd /var/log && tail -f kern.log ... Mar 11 17:04:29 hanchi kernel: [74013.619466] EBB: Hello world from the BBB LKM! Mar 11 17:19:20 hanchi kernel: [74904.867161] EBB: Goodbye world from the BBB LKM! Mar 11 17:23:17 hanchi kernel: [75141.784565] EBB: Hello world from the BBB LKM! ``` #### Testing the LKM Custom Parameter 在剛剛的 hello.c 中,我們有包含了一個自定義參數,它允許在初始化時將參數傳遞給核心模組,可以使用以下命令來進行測試 ```shell $ sudo insmod hello.ko name=wanghanchi ``` 然後再使用以下命令來觀察 log 輸出 ```shell $ cd /var/log && tail -f kern.log ``` 也可以使用以下命令來得到核心模組在記憶體中的偏移量 ```shell $ cat module|grep hello ``` 最後可以用以下命令來觀察輸出結果 ```shell $ sudo dmesg -t | tail -1 ``` ### 研讀 Part 2: A Character Device 這個程式使用了一個簡單的 char 驅動程式,可以用於在 Linux user space 程序和運行在 Linux 核心空間中的可加載核心模組 (LKM) 之間傳遞資訊。在 user space 應用程序向 LKM 發送一個字符串。LKM 然後用發送的消息以及發送的消息包含的字母數進行響應。 #### Major and Minor Numbers 設備驅動程式有一個關聯的主要和次要編號。例如,`/dev/ram0` 和 `/dev/null` 與主設備號為 1 的驅動程式相關聯,而 `/dev/tty0` 和 `/dev/ttyS0` 與主設備號為 4 的驅動程式相關聯。 例如下面的部份,可以看到 `ttyS0`, `ttyS1` 與 `ttyS10` 都是與主設備號為 4 的驅動程式相關 ```shell crw-rw---- 1 root dialout 4, 64 3月 11 04:30 ttyS0 crw-rw---- 1 root dialout 4, 65 3月 11 04:30 ttyS1 crw-rw---- 1 root dialout 4, 74 3月 11 04:30 ttyS10 ``` #### The Device Driver Source Code 等等的程式會使用到 file_operations 中的一些功能,詳細的內容可看 [linux/fs.h](https://github.com/torvalds/linux/blob/master/include/linux/fs.h) - file_operations - dev_open() 每次從 User space 中打開設備時調用 - dev_read() 當資料從設備發送到 User space 時調用 - dev_write() 當資料從 User space 發送到設備時調用 - dev_release() 當設備在 User space 關閉時調用 這個驅動程式有一個 class 名字(ebb)也有一個 device 的名字(ebbchar) 這邊有關於這些程式的一些要點 - 訊息的大小固定為 256 個 char - 這段程式碼並不是 **multi-process safe** - 這段程式碼的 ebbchar_init 比起上面那範例還要長很多,那是因為它現在可以自動分配主編號,註冊設備類,並註冊設備驅動程式。重要的是,如果這段程式碼出了任何的問題,它會小心安全的退出(或是使用 **goto** 語法) ## 修改 fibdrv 核心模組 ### client.c 先 trace 完這個在 User space 執行的程式 ```c int offset = 100; /* TODO: try test something bigger than the limit */ int fd = open(FIB_DEV, O_RDWR); if (fd < 0) { perror("Failed to open character device"); exit(1); } ``` 可以看到在這個部份定義了 Fibonacci 的上限為 100 接著在開啟在 `/dev/fibonacci` 的核心模組,注意這邊使用了 `open` ,他並不是使用一般的系統呼叫 [**open(2)**](https://man7.org/linux/man-pages/man2/openat.2.html) 而是被修改成為 vfs 的 open 了,詳情可以參考[老師的筆記](https://hackmd.io/@sysprog/linux2023-fibdrv/%2F%40sysprog%2Flinux2023-fibdrv-b#Linux-Virtual-File-System-%E4%BB%8B%E9%9D%A2) 以下是 VFS 所定義的系統呼叫 ```c const struct file_operations fib_fops = { .owner = THIS_MODULE, .read = fib_read, .write = fib_write, .open = fib_open, .release = fib_release, .llseek = fib_device_lseek, }; ``` ```c for (int i = 0; i <= offset; i++) { sz = write(fd, write_buf, strlen(write_buf)); printf("Writing to " FIB_DEV ", returned the sequence %lld\n", sz); } ``` 這段程式碼可以發現,它會執行到我們所設定的 offset ,並且重複輸出 `printf("Writing to " FIB_DEV ", returned the sequence %lld\n", sz);` 主要就是用來測試寫入核心模組的,可以看到它也是修改了系統呼叫中的 `write` 來完成,因此現在變成了 vfs 的 write 了 ```c for (int i = 0; i <= offset; i++) { lseek(fd, i, SEEK_SET); sz = read(fd, buf, 1); printf("Reading from " FIB_DEV " at offset %d, returned the sequence " "%lld.\n", i, sz); } ``` 這段主要修改系統呼叫 [read](https://man7.org/linux/man-pages/man2/read.2.html) 成 vfs 的 **read** 去讀取 fibonacci 中的值,最後迭代將其印出來 ```c for (int i = offset; i >= 0; i--) { lseek(fd, i, SEEK_SET); sz = read(fd, buf, 1); printf("Reading from " FIB_DEV " at offset %d, returned the sequence " "%lld.\n", i, sz); } ``` 這段程式碼也與上面的概念相同,只是從大到小,上面的是由小到大 ### Fast Doubling 加速 這邊使用到了 [Fast Doubling](https://www.nayuki.io/page/fast-fibonacci-algorithms) 的方法來進行加速,並且會與最一開始的版本進行效能比較 為了計算運算的時間,我們引入了 ktime 來進行時間的測量,並且透過目前沒有用到的 `wirte` 回傳時間資訊 ```c /* write operation is skipped */ static ssize_t fib_write(struct file *file, const char *buf, size_t size, loff_t *offset) { return ktime_to_ns(kt); } ``` 再來使用老師在[作業提示](https://hackmd.io/@sysprog/linux2023-fibdrv/%2F%40sysprog%2Flinux2023-fibdrv-d)的 Fast Doubling 方法,進行計算,藉此比較與原本方法的效能差異 由於要輸出時間資訊,並且會製成折線圖,因此會在 client.c 添加寫檔的相關程式碼,詳情請見 [commit](https://github.com/WangHanChi/fibdrv/commit/282c859f65bba9f6d6dda44840a0ba3df3d5a6f2) 接著因為接下來需要多次進行效能測試,因此寫了一個腳本來方便測試 此腳本修改自 [colinyoyo26](https://github.com/colinyoyo26/fibdrv/commit/4ce43c4e7ee0b80c4ce9e6dcd995bbfdf239206c),主要因為了我們在 client.c 中所輸出的檔案不同,因此針對檔案的讀取以及統計方法進行了修改,詳情可參考 [commit](https://github.com/WangHanChi/fibdrv/commit/f75ad71308309ec0551d38a6a7056cb1154ab28e) 接著再對 Makefile 進行添加 `make plot` 即可 ```makefile plot: all $(MAKE) unload $(MAKE) load @python3 scripts/driver.py $(MAKE) unload ``` 接著將 CPU 中的幾顆核心保留,參照這篇文章所提供的方法 [Linux性能優化(十五)——CPU綁定](https://blog.51cto.com/quantfabric/2594336) 1. 修改 grub 的配置文件 ```shell $ cd /etc/default/grub $ sudo vim grub ``` 修改 `GRUB_CMDLINE_LINUX` 這行,使其變成 `GRUB_CMDLINE_LINUX="isolcpus=0,1"`,孤立了兩個 CPU 的執行緒 2. 更新 grub ```shell $ sudo update-grub $ sudo update-grub2 $ sudo grub-mkconfig -o /boot/grub/grub.cfg ``` 3. 重新開機 ```shell $ sudo reboot ``` 檢查一下是否成功孤立 CPU 中的第 0, 1 執行緒,可以看到它從 2 開始 ```shell $ taskset -cp 1 pid 1's current affinity list: 2-11 ``` :::info 有個有趣的實驗,因為我的顯卡目前是 nvidia 1050ti ,不支援 av1 影片格式的硬解,所以從 youtube播放 8K 的影片會讓 CPU 滿載,但是因為先前孤立了 CPU0 與 CPU1,所以可以從系統監控看到這兩個執行緒的負載基本上都是0。 [8K youtube 影片](https://www.youtube.com/watch?v=PB4gId2mPNc&t=62s&ab_channel=%E6%A5%AD%E8%BC%9D%E9%A6%AE) ![](https://i.imgur.com/wYGNiuF.png) ::: 再來排除干擾效能分析的因素 - 抑制 [address space layout randomization](https://en.wikipedia.org/wiki/Address_space_layout_randomization) (ASLR) ```shell $ sudo sh -c "echo 0 > /proc/sys/kernel/randomize_va_space" ``` - 設定 scaling_governor 為 performance。準備以下 shell script,檔名為 performance.sh ```shell for i in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor do echo performance > ${i} done ``` 至於為何要設定這個的原因,可以從 [CPU Performance Scaling](https://www.kernel.org/doc/html/latest/admin-guide/pm/cpufreq.html#cpu-performance-scaling) 找到,它會向 CPU 請求使用**最高頻率**來執行 >performance When attached to a policy object, this governor causes the highest frequency, within the scaling_max_freq policy limit, to be requested for that policy. The request is made once at that time the governor for the policy is set to performance and whenever the scaling_max_freq or scaling_min_freq policy limits change after that. 之後再用 `$ sudo sh performance.sh` 執行 - 針對 Intel 處理器,關閉 turbo mode ```shell $ sudo sh -c "echo 1 > /sys/devices/system/cpu/intel_pstate/no_turbo" ``` 關於 intel turbo boost 的敘述可以觀看這裡 [Intel Turbo Boost](https://en.wikipedia.org/wiki/Intel_Turbo_Boost) 但因為我的處理器是 AMD 的 5600X,因此可以從這邊觀察是否有開啟 [AMD Turbo Core](https://en.wikipedia.org/wiki/AMD_Turbo_Core) ```shell $ cpupower frequency-info analyzing CPU 0: driver: acpi-cpufreq CPUs which run at the same hardware frequency: 0 CPUs which need to have their frequency coordinated by software: 0 maximum transition latency: Cannot determine or is not supported. hardware limits: 2.20 GHz - 4.65 GHz available frequency steps: 3.70 GHz, 2.80 GHz, 2.20 GHz available cpufreq governors: conservative ondemand userspace powersave performance schedutil current policy: frequency should be within 2.20 GHz and 3.70 GHz. The governor "ondemand" may decide which speed to use within this range. current CPU frequency: Unable to call hardware current CPU frequency: 2.19 GHz (asserted by call to kernel) boost state support: Supported: yes Active: no ``` 可以看到我的 CPU 有支持,但是沒有啟用 :::info 如果想要啟用的話,可以使用以下命令 ```shell $ cpupower frequency-set -g ondemand $ sudo sh -c "echo '1' > /sys/devices/system/cpu/cpufreq/boost" ``` 禁用 ```shell $ sudo sh -c "echo '0' > /sys/devices/system/cpu/cpufreq/boost" ``` 參考自 [Unlock Turbo Boost For Your Ryzen CPU On Linux](https://lucraymond.net/2022/04/12/unlock-your-ryzen-cpu-on-linux-and-enable-turbo-boost/) ::: - 關閉多執行緒功能 ```shell $ sudo sh -c "echo off > /sys/devices/system/cpu/smt/control" ``` 最後可以把出這樣的結果圖 - Iteration Method ![](https://i.imgur.com/l0YaFTg.png) - Fast Doubling Method ![](https://i.imgur.com/3dRm58X.png) :::info TODO : 思考為何 Fast Doubling 的方法會抖動的這麼嚴重,也許是我哪邊做錯了 ::: 但是以上方法只能計算到第 93 個序列而已,所以需要修改,至少要讓其可以計算到 500 個序列 ### 使用 struct 定義資料結構 使用以下資料結構來定義數值 ```c struct BigN { unsigned long long lower, upper; }; ``` 接著修改核心模組,使用 `copy_to_user` 這個函式將 kernel space 的資料拷貝到 user space ,避免直接使用到指標指到 kernel 的記憶體,導致程式被 `killed`。 `copy_to_user` 的使用方法可以從[這邊](https://archive.kernel.org/oldlinux/htmldocs/kernel-api/API---copy-to-user.html)看, - User space ```c char buf[1]; int fd = open(FIB_DEV, O_RDWR); ... read(fd, buf, 1); ``` - Kernel space ```c bn *output = kmalloc(sizeof(bn), GFP_KERNEL); /* get time */ kt = ktime_get(); fib_sequence(*offset, output); kt = ktime_sub(ktime_get(), kt); /* copy_to_user */ int i = copy_to_user(buf, output, sizeof(bn)); /* free */ kfree(output); /* check free */ if(!i){ printk(KERN_ALERT "copy_to_use is not used"); } ``` 然後可以檢查到它輸出到第 186 個序列的時候都是正確的,超過之後還是會 overflow ```shell ... Reading from /dev/fibonacci at offset 184, returned the sequence 6891616170087056899,10993266775918486379. Writing to /dev/fibonacci, returned the sequence 130 Reading from /dev/fibonacci at offset 185, returned the sequence 11150869200619234444,3465294890923511181. Writing to /dev/fibonacci, returned the sequence 120 Reading from /dev/fibonacci at offset 186, returned the sequence 18042485370706291343,14458561666841997560. Writing to /dev/fibonacci, returned the sequence 120 Reading from /dev/fibonacci at offset 187, returned the sequence 10746610497615974171,17923856557765508741. Writing to /dev/fibonacci, returned the sequence 110 Reading from /dev/fibonacci at offset 188, returned the sequence 10342351794612713899,13935674150897954685. ... ``` 而且可以發現到他的計算方式為逗號 `,` 前面的數字乘上 unsigned long long 的上界 (18446744073709551615) 再加上逗號後面的數字,就是這個 index 的值,以 **184** 為例 序列結果應為 $6891616170087056899*18446744073709551615+10993266775918486379$ 結果應為 $127127879743834334146972278486287885163$ 以計算機計算上式的結果為 $1.271278797*10^{38}$ ,比對後為**正確**的 但是這樣遠遠不到 500 的序列,雖然可以透過在 struct 新增 `unsigne long long` ,但是這樣治標不致本,並且也沒有可讀性,因此需要更換一個新的方法 ### 初步引入 BigNum 這邊先引入大數運算所需要的標頭檔以及來源檔,詳情可以參照 [老師的提示](https://hackmd.io/@sysprog/linux2023-fibdrv/%2F%40sysprog%2Flinux2023-fibdrv-d#%E5%88%9D%E6%AD%A5%E6%94%AF%E6%8F%B4%E5%A4%A7%E6%95%B8%E9%81%8B%E7%AE%97),並且我將分別放在 `fibdrv/inc` 以及 `fibdrv/src` 下,避免版面太亂,完整程式碼詳見這個 [commit](https://github.com/WangHanChi/fibdrv/commit/0dc23f4e93619a07f29689793a8ff56708b55a6a)。 #### `bn_to_string` 接著看到 `bn_to_string` 這個函式,這個函式比較難理解,主要就是將二進制轉成十進制的字串 舉一個簡單的例子來解釋,將 `1010` 轉成十進制會得到 `10`,而他的過程會是下面這樣 ``` 假設 現在只用 4 個 bit 來儲存 len = (8 * 1 * 1) / 3 + 2 + 0 for 迴圈的順序 : 1 -> 0 -> 1 -> 0 ------------------- 變數 : len = 4, j = 2 ------------------- 1 : carry = 1 s[2] = s[2] * 2 - 48 + 0 = 49 ('1') carry = 0 s[1] = s[1] * 2 - 48 + 0 = 48 ('0') carry = 0 s[0] = s[0] * 2 - 48 + 0 = 48 ('0') carry = 0 ------------------- 0 : carry = 0 s[2] = s[2] * 2 - 48 + 0 = 50 ('2') carry = 0 s[1] = s[1] * 2 - 48 + 0 = 48 ('0') carry = 0 s[0] = s[0] * 2 - 48 + 0 = 48 ('0') carry = 0 ------------------- 1 : carry = 1 s[2] = s[2] * 2 - 48 + 0 = 53 ('5') carry = 0 s[1] = s[1] * 2 - 48 + 0 = 48 ('0') carry = 0 s[0] = s[0] * 2 - 48 + 0 = 48 ('0') carry = 0 ------------------- 0 : carry = 0 s[2] = s[2] * 2 - 48 + 0 = 58 (':') 此時 s[j] > '9' -> s[j] -= 10, carry = 1 carry = 1 s[1] = s[1] * 2 - 48 + 0 = 49 ('1') carry = 0 s[0] = s[0] * 2 - 48 + 0 = 48 ('0') carry = 0 ------------------- 由上述得到了字串 10 ``` #### `bn_add` 根據 a 與 b 的正負號決定應該去到哪個判斷式 `bn_do_add` 或是 `bn_do_sub` sign = 0 : 正 sign = 1 : 負 #### `bn_msb` 與 `bn_clz` 找出 bn 結構體中的 number 的 msb 和 clz 。 #### `bn_do_add` 與 `bn_do_sub` 這兩個函式是 `bn_add` 與 `bn_sub` 的輔助函式 而 `bn_do_add` 會先調整 c 的 size,確保可以裝得下兩者相加的值後,再進行相加 下面這段程式碼是進行相加的程式碼,可以看到他使用了 `carry` 這樣的 64 bit 大小的變數來進行儲存,接著由 number 的小到大開始進行相加,在這邊有可能會讓原本的 unsigned int 產生溢位 (overflow),因此才選用 64 bit 的變數大小來存 ```c unsigned long long int carry = 0; for (int i = 0; i < c->size; i++) { unsigned int tmp1 = (i < a->size) ? a->number[i] : 0; unsigned int tmp2 = (i < b->size) ? b->number[i] : 0; carry += (unsigned long long int) tmp1 + tmp2; c->number[i] = carry; carry >>= 32; } if (!c->number[c->size - 1] && c->size > 1) bn_resize(c, c->size - 1); ``` `bn_do_sub` 也是一樣的流程與想法,但是因為在進行相減的時候可能會產生負值,因此會使用 long long int 來取代上述的 uint64_t #### `bn_mult` 與 `bn_mult_add` 在這個運算之中,也是使用了輔助函式 `bn_mult_add` ,主要負責處理乘法的計算,他是用連加來取代了乘法 接著,為了通過 verify.py 這個自動測試程式,因此需要擴充 `excepted.txt` 到 500 個序列的答案以及修改 `verify.py`,讓其可以比對到第 500 個序列,完整程式碼詳見這個 [commit](https://github.com/WangHanChi/fibdrv/commit/3b3682419e92ac81d0b0c3bb4a6a44c49d834c60) 第 500 個序列的答案,比對過網路上的答案是一致的 ```shell Reading from /dev/fibonacci at offset 500, returned the sequence 139423224561697880139724382870407283950070256587697307264108962948325571622863290691557658876222521294125. ``` 以及 `make check` 是否通過 ```shell $ make check make -C /lib/modules/5.19.0-35-generic/build M=/home/hank/linux2023/fibdrv modules make[1]: 進入目錄「/usr/src/linux-headers-5.19.0-35-generic」 warning: the compiler differs from the one used to build the kernel The kernel was built by: x86_64-linux-gnu-gcc (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0 You are using: gcc (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0 make[1]: 離開目錄「/usr/src/linux-headers-5.19.0-35-generic」 make unload make[1]: 進入目錄「/home/hank/linux2023/fibdrv」 sudo rmmod fibdrv || true >/dev/null rmmod: ERROR: Module fibdrv is not currently loaded make[1]: 離開目錄「/home/hank/linux2023/fibdrv」 make load make[1]: 進入目錄「/home/hank/linux2023/fibdrv」 sudo insmod fibdrv.ko make[1]: 離開目錄「/home/hank/linux2023/fibdrv」 sudo ./client > out make unload make[1]: 進入目錄「/home/hank/linux2023/fibdrv」 sudo rmmod fibdrv || true >/dev/null make[1]: 離開目錄「/home/hank/linux2023/fibdrv」 Passed [-] ``` 接著進行效能檢測 - Bignum 迭代版本 ![](https://i.imgur.com/He3zZvI.png) - Bignum fast doubling 版本 ![](https://i.imgur.com/G9lr4K7.png) 可以發現 fast_doubling 居然比起迭代版慢了不少,將兩張圖放在一起比較 ![](https://i.imgur.com/A6qfgnj.png) 於是檢查究竟是哪個部份出了問題,可以使用以下命令來進行效能分析 ``` $ sudo perf stat --repeat 5 -e cache-misses,cache-references,instructions,cycles ./plotsrc FILE=iteration $ sudo perf record -g --call-graph dwarf ./plotsrc FILE=iteration $ sudo perf report --stdio -g graph,0.5,caller > temp.txt ``` 首先先來觀察迭代版本的 ``` Performance counter stats for './plotsrc FILE=iteration' (5 runs): 57,230 cache-misses # 7.373 % of all cache refs ( +- 2.86% ) 733,337 cache-references ( +- 7.96% ) 154,304,388 instructions # 3.24 insn per cycle ( +- 0.01% ) 46,840,518 cycles ( +- 2.83% ) 0.01411 +- 0.00180 seconds time elapsed ( +- 12.72% ) ``` 再來是 fast_doubling 版本的 ``` 47,037 cache-misses # 5.419 % of all cache refs ( +- 5.00% ) 1,124,953 cache-references ( +- 6.56% ) 176,760,914 instructions # 2.99 insn per cycle ( +- 0.01% ) 64,174,685 cycles ( +- 2.34% ) 0.01764 +- 0.00158 seconds time elapsed ( +- 8.93% ) ``` 目前認為應該是 `bn_fib_fdoubling` 這邊的操作過於繁瑣並且在第 16 行應該要改成第 17 行的樣子才不會每次都需要進 31 次 for-loop,並且造成效能曲線出現近似橫線的情況 ```diff= void bn_fib_fdoubling(bn *dest, unsigned int n) { bn_resize(dest, 1); if (n < 2) { //Fib(0) = 0, Fib(1) = 1 dest->number[0] = n; return; } bn *f1 = dest; /* F(k) */ bn *f2 = bn_alloc(1); /* F(k+1) */ f1->number[0] = 0; f2->number[0] = 1; bn *k1 = bn_alloc(1); bn *k2 = bn_alloc(1); - for (unsigned int i = 1U << 31; i; i >>= 1) { + for (unsigned int i = 1U << (31 - __builtin_clz(n)); i; i >>= 1) { /* F(2k) = F(k) * [ 2 * F(k+1) – F(k) ] */ bn_cpy(k1, f2); bn_lshift(k1, 1); bn_sub(k1, f1, k1); bn_mult(k1, f1, k1); /* F(2k+1) = F(k)^2 + F(k+1)^2 */ bn_mult(f1, f1, f1); bn_mult(f2, f2, f2); bn_cpy(k2, f1); bn_add(k2, f2, k2); if (n & i) { bn_cpy(f1, k2); bn_cpy(f2, k1); bn_add(f2, k2, f2); } else { bn_cpy(f1, k1); bn_cpy(f2, k2); } } bn_free(f2); bn_free(k1); bn_free(k2); } ``` 修改之後可以發現效能提升了不少 ![](https://i.imgur.com/w3VRhhg.png) 接著可以再引入更進一步的優化 ### 引入更一步的優化 然後我發現在 [改善方案 1](https://hackmd.io/@sysprog/linux2023-fibdrv/%2F%40sysprog%2Flinux2023-fibdrv-d#%E6%94%B9%E5%96%84%E6%96%B9%E6%A1%88-1-%E6%94%B9%E5%AF%AB-bn_fib_fdoubling) 的 `bn_lshift3` 這個函式導致計算出錯的問題。 在計算到第 92 個數列的值的話,會發生計算錯的部份,並且**非常弔詭**的是在第 93 個數列的值又會是正確的,因此打算深入探討一下究竟那部份出錯。 ``` +++ scripts/expected.txt 2023-03-18 19:28:16.081718903 +0800 @@ -90,7 +90,7 @@ Reading from /dev/fibonacci at offset 89, returned the sequence 1779979416004714189. Reading from /dev/fibonacci at offset 90, returned the sequence 2880067194370816120. Reading from /dev/fibonacci at offset 91, returned the sequence 4660046610375530309. -Reading from /dev/fibonacci at offset 92, returned the sequence 33873975713800773233359507389. +Reading from /dev/fibonacci at offset 92, returned the sequence 7540113804746346429. Reading from /dev/fibonacci at offset 93, returned the sequence 12200160415121876738. Reading from /dev/fibonacci at offset 94, returned the sequence 19740274219868223167. Reading from /dev/fibonacci at offset 95, returned the sequence 31940434634990099905. ``` 這邊印出每個數列的 `bn 結構體` 中的 size 大小來進行分析,可以發現在第 92 size 用到 3 個 int 來進行儲存,但是到了 93 數列之後,就變回 2 個 int 來進行儲存。這明顯是有地方寫錯了 因此比較 `bn_fib_fdoubling` 與 `bn_fib_fdoubling_v1` 究竟區別在那後可以發現 `bn_lshift(f2, 1, k1);// k1 = 2 * F(k+1)` 這個函式是 `bn_fib_fdoubling` 所沒有用到的,因此詳細檢查這個 function。 可以發現這個部份是在做 shift 前的準備,判斷是否需要改變 size 的大小 但是下面程式碼的第 2 行有將 size 變大,但是沒有重新將 data 配置好,這邊就會導致在計算出錯,並且也只會讓這個數列的值出錯,下一個就會變成正確的了。 ```c= if (shift > z) { bn_resize(dest, src->size + 1); } else { bn_resize(dest, src->size); } ``` ``` [74119.000924] copy_to_use is not used [74119.000924] the int from str is : 5 [74119.000927] Using bn_fib_fdoubling_v1 in 91 2 [74119.000928] copy_to_use is not used [74119.000928] the int from str is : 5 [74119.000933] Using bn_fib_fdoubling_v1 in 92 3 [74119.000934] copy_to_use is not used [74119.000935] the int from str is : 5 [74119.000938] Using bn_fib_fdoubling_v1 in 93 2 [74119.000939] copy_to_use is not used [74119.000939] the int from str is : 5 [74119.000944] Using bn_fib_fdoubling_v1 in 94 3 ``` 因此這邊有兩個解決的方法,最簡單的就是將 `bn_lshift3` 這個有問題的函式註解,並且拆成兩個原本的函式代替。如下面這樣 ```diff void bn_fib_fdoubling_v1(bn *dest, unsigned int n) { ... for (unsigned int i = 1U << (31 - __builtin_clz(n)); i; i >>= 1) { /* F(2k) = F(k) * [ 2 * F(k+1) – F(k) ] */ /* F(2k+1) = F(k)^2 + F(k+1)^2 */ - // bn_lshift3(f2, 1, k1); // k1 = 2 * F(k+1) + bn_cpy(k1, f2); + bn_lshift2(k1, 1); bn_sub(k1, f1, k1); // k1 = 2 * F(k+1) – F(k) bn_mult(k1, f1, k2); // k2 = k1 * f1 = F(2k) bn_mult(f1, f1, k1); // k1 = F(k)^2 bn_swap(f1, k2); // f1 <-> k2, f1 = F(2k) now bn_mult(f2, f2, k2); // k2 = F(k+1)^2 bn_add(k1, k2, f2); // f2 = f1^2 + f2^2 = F(2k+1) now if (n & i) { bn_swap(f1, f2); // f1 = F(2k+1) bn_add(f1, f2, f2); // f2 = F(2k+2) } } ... } ``` 或是修改 `lshift3` 的實作功能使其可以正確的放置數值,補上這行就可以正常運作了 `dest->number[src->size] = src->number[src->size - 1] >> (32 - shift);` ![](https://i.imgur.com/gkBuGW1.png) 與一般 bn_fib_fdoubling 相比還是快了一些 :::info 為了可以讓效能測試的步驟簡化,我將用來記錄時間並輸出成文字檔的 source file 移出 `client.c`,並且改名叫做 `plot.c` 接著使用 Enumeration 列舉來定義不同的演算法 並且可以簡單的使用 `$ make help` `$ make plot FILE=0` `$ make cmpplot FILE=1+2+3` 來取得單一演算法的性能折線圖 這樣的做法可以快速的加入新的演算法而不用做過多的修改,也可以避免掉之前每換一個新的方法的時候,其他的都要註解或是刪除的尷尬情況。 ::: 先觀察優化程式碼到目前的效果如何 ![](https://i.imgur.com/ASOOoWR.png) 可以看到目前最新的優化版本 `bn_fib_fdoubling_v1` 的版本可以算到 500 位之後,也可以用蠻快的速度計算完成。 接下來用 `perf` 工具來進行分析,看看還有哪裡可以進行效能改進的 ```shell $ sudo sh -c "echo -1 > /proc/sys/kernel/perf_event_paranoid" $ sudo perf stat --repeat 2000 -e cache-misses,cache-references,instructions,cycles ./plotsrc FILE=5 ... Performance counter stats for './plotsrc FILE=5' (2000 runs): 46,104 cache-misses # 6.055 % of all cache refs ( +- 0.16% ) 653,304 cache-references ( +- 0.26% ) 14,196,142 instructions # 1.51 insn per cycle ( +- 0.01% ) 7,349,874 cycles ( +- 0.46% ) 0.004036 +- 0.000128 seconds time elapsed ( +- 3.17% ) ``` ### 引入 memory pool 再來引入 memory pool 來進行效能的優化 首先先來了解一下什麼是 memory pool memory pool 是一個管理記憶體的方式之一,通常用於需要大量頻繁分配和釋放記憶體的應用程式,例如網路伺服器、資料庫伺服器和圖形應用程式等。 使用 memory pool 的優點是可以減少頻繁的系統呼叫,因為在 memory pool 中分配和釋放的開銷通常比在作業系統中進行分配和釋放更小。 而使用 memory pool 的另一個好處是可以減少記憶體碎片化的風險。當大量的記憶體被頻繁地分配和釋放時,會產生許多小的未使用的記憶體片段,這些片段可能無法滿足更大的分配要求。使用 memory pool 可以使分配和釋放 memory block 變得更有效率,從而減少記憶體碎片化的風險。 更多詳細的說明可以往這邊找 [Memory pool](https://en.wikipedia.org/wiki/Memory_pool) 或是問問 chatGPT 。 在 [你所不知道的 C 語言:記憶體管理、對齊及硬體特性](https://hackmd.io/@sysprog/c-memory#%E4%BD%A0%E6%89%80%E4%B8%8D%E7%9F%A5%E9%81%93%E7%9A%84-C-%E8%AA%9E%E8%A8%80%EF%BC%9A%E8%A8%98%E6%86%B6%E9%AB%94%E7%AE%A1%E7%90%86%E3%80%81%E5%B0%8D%E9%BD%8A%E5%8F%8A%E7%A1%AC%E9%AB%94%E7%89%B9%E6%80%A7) 這裡有提到如何區分大的 memory 物件或是小的 memory 物件 - 大request(>= 512 bytes) : 使用best-fit (只找到大小剛好的或者最小卻剛好夠的,但可能會有壞處就是造成了剩下一點小小的別人也無法使用) 的策略,在一個 range(bin)的 list 中尋找 - First-fit : 第一個找到空間夠大的就放進去 - Best-fit : 只找到大小剛好的或者最小卻剛好夠的,但可能會有壞處就是造成了剩下一點小小的別人也無法使用 - Worst-fit : 找空間最大的記憶體區段來放入 - 小request(<= 64 bytes) : caching allocation,保留一系列固定大小區塊的 list 以利迅速回收再使用 - 在兩者之間 : 試著結合兩種方法,有每個 size 的 list (小的特性),也有合併(coalesce)機制、double linked list(大) - 極大的 request(>= 128 KB by default) : 直接使用 mmap(),讓 memory management 解決 而我們每次所請求的記憶體大小都會在 4 ~ 8 個 bytes,所以使用的方法會是 **caching allocation**,也因此採用 memory pool 的記憶體管理方式。 接下來看回到所要進行優化的程式碼 會先把結構體新增一個欄位存放 需要 allocate 的大小 ```diff typedef struct _bn { unsigned int *number; /* ptr to number */ unsigned int size; /* length of number */ + unsigned int capacity; /* total allocated length, size <= capacity */ int sign; } bn; ``` 接著設定 memory pool 初始的大小與 之後每塊所分配的大小,可以看到老師在最一開始就都設定為 4,看是我想說若是設定的大一些是否可以加快一點點效能 所以會跑兩張圖,分別是 初始大小 `INIT_ALLOC_SIZE` 為 4 與 8 的效能圖 ```c #define INIT_ALLOC_SIZE 4 #define ALLOC_CHUNK_SIZE 4 ``` - INIT_ALLOC_SIZE = 4 / ALLOC_CHUNK_SIZE = 4 ![](https://i.imgur.com/T9J2f3z.png) - INIT_ALLOC_SIZE = 8 / ALLOC_CHUNK_SIZE = 4 ![](https://i.imgur.com/50fCsd2.png) - INIT_ALLOC_SIZE = 4 / ALLOC_CHUNK_SIZE = 8 ![](https://i.imgur.com/fk25zX2.png) - INIT_ALLOC_SIZE = 8 / ALLOC_CHUNK_SIZE = 8 ![](https://i.imgur.com/xeygh8v.png) 可以發現將 `INIT_ALLOC_SIZE` 設定為 8 確實可以加速運作大約 35 % (從第 500 項的時間來進行比較),我推測是為了編譯器 align 64 bit 接來看看 perf graph ```shell # To display the perf.data header info, please use --header/--header-only options. # # # Total Lost Samples: 0 # # Samples: 36 of event 'cycles' # Event count (approx.): 41929955 # # Children Self Command Shared Object Symbol # ........ ........ ....... ................. .................................. # 99.68% 0.00% plotsrc [kernel.kallsyms] [k] entry_SYSCALL_64_after_hwframe | ---entry_SYSCALL_64_after_hwframe do_syscall_64 | |--89.19%--__x64_sys_read | ksys_read | vfs_read | fib_read | | | |--71.42%--bn_to_string | | | |--12.87%--bn_fib_fdoubling_v1 | | | | | |--10.04%--bn_mult | | | | | --2.84%--bn_add | | bn_do_add | | bn_resize | | | --4.90%--_printk | vprintk | vprintk_default | vprintk_emit | vprintk_store | prb_reserve | |--4.24%--__x64_sys_write | ksys_write | vfs_write | new_sync_write | tty_write | file_tty_write.constprop.0 | do_tty_write | |--3.34%--__x64_sys_execve | do_execveat_common.isra.0 | bprm_execve | bprm_execve.part.0 | exec_binprm | search_binary_handler | load_elf_binary | load_elf_interp.constprop.0 | elf_map | vm_mmap | vm_mmap_pgoff | do_mmap | mmap_region | vma_link | __vma_link_file | vma_interval_tree_insert | --2.92%--syscall_exit_to_user_mode exit_to_user_mode_prepare ``` 可以看到目前最花時間的步驟是 `bn_to_string` 這個轉換的過程,從程式碼也可以看到它用了三個 for loop ,確實是會佔用蠻大一部份的時間的,如果可以將這個部份優化,應該可以將效能進一步的提昇的。 ## 參考資料 - [fibdrv](https://hackmd.io/@sysprog/linux2023-fibdrv/%2F%40sysprog%2Flinux2023-fibdrv-b#-%E6%92%B0%E5%AF%AB-Linux-%E6%A0%B8%E5%BF%83%E6%A8%A1%E7%B5%84) - [linux](https://github.com/torvalds/linux) - [Linux性能优化(十五)——CPU绑定](https://blog.51cto.com/quantfabric/2594336) - [KYG-yaya573142-fibdrv](https://hackmd.io/@KYWeng/rkGdultSU)