--- tags: linux2022 --- # 2022q1 Homework3 (fibdrv) contributed by < `ibat10clw` > > [fibdrv](https://github.com/ibat10clw/fibdrv) ## 費氏數列 ### 版本一 (DP) 為理解 client 和 fibdrv 之間的互動,我的第一個版本只是將 `f[i] = f[i-1] + f[i-2]` 以字元陣列儲存,並使用字元加法做改寫,同時根據作業說明中 clinet.c 的提示使用 `copy_to_user` 將 fibdrv 計算結果的字元陣列取出。 ```c static void add(char *a, char *b, char *out) { int len_a = strlen(a); int len_b = strlen(b); int i = 0; int carry = 0; int diff = len_a - len_b; for (i = len_b - 1; i >= 0; i--) { int sum = (a[i + diff] - '0') + (b[i] - '0') + carry; out[i + diff] = '0' + sum % 10; carry = sum / 10; } for (i = diff - 1; i >= 0; i--) { int sum = (a[i] - '0') + carry; out[i] = '0' + sum % 10; carry = sum / 10; } if (carry) { if (diff == 0) len_a++; for (i = len_a; i >= 0; i--) { out[i + 1] = out[i]; } out[len_a + 1] = 0; out[0] = '0' + carry; } out[len_a] = 0; } ``` 加法部份是將傳入的字元陣列以較短的為準對齊,依序從最右邊相加到比較短陣列結束後,同時需要紀錄當前的相加是否需要進位,若有進位需求的話,則需要把結果累計到下一次的計算,再把剩下的數字填入 buf。 ```c static long my_fibo(int k, char *buf) { char f3[SIZE]; char f2[SIZE] = "1"; char f1[SIZE] = "0"; for (int i = 2; i <= k; ++i) { add(f2, f1, f3); strncpy(f1, f2, strlen(f2) + 1); strncpy(f2, f3, strlen(f3) + 1); } if (k == 0) { copy_to_user(buf, f1, strlen(f1) + 1); return 0; } if (k == 1) { copy_to_user(buf, f2, strlen(f2) + 1); return 1; } copy_to_user(buf, f3, strlen(f3) + 1); return k; } ``` 關於費氏數列的計算是以 DP 的方式依序將 `f(n-1)` 與 `f(n-2)` 相加後把結果存到 `f3` ,之後分別把 `f2` 和 `f3` 複製給 `f1` 和 `f2`以便進行下一輪的計算,而 buf 是由 `bif_read` 中的參數傳入的,因此最後只要將結果放入 `copy_to_user` 即可在 client 中將其印出。 ### 版本二 (Fast Doubling) 若要使用 fast doubling 的方法計算費氏數列還需要引入乘法與減法的操作 ```c void sub(char *a, char *b, char *out) { int len_a = strlen(a); int len_b = strlen(b); int i = 0; int borrow = 0; int idx = len_a - len_b; for (i = len_b - 1; i >= 0; i--) { int diff = (a[i + idx] - '0') - (b[i] - '0') - borrow; if (diff < 0) { diff += 10; borrow = 1; } else borrow = 0; out[i + idx] = '0' + diff; } for (i = idx - 1; i >= 0; i--) { int diff = (a[i] - '0') - borrow; if (diff < 0) { diff += 10; borrow = 1; } else borrow = 0; out[i] = '0' + diff; } out[len_a] = 0; i = 0; while (out[i] == '0') i++; if (i != 0) { for (int j = 0; j < len_a; ++j) out[j] = out[j + i]; } } ``` 減法與加法的想法是類似的,不過從需要檢查進位變成需要檢查借位,同樣的以較短的輸入為準對齊後從右邊開始計算回來,每個回合將對應的位數相減後判斷是否需要借位,借助著直式減法的思路,當需要借位時可以將當前的差 +10 後把前一個位數 -1 ,透過這種方法便可以達成以字元鎮列為儲存單位的減法 最後的 while 迴圈是判斷是否有 leading zero ,若有的話就將有效位數全部向前挪動 ```c void mul(char *a, char *b, char *out) { int len_a = strlen(a); int len_b = strlen(b); int N = len_a + len_b; int i; memset(out, '0', N); out[N] = 0; for (i = len_b - 1; i >= 0; i--) { for (int j = len_a - 1; j >= 0; j--) { int digit1 = a[j] - '0'; int digit2 = b[i] - '0'; int pos = 1 + i + j; int carry = out[pos] - '0'; int multiplication = digit1 * digit2 + carry; out[pos] = (multiplication % 10) + '0'; out[pos - 1] += (multiplication / 10); } } i = 0; while (out[i] == '0') i++; if (i != 0) { for (int j = 0; j < N; ++j) out[j] = out[j + i]; } } ``` 字元的乘法與前面的流程稍有不同,但一樣是模擬直式的計算流程取得結果 首先把兩字元陣列的長度相加,並初始化為 0 以儲存計算結果 接著兩層的 for 迴圈分別看過所有存在陣列內的數字,兩兩計算相乘的結果,同時每次計算時還得先找出要將結果填在 out 的哪個位置,若有進位的話則在將其填在 pos - 1 的地方,當所有的數字都看過後即可取得結果 最底下的 while 迴圈同樣是去除存在陣列中的 leading zero ```c= static long fast_fib(int k, char *buf) { if (k == 0) { copy_to_user(buf, "0", 2); return k; } if (k == 1) { copy_to_user(buf, "1", 2); return k; } int stack[100]; int top = 0; while (k > 1) { stack[top++] = k; k /= 2; } char f1[SIZE] = "1"; char f2[SIZE] = "1"; char tmp1[SIZE]; char tmp2[SIZE]; char res1[SIZE]; while (top--) { int n = stack[top]; // f1 = f1 * (2 * f2 - f1) mul("2", f2, tmp1); sub(tmp1, f1, tmp2); mul(tmp2, f1, res1); // f2 = f1 ** 2 + f2 ** 2 mul(f1, f1, tmp1); mul(f2, f2, tmp2); add(tmp2, tmp1, f2); if (n % 2 == 1) { add(f2, res1, tmp1); strncpy(f1, f2, strlen(f2) + 1); strncpy(f2, tmp1, strlen(tmp1) + 1); } else { strncpy(f1, res1, strlen(res1) + 1); } } copy_to_user(buf, f1, strlen(f1) + 1); return k; } ``` 在使用 fast doubling 的方法時需要根據當前的 fibonacci number 是偶數項或奇數項來決定下次的計算方法,因此這邊使用了一個 stack 來紀錄項數的奇偶,以便決定後續的計算方式。 當項數為奇數的時候,就用原始定義的 `fib(n) = fib(n-1) + fib(n-2)` 把數列往前推進一項,等待全部計算完畢後同樣使用 copy_to_user 將結果傳出。 ### 效能評測 根據作業說明的提示,可以使用 `ktime` 相關的 API 在 kernel 中紀錄計算 fibonacci 的時間,再搭配還沒有被使用上的 `fib_write` 從 client 中獲得紀錄的時間 ```c static long long fib_time_proxy(long long k, char *buf) { kt = ktime_get(); long long result = fast_fib(k, buf); kt = ktime_sub(ktime_get(), kt); return result; } ``` 在呼叫 fast_fib 的前後先計算時間差,之後在從 client 呼叫 write 得到結果 ```c for (int i = 0; i <= offset; i++) { lseek(fd, i, SEEK_SET); clock_gettime(CLOCK_REALTIME, &t1); sz = read(fd, buf, sizeof(buf)); clock_gettime(CLOCK_REALTIME, &t2); sz = write(fd, write_buf, strlen(write_buf)); printf("%d %lld %ld %lld\n", i, sz, t2.tv_nsec - t1.tv_nsec, (t2.tv_nsec - t1.tv_nsec) - sz);//k, u, k2u } ``` 此外在 client 中同樣在 read 前後各紀錄一次時間,就可以計算執行時間 而 `copy_to_user` 的開銷則可以透過總執行時間扣掉在 kernel 的時間,空缺的那一塊就是 `copy_to_user` 的開銷 ## 大數運算 上面的費氏數列實作雖然能計算出正確的答案,但由於使用字串的方式模擬 10 進位的運算,每次運算相當於只能處理 1 個位數,以現在 CPU 的規格來說並沒有完全發揮他的效能,因此先全面改寫運算的規則後再重新實作費氏數列。 ### 用於儲存大數的資料結構 ```c typedef struct _bn { unsigned int number[BNSIZE]; unsigned int size; } bn; ``` `unsigned int` 共可以儲存 32 bit 的資料,使用一個陣列儲存運算的結果,並且根據 size 來決定目前有效資料的長度,LSB 存於 index 0 的位置 示意圖如下: ![](https://i.imgur.com/uGeTfw1.png) 若要解釋以上資料則從 index 0 開使將數字以 2 進位的方式連接,可得以下結果: `00000000 00000000 000000001 11000011` 如此便可以將 2 進位的格式在輸出時轉換為 10 進位 ### 將大數以十進位的方式輸出 這個部份參考了 [KYG-yaya573142](https://hackmd.io/@KYWeng/rkGdultSU#%E5%A4%A7%E6%95%B8%E9%81%8B%E7%AE%97) 的實作 ```c= static char *bn_to_string(const bn *src) { // log10(x) = log2(x) / log2(10) ~= log2(x) / 3.322 size_t len = (8 * sizeof(int) * src->size) / 3 + 2; 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 long d = 1U << 31; d; d >>= 1) { // double logical not can make the result to 1 int carry = !!(d & src->number[i]); for (int j = len - 2; j >= 0; j--) { s[j] += s[j] - '0' + carry; carry = (s[j] > '9'); if (carry) s[j] -= 10; } } } // skip leading zero while (p[0] == '0' && p[1] != '\0') { p++; } memmove(s, p, strlen(p) + 1); return s; } ``` 一般將 2 進位的數字轉換為 10 進位時,可以借助下列公式 $\displaystyle\sum_{b=1}^{N} {2^N}\times b$ `for b = lsb to msb` 範例如下: `00000000 00000000 000000001 11000011` 根據 bit=1 所在的位置可以得出該 2 進位數的 10 進位表示為 $2^8+2^7+2^6+2^1+2^0$ 由於輸入的 bn 可能非常的大,因此需要直接將其轉為字元陣列的形式保存,演算法描述如下 1. 從 bn 的 MSB 開始讀過所有的有效位元(根據size取捨) * 對應程式碼第 10 行的 for 迴圈 3. 由於 bn 的每個欄位是 32 bit,因此使用 32 bit 的無號數對每個 bit 做 bitwise and 判斷是否為1,若出現的是 1 就將字元陣列 +1 * 對應程式碼 11 行的 for 迴圈 5. 從初始化字元陣列的尾巴開始把每個位數扣掉 ASCII 的偏移後與自己相加,相當於乘以 2 的效果,由於是從 MSB 開始做轉換,當第一個 bit 1 出現到轉換結束執行的次數恰好能將其恢復成 power of 2,同時也需要判斷字元陣列表示的十進位數是否需要進位 * 對應程式碼 14 行的 for 迴圈 如此便能夠在輸出時將 bn 轉化為十進位表示的字串,並且透過 `copy_to_user` 將結果傳到 userspace ### 大數加法 bn 的儲存格式在選擇時使用了 unsigned integer,unsigned integer 在 overflow 發生的時候會將結果 `mod` 該型態的最大值後再做保存,根據這個特性只需要判斷是否發生 overflow,若發生的話就將 bn 的下一個欄位 +1 來做進位,如此便能完成大數加法 ```c= #define MAXSLOT 0xffffffffUL+1UL static void add_bn(bn *a, bn *b, bn *c) { unsigned long carry = 0; int i; c->size = a->size; for (i = 0; i < b->size; ++i) { unsigned int sum = a->number[i] + b->number[i] + carry; carry = ((unsigned long) a->number[i] + b->number[i] + carry) >= MAXSLOT; c->number[i] = sum; } for (; i < a->size; ++i) { unsigned int sum = a->number[i] + carry; carry = ((unsigned long) a->number[i] + carry) >= MAXSLOT; c->number[i] = sum; } if (carry) { c->size++; c->number[i]++; } } ``` 其中 MAXSLOT 定義為 `UINT_MAX+1` 是為了 `branchless` 的實作,只要當前要相加的 bn 和 carry 加在一起後判斷是否會超過 `MAXSLOT` 若會的話則代表 overflow 發生了,這時就要將 carry 設為 1,這部份的操作則是透過 logical operator 來達成