contributed by < OscarShiang
>
linux2020
lsmod
的輸出結果有一欄名為 Used by
,這是 "each module's use count and a list of referring modules",但如何實作出來呢?模組間的相依性和實際使用次數 (reference counting) 在 Linux 核心如何追蹤呢?fibdrv.c
存在著 DEFINE_MUTEX
, mutex_trylock
, mutex_init
, mutex_unlock
, mutex_destroy
等字樣,什麼場景中會需要呢?撰寫多執行緒的 userspace 程式來測試,觀察 Linux 核心模組若沒用到 mutex,到底會發生什麼問題Fibonacci 數的定義如下:
而在程式中的實作為
假設計算 F(6) 的時候,最後程式呼叫的結果就會變成下圖:
在該展開圖裡,F(4), F(3), F(2) 在遞迴的過程中就會被重複計算,很顯然這樣的計算效率非常的差。根據 Vorobev 等式,我們可以將 Fibonacci 數的計算方式整理成下列形式:
而 與 則變成
所以 與 就可以表示為
只要我們有 ,這樣的整理可以避免逐項計算 Fibonacci 數所花費的時間:
同時因為電腦數值系統的特性,可以將其轉換為利用數值的 bit 來進行判斷:
0
: 計算 與 1
: 計算 與 後再計算 為了在 fibdrv 這個核心模組裡面測量使用原本的遞迴方式計算 fibonacci 與使用 Vorobev 等式計算 fibonacci 的時間差異,並進行分析
以下使用下列 fast doubling 的函式進行測試
根據 The Linux Kernel Documentation 中所描述 ktime 的 API
我將 fib_read
這個函式改寫
在執行 User Space 的 client
程式後利用終端機指令 dmesg
查看 fibdrv
的輸出
將這些實驗的數據以 gnuplot 繪製成圖表
在圖表中可以看到,兩者之間的差異在第 30 項前的差異並不大,但在第 30 項後差異持續擴大
根據 task switch 的描述:
In computing, a context switch is the process of storing the state of a process or thread, so that it can be restored and resume execution at a later point.
所以在進行上述實驗的時候就會導致 tail recursive 的花費時間分佈有抖動的情況發生
為了避免這樣的情況,我將函式改到 User Space ,並以 time.h
中的 struct timespec
計時
並透過 taskset
將行程綁定在第 0 個 CPU 上執行
最後將實驗結果繪製成下圖
從這張圖中可以看到,時間花費數值跳動的情況已經改善許多
透過設定 /boot/grub/grub.cfg
更改 bootloader 的參數,將開機指令 linux
後面加上 isolcpus=0
將第 0 個 CPU 排除在開機程序之外
重新開機後,確認第 0 個 CPU 已經被排除成功
利用 taskset
設定測試程式執行於第 0 個 CPU 上,測試結果如下
可以發現在限定使用特定 CPU 來進行實驗時,實驗所造成的誤差與干擾與在完全不調整的情況下相差非常多
為了避免單一實驗之極端值所造成的實驗誤差,我以 Python 腳本將實驗的過程與資料的整理自動化
重複實驗 次後利用 95% 的信賴區間統計資料,得到下圖
但不是所有的整數都會將其記憶體的 32 個位子填滿,所有我們可以利用 clz 計算 leading zeros 的個數,讓我們可以直接從數字的有效位元開始計算,而不需要一直判斷不需要的 leading zeros
根據 GCC 內建函式的描述,在 GCC 中就有直接可以使用的 clz / ctz 功能,所以我們可以在程式中透過 macro 將 clz 引進程式中
並利用 clz 先將 leading zeros 計算出來,作為 for 迴圈的中止點
根據 GCC 內建函式對於 __builtin_clz
回傳值的說明
Returns the number of leading 0-bits in x, starting at the most significant bit position. If x is 0, the result is undefined.
所以為避免發生 undefined value 產生,在 n = 0
時直接回傳 0
因為 n = 0
的情況在計算的時候只會發生 2 次,所以利用 unlikely
巨集 (在 Linux 核心原始程式碼也存在) 提示 compiler 產生對 CPU 分支預測友善的程式碼。
將 unlikely
加入程式中
並將 n = 0
的判斷加入 unlikely
的標示
與前面的實驗相同,本程式一樣使用 taskset
綁定來進行測量,而實驗結果如下圖所示
在上圖的實驗中,有 41 項 Fibonacci 數的計算是使用 clz 改善後的版本較佳,所以根據實驗結果可以知道 clz 的確能加速 fast doubling 的計算
透過重複實驗降低實驗誤差與極端值後結果呈現下圖
在進行 make check
時,會發現測試在第 93 項時會發生錯誤
利用測試程式輸出計算結果後發現在第 92 項之後出現 Overflow 的問題
因為上述的情況發生在使用 long long
時,表示使用內建的整數型態以無法符合我們的要求,所以需要改以自定義的 struct
來改進結構
在 struct
的設計與運算實作上我參考〈Bignums Alrithmetic〉進行設計,使用 usigned int *
來存取一個動態長度的陣列
對照 quiz4,你可看到透過 small buffer optimization 手法實作的 vector,也能作為上述動態長度陣列用途,這也是隨堂測驗的規劃。
:notes: jserv
接著依序將加法、減法與陣列的調整操作實作出來
加法的部分,我參考〈Bignums Alrithmetic〉所提及的做法,利用 uint64_t
保留每個元素相加之後的進位。最後利用 15 ~ 20 行的 for 迴圈確認陣列的實際長度並將陣列的長度進行調整
但因為在 GCC 裡沒有對應 64 bit 以上 int
型態位元轉換的實作,所以我參考 How to convert a 128-bit integer to a decimal ascii string in C? 討論中,將大數轉換成十進位的字串形式,方便之後輸出運算的結果
利用 bitwise 的運算將十進位的每位數字算出來後存進 str
,最後利用 27 行的迴圈計算 str
中未使用的數量,最後利用 malloc
取得一塊記憶體空間,並將 str
中有效的元素利用 memcpy
將十進位的字串指標 p
回傳回去
因為使用 fast doubling 計算 fibonacci 數的做法有使用到乘法,所以我使用 Karatsuba algorithm 來實作大數的乘法
因為 Karatsuba 用到 bitwise 的左、右位移,所以依需求實作出大數位元的位移
右位移在實作上先處理超過 32 位元的位移,利用 memcpy
將整個陣列進行位移,當處理完超過 32 位元以上的位移後,處理小於 32 位元的位移,利用 mask 擷取跨陣列元素位移的位元,並將其利用 |=
放在對應的位置上,完成整個大數的位移。
左位移在實作上則與右位移有些不同:左位移首先處理小於 32 位元的位移,並利用 clz
檢查是否會導致 msb
溢位的問題產生,若會產生問題,則調整陣列大小,並將 msb
移動到新的陣列元素上
接著再進行超過 32 位元的位移,一樣利用 memcpy
進行陣列元素長度的移動,最後將陣列大小寫入回 out->size
完成位移
完成左右位移的建構之後就可以實作出 Karatsuba 的乘法了
最後將 bn
的結構引進測試程式中,並改寫 fibonacci 的計算函式,測試結果如下:
與 The first 300 Fibonacci numbers 的資料比對後確認計算結果無誤,並將其引進 fibdrv
中
將 fib_sequence
進行改寫
因為最後的計算結果會以字串的方式回傳回來,所以將 fib_read
改寫為使用 linux/uaccess.h
所提供的 copy_to_user
函式
但因為在 The Linux Kernel API 中並沒有使用者層級常用的 malloc
, free
, memcpy
等函式,所以將 bign.c
加上 macro 改寫原先使用 stdlib.h
所進行的記憶體管理
可比照 memory.h,對記憶體操作函式包裝,你可以提供兩種版本,一個是對應到 <stdlib.h>
,另一個則是 kmalloc, kzalloc 一類 Linux 核心函式。
:notes: jserv
好的,已新增
memory.h
來包裝記憶體管理函式
Oscar[color= orange]
在新增 memory.h
後,嘗試將 bign.c
, bign.c
與 memory.h
在編譯過程中與 fibdrv.ko
連結起來,卻得到下列結果
直接將 bign.c
中的所有函式定義直接加入 fibdrv.c
中進行編譯,但在執行 client
程式時,在計算 Fibonacci 數時電腦當機。
利用 Valgrind 觀察使用者層級的測試程式,推測是記憶體管理問題導致問題產生