# 2023q1 Homework3 (fibdrv) contributed by < [LiChiiiii](https://github.com/LiChiiiii/fibdrv) > > 題目 - [L04: fibdrv](https://hackmd.io/@sysprog/linux2023-fibdrv/%2F%40sysprog%2Flinux2023-fibdrv-a) >:::spoiler 開發環境 > ```shell > $ gcc --version > gcc (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.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): 8 > On-line CPU(s) list: 0-7 > Vendor ID: GenuineIntel > Model name: Intel(R) Core(TM) i7-6700 CPU @ 3.40GHz > CPU family: 6 > Model: 94 > Thread(s) per core: 2 > Core(s) per socket: 4 > Socket(s): 1 > Stepping: 3 > CPU max MHz: 4000.0000 > CPU min MHz: 800.0000 > BogoMIPS: 6799.81 > ``` > ::: ## 修正 $Fibonacci()$ 的缺陷 ### 加速計算 $Fibonacci()$ 原本的程式碼是利用 dynamic programming 來計算,時間複雜度為 $O(n)$,空間複雜度為 $O(n)$ 。因此改為使用 [clz / ctz](https://en.wikipedia.org/wiki/Find_first_set) 一類的指令,搭配 [Fast Doubling](https://chunminchang.github.io/blog/post/calculating-fibonacci-numbers-by-fast-doubling) 使得時間複雜度降到 $O(logn)$,空間複雜度降到 $O(1)$ 。 原本的費氏數列定義為: $$ \begin{split} F(k-1) &= F(k+1) - F(k) \end{split} $$ 經過矩陣的推導後,可得到對於奇數和偶數的不同算法: $$ \begin{split} F(2k) &= F(k)[2F(k+1) - F(k)] \\ F(2k+1) &= F(k+1)^2+F(k)^2 \end{split} $$ 根據這個公式,每次進入 `recursive` 的數字一定是前一個數字的一半,而且 `recursive tree` 不會有增長,每次呼叫只會重複呼叫自己一次而已。 #### 程式碼運作原理 使用 `__builtin_clzll` 來計算有多少 leading 0s。 遇到 `0` : 求 $F(2n)$ 和 $F(2n+1)$ 遇到 `1` : 求 $F(2n)$ 和 $F(2n+1)$ ,再求 $F(2n+2)$ ==(第 15 行的條件式)== ```c= static long long fib_sequence(long long k) { long long a = 0, b = 1; long long temp1, temp2; int num_bits = sizeof(k) * 8 - __builtin_clzll(k); for (int i = num_bits; i >= 1; i--) { temp1 = a * (2 * b - a); temp2 = b * b + a * a; a = temp1; b = temp2; // Check the i-th bit of k if ((k >> (i - 1)) & 1) { temp1 = a + b; a = b; b = temp1; } } return a; } ``` 以 $F(6)$ 為例: 6~10~ = 110~2~ | i | start | 3 | 2 | 1 | result | | ---- | ----- |:---------------:|:---------------:|:---------------:|:------:| | n | - | ==1==10 | 1==1==0 | 11==0== | - | | F(n) | F(0) | F(0*1+1) = F(1) | F(2*1+1) = F(3) | F(2*3) = F(6) | F(6) | | a | 0 | 1 | 2 | 8 | 8 | | F(n) | F(1) | F(0*1+2) = F(2) | F(2*1+2) = F(4) | F(2*3+1) = F(7) | - | | b | 1 | 1 | 3 | 13 | - | ```graphviz strict digraph G { 1[label="F(6)"] 2[label="F(3)"] 3[label="F(4)"] 4[label="F(1)", style=filled] 5[label="F(2)", style=filled] 6[label=" " ,color=white] 7[label=" " ,color=white] {rank = same; 2;3;} {rank = same; 4;5;} 1 -> {2, 3} 2 -> {4, 5} 2 -> 3 [style=dashed; arrowhead=vee] 5 -> 3 [style=dashed; arrowhead=vee] 3 -> {6, 7} [color=white] } ``` >參考 [L04: fibdrv](https://hackmd.io/@sysprog/linux2023-fibdrv/%2F%40sysprog%2Flinux2023-fibdrv-a) ### 計算 $F(93)$ (包含) 之後的 Fibonacci 數 #### 自定義結構以計算大數 原始程式碼以 `long long` 來儲存費氏數列的計算結果,但是在 $F(93)$ 之後的運算會發生 overflow,導致無法正確地計算結果。因此自行定義結構 `BigN` 來儲存 128 位元的整數: ```c typedef struct BigN { unsigned long long lower, upper; }BigN; ``` 其中 `lower` 表示低位的 64 位元, `upper` 表示高位的 64 位元。 :::spoiler 除了定義結構外,還定義其他函數來實作 `BigN` 結構體的運算,包含加法運算、減法運算、乘法運算、右移 1 bit、左移 1 bit。其中**BigN的乘法運算**使用左移右移和加法運算來實作,減少原乘法運算的成本。 #### 加法運算 各自計算兩個 64 位元整數的和,若 `res.lower < a.lower` 則要進位 `upper` 。 ```c BigN BigN_add(BigN a, BigN b) { BigN res; res.lower = a.lower + b.lower; res.upper = a.upper + b.upper + (res.lower < a.lower); return res; } ``` #### 減法運算 各自計算兩個 64 位元整數的差,若 `a.lower < b.lower` 則要借位 `upper` 。 ```c BigN BigN_sub(BigN a, BigN b) { BigN res; res.lower = a.lower - b.lower; res.upper = a.upper - b.upper - (a.lower < b.lower); return res; } ``` #### 乘法運算 使用了一個 `while` 循環來將 `b` 不斷右移,同時將 `a` 左移。在每次循環中,如果 `b` 的最低位是1,則將 `a` 加入到 res 中,最終得到 `a` 和 `b` 的乘積 `res`。 ```c BigN BigN_multi(BigN a,BigN b) { BigN res = {0, 0}; while (b.lower || b.upper) { if (b.lower & 1) { res = BigN_add(res, a); } a = BigN_shift_left(a); b = BigN_shift_right(b); } return res; } ``` #### 右移 1 bit ```c BigN BigN_shift_right(BigN a) { BigN res; res.upper = a.upper >> 1; res.lower = (a.lower >> 1) | (a.upper << 63); return res; } ``` #### 左移 1 bit ```c BigN BigN_shift_left(BigN a) { struct BigN res; res.lower = a.lower << 1; res.upper = (a.upper << 1) | (a.lower >> 63); return res; } ``` ::: 最後利用上述自定義的結構和函數來更改程式碼: ```c static BigN fib_sequence(long long k) { BigN a = {0, 0}; BigN b = {1, 0}; // Get the number of binary digits in k int num_bits = sizeof(k) * 8 - __builtin_clzll(k); for (int i = num_bits; i >= 1; i--) { BigN temp1 = BigN_multi(a , BigN_sub(BigN_shift_left(b), a)); BigN temp2 = BigN_add(BigN_multi(b,b) , BigN_multi(a,a)); a = temp1; b = temp2; // Check the i-th bit of k if ((k >> (i - 1)) & 1) { temp1 = BigN_add(a,b); a = b; b = temp1; } } return a; } ``` ---- #### 由 client 端印出結果 原始程式碼在 fibdrv.c 中的 `fib_read()` 回傳計算出的 Fibonacci 數,最初想法是將 `fib_read()` 回傳的 `ssize_t` 的型態更改成自定義的 `BigN` 傳給 client ,但是不能隨意更改 [read](https://linux.die.net/man/2/read) ,且在使用者模式的位址空間配置一個 buffer 空間時,核心裝置驅動不能直接寫入該地址,因此使用到 [copy_to_user](https://elixir.bootlin.com/linux/latest/ident/copy_to_user) ,將想傳給使用者模式 (即運作中的 client) 的值複製到到`fib_read()` 的 buf 參數後,client 端方可接收到此值並印出。 ##### fibdrv.c ```diff -- static BigN fib_sequence(long long k) ++ static long long fib_sequence(long long k, char *buf) { BigN a = {0, 0}; BigN b = {1, 0}; // Get the number of binary digits in k int num_bits = sizeof(k) * 8 - __builtin_clzll(k); for (int i = num_bits; i >= 1; i--) { BigN temp1 = BigN_multi(a , BigN_sub(BigN_shift_left(b), a)); BigN temp2 = BigN_add(BigN_multi(b,b) , BigN_multi(a,a)); a = temp1; b = temp2; // Check the i-th bit of k if ((k >> (i - 1)) & 1) { temp1 = BigN_add(a,b); a = b; b = temp1; } } ++ int test = __copy_to_user(buf, &a , sizeof(struct BigN)); ++ if (test) { ++ printk("The copy from kernel to user is fail."); ++ return -1; ++ } -- return a; ++ return sizeof(a); } ``` ##### client.c ```c unsigned long long buf[2]; ... for (int i = 0; i <= offset; i++) { lseek(fd, i, SEEK_SET); sz = read(fd, buf, sizeof(buf)); printf("Reading from " FIB_DEV " at offset %d, returned the sequence " "%016llu , %016llu.\n", i, buf[1],buf[0]); } ``` ---- #### 開發過程紀錄 上述程式碼修正過後發現 $F(93)$ 可以得到正確的數值,但是 $F(94)$ 之後依然是錯誤的。那是因為在 `BigN_add()` 中的 `res.lower = a.lower + b.lower` 發生了 overflow 。 ```c= BigN BigN_add(BigN a, BigN b) { BigN res; res.lower = a.lower + b.lower; res.upper = a.upper + b.upper + (res.lower < a.lower); return res; } ``` 我嘗試加上 `(a.lower+b.lower) < a.lower` 判斷低 64 位元在相加時是否發生 overflow ,若發生則宣告 128 位元的 `temp` 存放相加後的值,再將 `temp & 0xFFFFFFFFFFFFFFFF` 取出低 64 位元,並右移 64 位元取得進位的值(第 8 、 9 行)。 ```c= BigN BigN_add(struct BigN a, struct BigN b) { BigN res = {0,0}; int carry = 0; if((a.lower+b.lower) < a.lower) { unsigned __int128 temp = (unsigned __int128)a.lower + b.lower; res.lower = temp & 0xFFFFFFFFFFFFFFFF; carry = temp >> 64; } ... return res; } ``` 修正完的結果依然是錯誤的。 舉例來說, $F(94)$ = 19740274219868223167 ,在 `BigN_add()` 希望可以得到 `res.upper = 1` 和 `res.lower = 9740274219868223167`,但 `19740274219868223167` 在二進制取出的低 64 位元得到的會是 `1293530146158671551` 。 因此我嘗試將第 8 行改成使用 mod 來得到我想要的餘數,卻出現以下錯誤訊息: ```shell ERROR: modpost: "__umodti3" [/home/lichi/Documents/fibdrv/fibdrv.ko] undefined! ERROR: modpost: "__modti3" [/home/lichi/Documents/fibdrv/fibdrv.ko] undefined! ``` 顯然必須避免使用 128 位無符號整數取模運算。 我嘗試將 upper 轉成 __int128 的型態並左移 64 bits(就是乘上 $2^{64}$ 的意思),接著將 upper 和 lower 轉成字串之後相加,來避免 overflow 的問題。最後把欲回傳運算結果轉成字串後存入 buf ,再由 client 印出存在 buf 的字串。 :::spoiler 定義函數實作 128 位元無號整數轉成十進位字串以及將兩個十進位字串相加 **字串反轉** ```c void reverse_str(char *str, int len) { int start = 0; int end = len - 1; while (start < end) { char temp = str[start]; str[start] = str[end]; str[end] = temp; start++; end--; } } ``` **128 位元的整數轉成字串** ```c char *uint128_to_string(__int128 num, char *buffer, int bufferSize) { if (num == 0) { buffer[0] = '0'; buffer[1] = '\0'; return buffer; } int i = 0; while (num > 0) { unsigned long remainder = num % 10; buffer[i++] = '0' + remainder; num /= 10; } buffer[i] = '\0'; reverse_str(buffer, i); return buffer; } ``` **兩個字串相加** ```c char *add_str(const char *str1, const char *str2) { int len1 = strlen(str1); int len2 = strlen(str2); int maxLen = len1 > len2 ? len1 : len2; int carry = 0; char *result = (char *) malloc(maxLen + 2); // 分配足夠的空間來存儲結果,包括結束符 '\0' if (result == NULL) { return NULL; } result[maxLen + 1] = '\0'; for (int i = 0; i < maxLen; i++) { int digit1 = i < len1 ? str1[len1 - 1 - i] - '0' : 0; int digit2 = i < len2 ? str2[len2 - 1 - i] - '0' : 0; int sum = digit1 + digit2 + carry; result[maxLen - i] = (sum % 10) + '0'; carry = sum / 10; } if (carry) { result[0] = carry + '0'; } else { memmove(result, result + 1, maxLen); // 移除首位的 0,如果存在 result[maxLen] = '\0'; // 縮小結果字串的長度 } return result; } ``` ::: ```c static long long fib_sequence(long long k, char *buf) { ... char buf_1[41]; char buf_2[41]; char *lower = uint128_to_string((uint128_t)a.lower , buf_1, sizeof(buf_1)); char *upper = uint128_to_string((uint128_t)a.upper << 64 , buf_2, sizeof(buf_2)); char *result = add_str(upper, lower); int len = strlen(result)+1; int test = __copy_to_user(buf, result, len); if (test) { printk("The copy from kernel to user is fail."); return -1; } return len; } ``` <!-- 我試著將自行定義的 `BigN` 改成使用 [GCC __int128](https://gcc.gnu.org/onlinedocs/gcc/_005f_005fint128.html) 型態,程式碼如下: ```c typedef unsigned __int128 uint128_t; static long long fib_sequence(long long k, char *buf) { uint128_t a = 0; uint128_t b = 1; int num_bits = sizeof(k) * 8 - __builtin_clzll(k); for (int i = num_bits; i >= 1; i--) { uint128_t temp1 = a * ((b<<1)-a); uint128_t temp2 = b*b + a*a ; a = temp1; b = temp2; if ((k >> (i - 1)) & 1) { temp1 = a + b; a = b; b = temp1; } } int test = __copy_to_user(buf, &a , sizeof(uint128_t)); if (test) { printk("The copy from kernel to user is fail."); return -1; } return sizeof(a); } ``` 直接使用兩個 128 位元的變數來做運算,雖然解決了在加法中出現的溢位問題,但是在第 21 行將運算結果存入 buf 時,一樣被切成高 64 位元及低 64 位元,也就是 $F(94)$ 運算結果的低 64 位元依然印出 `1293530146158671551` 而不是正確的 `9740274219868223167` 。 因此我嘗試將最後欲回傳運算結果轉成字串後存入 buf ,再由 client 印出存在 buf 的字串: ```c static long long fib_sequence(long long k, char *buf) { ... char buffer[41]; char *str = uint128_to_string(a, buffer, sizeof(buffer)); int len = strlen(str)+1; int test = __copy_to_user(buf, str , len); if (test) { printk("The copy from kernel to user is fail."); return -1; } return len; } ``` --> 更改結果成功印出正確數值。 ```shell 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. Reading from /dev/fibonacci at offset 96, returned the sequence 51680708854858323072. Reading from /dev/fibonacci at offset 97, returned the sequence 83621143489848422977. Reading from /dev/fibonacci at offset 98, returned the sequence 135301852344706746049. Reading from /dev/fibonacci at offset 99, returned the sequence 218922995834555169026. Reading from /dev/fibonacci at offset 100, returned the sequence 354224848179261915075. ``` ## 時間測量和效能分析 使用 `ktime` 在核心模組中測量執行時間,發現 `fib_write` 此暫時沒作用,因此在此函式實作並回傳 **kernel space** 的執行時間。 ```c static ssize_t fib_write(struct file *file, const char *buf, size_t size, loff_t *offset) { ktime_t start, kt; start = ktime_get(); fib_sequence(*offset, buf); kt = ktime_sub(ktime_get(), start); return ktime_to_ns(kt); } ``` 使用 `clock_gettime` 計算在呼叫 `fib_write` 時所需的時間,可得到 **user space** 的執行時間。兩數相減即可得到 **system call** 的時間。 ```c for (int i = 0; i <= offset; i++) { struct timespec start, end; clock_gettime(CLOCK_REALTIME, &start); long kernel_time = write(fd, buf, 1); clock_gettime(CLOCK_REALTIME, &end); long user_time = end.tv_nsec - start.tv_nsec; printf("%d ", i); printf("user space execute time: %ld ns,", user_time); printf("kernel space execute time : %ld ns,", kernel_time); printf("system call execute time : %ld ns \n", user_time - kernel_time); } ``` ![](https://i.imgur.com/YpIb6mT.png) #### 用統計手法去除極端值 導入 [driver.py](https://github.com/colinyoyo26/fibdrv/commit/4ce43c4e7ee0b80c4ce9e6dcd995bbfdf239206c) 去除 95% 區間之外的值,可以把圖中的尖端消除掉。 ![](https://i.imgur.com/o75Cho4.png) <!-- ## 自我檢查清單 - [ ] 研讀上述 ==Linux 效能分析的提示== 描述,在自己的實體電腦運作 GNU/Linux,做好必要的設定和準備工作 $\to$ 從中也該理解為何不希望在虛擬機器中進行實驗; - [ ] 研讀上述費氏數列相關材料 (包含論文),摘錄關鍵手法,並思考 [clz / ctz](https://en.wikipedia.org/wiki/Fst_set) 一類的指令對 Fibonacci 數運算的幫助。請列出關鍵程式碼並解說 - [ ] 複習 C 語言 [數值系統](https://hackmd.io/@sysprog/c-s) 和 [bitwise operatips://hackmd.io/@sysprog/c-bitwise),思考 Fibonacci 數快速計算演算法的實作中如何減少乘法運算的成本; - [ ] 學習指出針對大數運算的加速運算和縮減記憶體操作成本的舉措,紀錄你的認知和疑惑 - [ ] 注意到 `fibdrv.c` 存在著 `DEFINE_MUTEX`, `mutex_tr `mutex_init`, `mutex_unlock`, `mutex_destroy` 等字樣,什麼場景中會需要呢?撰寫多執行緒的 ue 程式來測試,觀察 Linux 核心模組若沒用到 mutex,到底會發生什麼問題。嘗試撰寫使用 [POSIX Thread](https://en.wikipedia.org/wiki/POSIX_Threads) 的程式碼來確認。 $\to$ 搭配閱讀〈[並行和多執行緒程式設計](https://hackmd.io/@sysprog/concurrency)〉 -->