contributed by < ericlai1021
>
fibdrv.ko
核心模組得到以下輸出
fibdrv.ko
核心模組在 Linux 核心掛載後的行為/dev
目錄下可以發現的確有一個名為 fibonacci
的 character device繼續觀察
可以注意到 509
這個數字,不同電腦設備會產生不同數字,試著對照 fibdrv.c
,發現關鍵在 alloc_chrdev_region() , 此函式會向 linux kernel 申請設備號。
這和 fibdrv.c
透過 MODULE_VERSION
所指定的版本號碼相同。
這兩道命令的輸出都是 0,意味著目前的 reference counting。
fibdrv.ko
核心模組後,執行 client
並觀察輸出可以發現在 offset 大於 92 之後都會得到同樣的值,猜測是因為 overflow ,於是先去查看 client.c
,基本上該檔案只是對 /dev/fibonacci
此裝置做開檔、讀檔、寫檔的動作,所以 fibonacci sequence 的計算都在 fibdrv.c
檔案內,馬上去查看,發現以下程式碼
再往下看到 fib_device_lseek()
,此函式中會判斷欲找尋的位置是否大於 MAX_LENGTH
,若大於則回傳 MAX_LENGTH
,因此要讀取大於 F(92) 的數值都會讀到 F(92)。
參考 作業說明 (三) 當中的設定
使用 isolcpus
這個 Linux 核心起始參數,修改 /etc/default/grub
設定檔,在 GRUB_CMDLINE_LINUX_DEFAULT
欄位加入 isolcpus=0
使開機時保留第 0 個 CPU 核給那些被 taskset 特別指定的行程使用。
修改完輸入 sudo update-grub
以更新設定,重新開機後確認設定是否生效。
可以發現第 0 個 CPU 確實被保留下來,再使用 taskset
查看 PID=1 的行程的處理器親和性,此時已經不包含第 0 個 CPU。
使用 ktimer 相關的 API 來量測時間。 改寫 fibdrv.c
當中的 fib_read()
和 fib_write()
兩個函式。
首先宣告一個 ktime_t
的變數:
針對 fib_read()
的修改如下:
針對 fib_write()
的修改如下:
我們可發現到 write 在此核心模組暫時沒作用,所以可以用來輸出上一次執行 fib_sequence() 的時間。
在 client.c
中先用 lseek(fd, i, SEEK_SET) 將檔案的讀寫位置指向特定位置 (即第 i 個位置),接著再用 read() 取得 Fib(i) 的數值,最後再使用 write() 取得計算 Fib(i) 的時間,相對應的程式碼如下:
參考作業說明 (一) 、 (四) 以迭代方式實作 bottom-up Fast Doubling。
使用 Fast Doubling 可以得出以下公式
對應的虛擬碼如下:
搭配 bitwise 操作求得 number of binary digit in n
以及判斷 current binary digit
是否為 1。
long long
,所以首先將 sizeof(long long)
向左位移 3 個位元即可求得 long long
所需的位元數,接著再減去 n 的 leading 0-bits 數量就可以求得 n 的二進制位數。count++
, 最後 (sizeof(long long) << 3) - count
即為 n 的 leading 0-bits 數量。可以發現 Fast Doubling 在計算 Fib(0) 時會花費大量時間,猜測是跟使用 clz 指令有關,目前正在做實驗研究,若我們將 y 軸範圍縮小到 2500 ns 會得到以下結果
即便尚未排除干擾效能的因素,仍可以看到 Fast Doubling 明顯優於遞迴方式
fib_sequence()
的一開始加上提前中止的條件處理
抑制 address space layout randomization (ASLR)
設定 scaling_governor 為 performance。準備以下 shell script,檔名為 performance.sh
:
之後再用 $ sudo sh performance.sh
執行
針對 Intel 處理器,關閉 turbo mode
關閉 SMT (Intel 稱它為 Hyper-Threading)
可以看出來兩種方式的曲線都明顯平滑許多
可以看到使用 clz 指令對計算費氏數列有顯著的改善。
參考作業說明 (四) 初步支援大數運算
為了能正確計算 92 項以後的費氏數列,引入長度可變動的數值表示法,藉由動態配置不同大小記憶體來呈現更大範圍的整數,結構體的定義如下
值得注意的地方是,我透過觀察發現即便是使用 fast doubling 在計算費氏數列的過程中,所有會用到自定義結構體 bn
宣告的變數 (e.g., a
, b
, t1
, t2
) 皆為非負整數,所以我就沒有定義 sign
來表示數值的正負。
number
: 指向無號整數陣列的開頭size
: 配置給 number array 的大小,單位為 sizeof(unsigned int)
初步嘗試使用大數運算來實作迭代作法的費氏數列計算,首先會需要加入以下功能
bn_to_string
,將 bn
型態變數轉換成字串bn_add
,將兩個 bn
型態變數相加bn_cpy
,將一個 bn
型態變數複製到另一個 bn
型態變數(即做 assign 的動作)在講解以上功能之前要先介紹支援 bn
型態的記憶體配置方法以及釋放方法,這邊為了能更加彈性的使用 bn
結構體,因此皆採用動態配置記憶體的方式來使用 bn
結構體
先配置空間給 bn
結構體,再配置 sizeof(unsigned int) * size
的大小給 number
指標變數,最後將所有數值初始化為 0
參考教材 kmalloc vs. vmalloc 及網路材料 difference between kmalloc and vmalloc 後選擇使用 kmalloc 來配置記憶體是因為 kmalloc 會配置實體連續的記憶體空間,接著就可以簡單的使用 memset
來初始化此空間,但當沒有足夠的實體記憶體空間時 kmalloc 就會回傳錯誤,通常是回傳 NULL
依序將 number
以及 bn
結構體釋放
由於大數沒辦法直接以數值的形式列出,這裡改用字串來呈現,轉換的概念很簡單,就是將大數除以 10 取餘數取得個位數,接著再將大數更新為商數,不斷重複此步驟直到大數為 0
實作加法的概念很直觀,就是從 lsb 開始相加,將進位的部份加到下一輪的相加結果
使用 memcpy
將一個 bn
結構體的數值部份複製到另一個 bn
結構體的數值部份
使用大數運算實作 fast doubling 會需要額外實作以下功能
bn_sub
,將兩個 bn
型態變數相減bn_lshift
,將 bn
型態變數向左位移bn_mult
,將兩個 bn
型態變數相乘實作減法的概念類似加法,一樣從 lsb 開始相減,若相減結果為負數,則向下一位借位
對 bn
結構體左移 shift
個 bit ,因為左移超過 32 bits 的處理相對複雜,因此就先做出比較簡單的版本
實作大數乘法的概念類似硬體乘法器 (如下圖),從 multiplier
的 lsb 開始判斷,若當前 bit 為 1 ,則將 multiplicand
加至 product
,再將 product
往左位移 1 bit ,重複上述直到 multiplier
的判斷到達 msb 為止
使用以上實作出來的大數 API 來正確計算 92 項以後的費氏數列,程式邏輯皆參照原始的實作,首先展示迭代演算法
接著是 fast doubling 演算法的實作
使用 /scripts/verify.py 來驗證,至少能夠正確計算至第 3000 項
後續改進皆更新在 2023期末專題