# 2023q1 Homework3 (fibdrv) contributed by <`kart81604`> ## 開發環境 ```shell $ gcc -version gcc (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0 $ 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-7700HQ CPU @ 2.80GHz CPU family: 6 Model: 158 Thread(s) per core: 2 Core(s) per socket: 4 Socket(s): 1 Stepping: 9 CPU max MHz: 3800.0000 CPU min MHz: 800.0000 BogoMIPS: 5599.85 ``` ## 修改 `fibdrv.c` 執行 `make file` 會出現 ``` ... f(93) fail input: 7540113804746346429 expected: 12200160415121876738 ``` 表示與預期的不同,輸出應為下面的值,先照著[作業說明](https://hackmd.io/@sysprog/linux2023-fibdrv/%2F%40sysprog%2Flinux2023-fibdrv-d#F92-%E4%BB%A5%E5%BE%8C%E7%9A%84%E6%95%B8%E5%80%BC%E9%8C%AF%E8%AA%A4%E7%9A%84%E5%8E%9F%E5%9B%A0)將 `MAX_LENGTH` 從 92 更改至 93 , `fib_sequence` 的回傳值從 `long long` 更改至 `uint64_t` ,再執行 `make check` 預期 f(93) 應跑出正確的數字。 out 檔如下 ``` Reading from /dev/fibonacci at offset 92, returned the sequence 7540113804746346429. Reading from /dev/fibonacci at offset 93, returned the sequence -6246583658587674878. ``` 仍然 overflow ,再將 `client.c` 作修改。 ```diff - long long sz; + u_int64_t sz; ... - printf("... %lld ..."); + printf("... %llu ..."); ``` f(93) 就能輸出正確值 ``` Reading from /dev/fibonacci at offset 92, returned the sequence 7540113804746346429. Reading from /dev/fibonacci at offset 93, returned the sequence 12200160415121876738. ``` 引入 [big number](https://github.com/sysprog21/bignum/blob/master/bignum.c#L114) 並研讀此方法,使其能顯示出超過 64 位元的二進位數字。 ```c= /* * output bn to decimal string * Note: the returned string should be freed with kfree() */ char *bn_to_string(bn src) { // log10(x) = log2(x) / log2(10) ~= log2(x) / 3.322 size_t len = (8 * sizeof(int) * src.size) / 3 + 2 + src.sign; char *s = kmalloc(len, GFP_KERNEL); char *p = s; memset(s, '0', len - 1); s[len - 1] = '\0'; for (int i = src.size - 1; i >= 0; i--) { for (unsigned int d = 1U << 31; d; d >>= 1) { /* binary -> decimal string */ int carry = !!(d & src.number[i]); for (int j = len - 2; j >= 0; j--) { s[j] += s[j] - '0' + carry; // double it carry = (s[j] > '9'); if (carry) s[j] -= 10; } } } // skip leading zero while (p[0] == '0' && p[1] != '\0') { p++; } if (src.sign) *(--p) = '-'; memmove(s, p, strlen(p) + 1); return s; } ``` 主要實現顯示大數的函式 `bn_to_string` ,因沒有資料型態能夠顯示出超過一定位元的數字,因此將儲存至 char 型別的陣列之中。 其運作方式如下,考慮二進位表示 10101001 ,則十進位表示為 169 ,可以發現,要將1、6、9 這三個數字儲存至 cahr 陣列 `s` , `s[0]` 儲存 `1` , `s[1]` 儲存 `6` , `s[2]` 儲存 `9`,我們希望是得到以上的結果,陣列中,靠前(位子接近 0)的位子,儲存十進位中較高的位元。 觀察函式 16 行的 for 迴圈,因為前幾 bits 皆為 0 ,不會有任何行為,我們從開始碰到 1 開始討論,以下是過程。 ``` v v v v v v 10101001 10101001 10101001 10101001 10101001 10101001 s[0][1][2] s[0][1][2] s[0][1][2] s[0][1][2] s[0][1][2] s[0][1][2] 0 0 1 0 0 2 0 0 5 0 1 0 0 2 1 0 4 2 v v 10101001 10101001 s[0][1][2] s[0][1][2] 0 8 4 1 6 9 ``` 可以發現,第一次碰到的 1 ,會通過第 20 行來進行乘以 2 的行為,如果需要進位,則透過第 21 ~ 23 行來處理,這行為會做剩餘還未處理的二進位的個數次數,因此第一個 1 會乘以 2 七次,也就會得到 128 ,為我們需要的數字,透過這方法,後面的數字就同理,就能使用 char 型別陣列來表示十進位數字。 透過 `bn` 結構,已能印出 f(1000) 。 ### 分析執行時間 ```diff + static ktime_t kt; /* calculate the fibonacci number at given offset */ static ssize_t fib_read(struct file *file, char *buf, size_t size, loff_t *offset) { bn *fib = bn_alloc(1); + kt = ktime_get(); bn_fib_fdoubling(fib, *offset); + kt = ktime_sub(ktime_get(), kt); char *s = bn_to_string(*fib); copy_to_user(buf, s, strlen(s) + 1); bn_free(fib); - return strlen(s) + 1; + return ktime_to_ns(kt); } ``` 在執行 `bn_fib_fdoubling` 前後加入 `kt` 的計算,可以得到執行運算的時間,在透過 `client` 呼叫, `bn_fib_fdoubling` 的執行時間入下圖所示。 ![](https://i.imgur.com/jdAu9Kf.png) 有許多峰值,~~但最令人疑惑的是運算時間沒有隨著 n 值成長。(也許我該好好的審視一下大數運算)~~ 關於時間沒有隨著 n 值成長,是因為 `bn_fib_fdoubling` 不管 n 是多少,裡面的 for 迴圈都會執行 32 次,因此所有 n 值的情況下,執行時間都差不多。 (猜測)而出現峰值的時候應該是執行了 `bn_resize` 。 將 `bn_to_string` 時間也考慮進去後,得到下圖。 ![](https://i.imgur.com/PWhdA6p.png) 可以得知運算耗時的地方在於將 2 進位轉換成字串的運算上。 透過作業提供的 [script](https://github.com/colinyoyo26/fibdrv/commit/4ce43c4e7ee0b80c4ce9e6dcd995bbfdf239206c) 來求出 kernel 與 user 端的效能表現。 ![](https://i.imgur.com/weJV7ol.png)