contributed by < WangHanChi
>
Linux kernel 與 Ubuntu 的版本如下:
本段落參考自 Virtual Machines: A Closer Look at Pros and Cons 與 Containers vs Virtual Machines (VMs) 與 什麼是虛擬機器 (VM)?
如果不想要幫電腦安裝雙系統又想要使用 linux 的話,通常會有兩種用法
以下將分開介紹
虛擬機 (VM)
容器
這邊的步驟與老師筆記的步驟相同
得到以下結果
接著進入 /usr/src/linux-headers-5.19.0-35-generic/
,可以發現目錄夾下的目錄也跟文章中的一致
接著使用一個簡單的 Hello, world 來測試 LKM
程式碼中有許多可以討論的地方:
module_param(name, type, permissions)
,主要在載入 module 時帶參數進去,三個參數分別是變數名稱、變數的資料型別,以及其對應 sysfs
檔案的權限,詳細的 moduleparam 可以 linux/moduleparam.h 中找到。而這邊所設定的權限 S_IRUGO
代表的是允許用戶/組/其他人進行讀取訪問module_init
, module_exit
(48, 49) 行printk
功能與一般的 printf 相似,只是它額外需要傳遞 log levels 進去,log levels 定義在 linux/kern_levels.h,而 printk 的定義在 linux/printk.hprintk(KERN_INFO .....)
一致,詳情可以參考這裡這個 makefile 的第一行 obj-m+=hello.o
定義了要構件的模組 obj-m
定義一個可加載模組目標
如果 make
成功的話,就可以在資料夾下面看到多了一個名為 hello 的可加載的模組,其副檔名為 .ko
使用以下命令來查看所有核心模組的權限
接著使用以下命令來插入 (insert) 模組
接著輸入以下命令可以看到剛做的 module 在上面
也可以使用以下命令來查看模組資訊
最後要移除模組的話,可以使用以下命令
最後可以觀察 kernel log 來看看是否真的有紀錄
在剛剛的 hello.c 中,我們有包含了一個自定義參數,它允許在初始化時將參數傳遞給核心模組,可以使用以下命令來進行測試
然後再使用以下命令來觀察 log 輸出
也可以使用以下命令來得到核心模組在記憶體中的偏移量
最後可以用以下命令來觀察輸出結果
這個程式使用了一個簡單的 char 驅動程式,可以用於在 Linux user space 程序和運行在 Linux 核心空間中的可加載核心模組 (LKM) 之間傳遞資訊。在 user space 應用程序向 LKM 發送一個字符串。LKM 然後用發送的消息以及發送的消息包含的字母數進行響應。
設備驅動程式有一個關聯的主要和次要編號。例如,/dev/ram0
和 /dev/null
與主設備號為 1 的驅動程式相關聯,而 /dev/tty0
和 /dev/ttyS0
與主設備號為 4 的驅動程式相關聯。
例如下面的部份,可以看到 ttyS0
, ttyS1
與 ttyS10
都是與主設備號為 4 的驅動程式相關
等等的程式會使用到 file_operations 中的一些功能,詳細的內容可看 linux/fs.h
這個驅動程式有一個 class 名字(ebb)也有一個 device 的名字(ebbchar)
這邊有關於這些程式的一些要點
先 trace 完這個在 User space 執行的程式
可以看到在這個部份定義了 Fibonacci 的上限為 100
接著在開啟在 /dev/fibonacci
的核心模組,注意這邊使用了 open
,他並不是使用一般的系統呼叫 open(2) 而是被修改成為 vfs 的 open 了,詳情可以參考老師的筆記
以下是 VFS 所定義的系統呼叫
這段程式碼可以發現,它會執行到我們所設定的 offset ,並且重複輸出
printf("Writing to " FIB_DEV ", returned the sequence %lld\n", sz);
主要就是用來測試寫入核心模組的,可以看到它也是修改了系統呼叫中的 write
來完成,因此現在變成了 vfs 的 write 了
這段主要修改系統呼叫 read 成 vfs 的 read 去讀取 fibonacci 中的值,最後迭代將其印出來
這段程式碼也與上面的概念相同,只是從大到小,上面的是由小到大
這邊使用到了 Fast Doubling 的方法來進行加速,並且會與最一開始的版本進行效能比較
為了計算運算的時間,我們引入了 ktime 來進行時間的測量,並且透過目前沒有用到的 wirte
回傳時間資訊
再來使用老師在作業提示的 Fast Doubling 方法,進行計算,藉此比較與原本方法的效能差異
由於要輸出時間資訊,並且會製成折線圖,因此會在 client.c 添加寫檔的相關程式碼,詳情請見 commit
接著因為接下來需要多次進行效能測試,因此寫了一個腳本來方便測試
此腳本修改自 colinyoyo26,主要因為了我們在 client.c 中所輸出的檔案不同,因此針對檔案的讀取以及統計方法進行了修改,詳情可參考 commit
接著再對 Makefile 進行添加 make plot
即可
接著將 CPU 中的幾顆核心保留,參照這篇文章所提供的方法 Linux性能優化(十五)——CPU綁定
修改 GRUB_CMDLINE_LINUX
這行,使其變成 GRUB_CMDLINE_LINUX="isolcpus=0,1"
,孤立了兩個 CPU 的執行緒
2. 更新 grub
檢查一下是否成功孤立 CPU 中的第 0, 1 執行緒,可以看到它從 2 開始
有個有趣的實驗,因為我的顯卡目前是 nvidia 1050ti ,不支援 av1 影片格式的硬解,所以從 youtube播放 8K 的影片會讓 CPU 滿載,但是因為先前孤立了 CPU0 與 CPU1,所以可以從系統監控看到這兩個執行緒的負載基本上都是0。
8K youtube 影片
再來排除干擾效能分析的因素
至於為何要設定這個的原因,可以從 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 boost 的敘述可以觀看這裡 Intel Turbo Boost
但因為我的處理器是 AMD 的 5600X,因此可以從這邊觀察是否有開啟 AMD Turbo Core
可以看到我的 CPU 有支持,但是沒有啟用
最後可以把出這樣的結果圖
TODO : 思考為何 Fast Doubling 的方法會抖動的這麼嚴重,也許是我哪邊做錯了
但是以上方法只能計算到第 93 個序列而已,所以需要修改,至少要讓其可以計算到 500 個序列
使用以下資料結構來定義數值
接著修改核心模組,使用 copy_to_user
這個函式將 kernel space 的資料拷貝到 user space ,避免直接使用到指標指到 kernel 的記憶體,導致程式被 killed
。 copy_to_user
的使用方法可以從這邊看,
然後可以檢查到它輸出到第 186 個序列的時候都是正確的,超過之後還是會 overflow
而且可以發現到他的計算方式為逗號 ,
前面的數字乘上 unsigned long long 的上界 (18446744073709551615) 再加上逗號後面的數字,就是這個 index 的值,以 184 為例
序列結果應為
\(6891616170087056899*18446744073709551615+10993266775918486379\)
結果應為 \(127127879743834334146972278486287885163\)
以計算機計算上式的結果為 \(1.271278797*10^{38}\) ,比對後為正確的
但是這樣遠遠不到 500 的序列,雖然可以透過在 struct 新增 unsigne long long
,但是這樣治標不致本,並且也沒有可讀性,因此需要更換一個新的方法
這邊先引入大數運算所需要的標頭檔以及來源檔,詳情可以參照 老師的提示,並且我將分別放在 fibdrv/inc
以及 fibdrv/src
下,避免版面太亂,完整程式碼詳見這個 commit。
bn_to_string
接著看到 bn_to_string
這個函式,這個函式比較難理解,主要就是將二進制轉成十進制的字串
舉一個簡單的例子來解釋,將 1010
轉成十進制會得到 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 的變數大小來存
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
第 500 個序列的答案,比對過網路上的答案是一致的
以及 make check
是否通過
接著進行效能檢測
可以發現 fast_doubling 居然比起迭代版慢了不少,將兩張圖放在一起比較
於是檢查究竟是哪個部份出了問題,可以使用以下命令來進行效能分析
首先先來觀察迭代版本的
再來是 fast_doubling 版本的
目前認為應該是 bn_fib_fdoubling
這邊的操作過於繁瑣並且在第 16 行應該要改成第 17 行的樣子才不會每次都需要進 31 次 for-loop,並且造成效能曲線出現近似橫線的情況
修改之後可以發現效能提升了不少
接著可以再引入更進一步的優化
然後我發現在 改善方案 1 的 bn_lshift3
這個函式導致計算出錯的問題。
在計算到第 92 個數列的值的話,會發生計算錯的部份,並且非常弔詭的是在第 93 個數列的值又會是正確的,因此打算深入探討一下究竟那部份出錯。
這邊印出每個數列的 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 配置好,這邊就會導致在計算出錯,並且也只會讓這個數列的值出錯,下一個就會變成正確的了。
因此這邊有兩個解決的方法,最簡單的就是將 bn_lshift3
這個有問題的函式註解,並且拆成兩個原本的函式代替。如下面這樣
或是修改 lshift3
的實作功能使其可以正確的放置數值,補上這行就可以正常運作了 dest->number[src->size] = src->number[src->size - 1] >> (32 - shift);
與一般 bn_fib_fdoubling 相比還是快了一些
為了可以讓效能測試的步驟簡化,我將用來記錄時間並輸出成文字檔的 source file 移出 client.c
,並且改名叫做 plot.c
接著使用 Enumeration 列舉來定義不同的演算法
並且可以簡單的使用
$ make help
$ make plot FILE=0
$ make cmpplot FILE=1+2+3
來取得單一演算法的性能折線圖
這樣的做法可以快速的加入新的演算法而不用做過多的修改,也可以避免掉之前每換一個新的方法的時候,其他的都要註解或是刪除的尷尬情況。
先觀察優化程式碼到目前的效果如何
可以看到目前最新的優化版本 bn_fib_fdoubling_v1
的版本可以算到 500 位之後,也可以用蠻快的速度計算完成。
接下來用 perf
工具來進行分析,看看還有哪裡可以進行效能改進的
再來引入 memory pool 來進行效能的優化
首先先來了解一下什麼是 memory pool
memory pool 是一個管理記憶體的方式之一,通常用於需要大量頻繁分配和釋放記憶體的應用程式,例如網路伺服器、資料庫伺服器和圖形應用程式等。
使用 memory pool 的優點是可以減少頻繁的系統呼叫,因為在 memory pool 中分配和釋放的開銷通常比在作業系統中進行分配和釋放更小。
而使用 memory pool 的另一個好處是可以減少記憶體碎片化的風險。當大量的記憶體被頻繁地分配和釋放時,會產生許多小的未使用的記憶體片段,這些片段可能無法滿足更大的分配要求。使用 memory pool 可以使分配和釋放 memory block 變得更有效率,從而減少記憶體碎片化的風險。
更多詳細的說明可以往這邊找 Memory pool 或是問問 chatGPT 。
在 你所不知道的 C 語言:記憶體管理、對齊及硬體特性 這裡有提到如何區分大的 memory 物件或是小的 memory 物件
而我們每次所請求的記憶體大小都會在 4 ~ 8 個 bytes,所以使用的方法會是 caching allocation,也因此採用 memory pool 的記憶體管理方式。
接下來看回到所要進行優化的程式碼
會先把結構體新增一個欄位存放 需要 allocate 的大小
接著設定 memory pool 初始的大小與 之後每塊所分配的大小,可以看到老師在最一開始就都設定為 4,看是我想說若是設定的大一些是否可以加快一點點效能
所以會跑兩張圖,分別是 初始大小 INIT_ALLOC_SIZE
為 4 與 8 的效能圖
INIT_ALLOC_SIZE = 4 / ALLOC_CHUNK_SIZE = 4
INIT_ALLOC_SIZE = 8 / ALLOC_CHUNK_SIZE = 4
INIT_ALLOC_SIZE = 4 / ALLOC_CHUNK_SIZE = 8
INIT_ALLOC_SIZE = 8 / ALLOC_CHUNK_SIZE = 8
可以發現將 INIT_ALLOC_SIZE
設定為 8 確實可以加速運作大約 35 % (從第 500 項的時間來進行比較),我推測是為了編譯器 align 64 bit
接來看看 perf graph
可以看到目前最花時間的步驟是 bn_to_string
這個轉換的過程,從程式碼也可以看到它用了三個 for loop ,確實是會佔用蠻大一部份的時間的,如果可以將這個部份優化,應該可以將效能進一步的提昇的。