# 2022q1 Homework3 (fibdrv)
contributed by < `freshLiver` >
###### tags: `linux2022` `kernel module` `fibonacci`
## 準備工作
### 測試環境
```shell
$ lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Byte Order: Little Endian
Address sizes: 39 bits physical, 48 bits virtual
CPU(s): 12
On-line CPU(s) list: 0-11
Thread(s) per core: 2
Core(s) per socket: 6
Socket(s): 1
NUMA node(s): 1
Vendor ID: GenuineIntel
CPU family: 6
Model: 167
Model name: 11th Gen Intel(R) Core(TM) i5-11400 @ 2.60GHz
Stepping: 1
CPU MHz: 665.263
CPU max MHz: 4400.0000
CPU min MHz: 800.0000
BogoMIPS: 5184.00
Virtualization: VT-x
L1d cache: 288 KiB
L1i cache: 192 KiB
L2 cache: 3 MiB
L3 cache: 12 MiB
NUMA node0 CPU(s): 0-11
```
### Kernel Headers
```json
"includePath": [
"${default}",
"/lib/modules/5.11.0-49-generic/build/arch/x86/include",
"/lib/modules/5.11.0-49-generic/build/arch/x86/include/generated",
"/lib/modules/5.11.0-49-generic/build/include",
"/lib/modules/5.11.0-49-generic/build/include/generated"
],
"defines": [
"MODULE",
"__KERNEL__"
]
```
VS Code 相關的設定 ( `.vscode/c_cpp_properties.json` ),可以避免編輯器找不到 Kernel Header 而跳出錯誤。
## fibdrv 實作
### 現有缺陷
#### Variable-length Array
如同註解所示,Linux Kernel 中不應該使用 C99 新增的 VLA 來宣告陣列,可以改用大小為 2 的陣列搭配 `swap` 巨集改寫:
```c
static long long fib_sequence(long long k)
{
long long f[2] = {0, 1};
for (int i = 1; i <= k; i++) {
swap(f[0], f[1]);
f[0] += f[1];
}
return f[0];
}
```
但這樣一樣會因為 64 位元的限制造成 `fib(93)` 後的結果仍是錯誤的。
### 更快速的 Fibonacci 運算實作
- [x] [費氏數列分析](https://hackmd.io/@sysprog/fibonacci-number)
- [ ] [Fibonacci and Golden Ratio Formulae](http://www.maths.surrey.ac.uk/hosted-sites/R.Knott/Fibonacci/fibFormulae.html)
- [x] [The Fibonacci Q-matrix](https://youtu.be/lTHVwsHJrG0)
- [x] [Fast Doubling](https://chunminchang.github.io/blog/post/calculating-fibonacci-numbers-by-fast-doubling)
#### 實作 Fast Doubling
使用 Fast Doubling 的規則加上 Bitwise 的應用進行遞迴版的實作:
```c
static inline uint64_t fast_doubling(uint32_t target)
{
if (target <= 2)
return !!target;
// fib(2n) = fib(n) * (2 * fib(n+1) − fib(n))
// fib(2n+1) = fib(n) * fib(n) + fib(n+1) * fib(n+1)
uint64_t n = fast_doubling(target >> 1);
uint64_t n1 = fast_doubling((target >> 1) + 1);
// check 2n or 2n+1
if (target & 1)
return n * n + n1 * n1;
return n * ((n1 << 1) - n);
}
```
但雖然減少了不少計算,但還是會有重複計算的部份:
```graphviz
digraph FIB {
"3a" [label="3"; color="red"]
"3b" [label="3"; color="red"]
"2a" [label="2"]
"2b" [label="2"]
"2c" [label="2"]
"1a" [label="1"]
"1b" [label="1"]
6 -> "3a";
6 -> 4;
"3a" -> "2a";
"3a" -> "1a";
4 -> "2b";
4 -> "3b";
"3b" -> "2c";
"3b" -> "1b";
}
```
而當 `target` 越大時重複的計算會造成的效能影響越大。
#### Bottom-up Fast Doubling
從一數的 2 進制表示可以發現該數是如何產生的,以 $87_{10}$ 為例:
```text
87 = 1010111 (87 >> i+1)
i = 0 : 43 = (1010111 - 1) << 1 = 101011
i = 1 : 21 = ( 101011 - 1) << 1 = 10101
i = 2 : 10 = ( 10101 - 1) << 1 = 1010
i = 3 : 5 = ( 1010 - 0) << 1 = 101
i = 4 : 2 = ( 101 - 1) << 1 = 10
i = 5 : 1 = ( 10 - 0) << 1 = 1
i = 6 : 0 = ( 1 - 1) << 1 = 0
^
87 的第 i 個位元
```
若是進行移項並反過來看的話會變成:
```text
(87 >> i+1)
i = 6 : 1 = 0 << 1 + 1 = 1
i = 5 : 2 = 1 << 1 + 0 = 10
i = 4 : 5 = 10 << 1 + 1 = 101
i = 3 : 10 = 101 << 1 + 0 = 1010
i = 2 : 21 = 1010 << 1 + 1 = 10101
i = 1 : 43 = 10101 << 1 + 1 = 101011
i = 0 : 87 = 101011 << 1 + 1 = 1010111
^
87 的第 i 個位元
```
從 $n=0$ 開始看,可以發現每次位移後只要檢查目標數對應的位元,就可以知道下次應以 $fib(2n)$ 還是 $fib(2n+1)$ 為基礎進行右移。
若以文字表示流程的話可以寫成:
1. 從最高位元的 1 開始,此時 $n=1$,而:
- $fib(n)=1$
- $fib(n+1)=1$
2. 若下一個位元不存在的話跳到第 3 步,否則(假設目前為 $n=k$):
- 透過 $fib(k)$ 以及 $fib(k+1)$ 計算 $fib(2k)$ 以及 $fib(2k+1)$
- 檢查下一個位元:
- 0:$n = 2k$
- 1:$n = 2k + 1$,此時需要 $fib(n+1)$ 讓下一迭代能夠計算 $fib(2n)$ 以及 $fib(2n+1)$
- 回到第 2 步
3. 此時 $n$ 為目標數,回傳 $fib(n)$
若寫成程式碼的話則會變成:
```c
static inline uint64_t fast_doubling_iter(uint64_t target)
{
if (target <= 2)
return !!target;
// find first 1
uint8_t count = 63 - __builtin_clzll(target);
uint64_t fib_n0 = 1, fib_n1 = 1;
for (uint64_t i = count, fib_2n0, fib_2n1; i-- > 0;) {
fib_2n0 = fib_n0 * ((fib_n1 << 1) - fib_n0);
fib_2n1 = fib_n0 * fib_n0 + fib_n1 * fib_n1;
if (target & (1UL << i)) {
fib_n0 = fib_2n1;
fib_n1 = fib_2n0 + fib_2n1;
} else {
fib_n0 = fib_2n0;
fib_n1 = fib_2n1;
}
}
return fib_n0;
}
```
:::warning
由於 cppcheck 會偵測區域變數的 scope 是否能夠再縮減,因此這邊將 `i` 一併宣告成 64 位元的無號整數,以便將 `for` 迴圈需要用到的區域變數一併宣告。
:::
而迴圈內 `if...else...` 的部份可以利用 `-!!(target & (1 << i))` 作為 mask 的技巧簡化成:
```c
static inline uint64_t fast_doubling_iter(uint64_t target)
{
if (target <= 2)
return !!target;
// find first 1
uint8_t count = 63 - __builtin_clzll(target);
uint64_t fib_n0 = 1, fib_n1 = 1;
for (uint64_t i = count, fib_2n0, fib_2n1, mask; i-- > 0;) {
fib_2n0 = fib_n0 * ((fib_n1 << 1) - fib_n0);
fib_2n1 = fib_n0 * fib_n0 + fib_n1 * fib_n1;
mask = -!!(target & (1UL << i));
fib_n0 = (fib_2n0 & ~mask) + (fib_2n1 & mask);
fib_n1 = (fib_2n0 & mask) + fib_2n1;
}
return fib_n0;
}
```
### 更精確的 Fibonacci 運算實作
#### 基於 `list_head` 的大數運算
先在熟悉的 user space 進行實作來檢驗程式碼的正確性。
##### 實作理念
```c
typedef struct {
size_t len;
struct list_head link;
} bignum_head;
typedef struct {
uint64_t value;
struct list_head link;
} bignum_node;
#define NEW_NODE(head, val) \
({ \
bignum_node *node = malloc(sizeof(bignum_node)); \
if (node) { \
node->value = val; \
list_add_tail(&node->link, head); \
} \
})
```
- 使用鏈結串列避免像字串操作須重複 `malloc` 以及 `free` 的成本
- 利用內建的 64 位元整數型別進行運算
- 減少解碼成本(僅須 `printf` 類的格式化輸出函式)
```c=
static inline void bignum_add_to_smaller(struct list_head *lgr,
struct list_head *slr)
{
struct list_head **l = &lgr->next, **s = &slr->next;
for (bool carry = 0;; l = &(*l)->next, s = &(*s)->next) {
if (*s == slr) {
if (*l == lgr) {
if (carry)
NEW_NODE(slr, 1);
break;
}
NEW_NODE(slr, 0);
}
bignum_node *lentry = list_entry(*l, bignum_node, link),
*sentry = list_entry(*s, bignum_node, link);
carry = FULL_ADDER_64(lentry->value, sentry->value, carry);
}
}
```
:::danger
程式碼中的 `l` 以及 `s` 兩個 indirect pointer 雖然看似多餘,但是其實不可被簡化,否則會影響答案,因為:
第 14 行的 `NEW_NODE` 會新增一節點並透過 `list_add_tail` 加到 `slr` 的尾端,因此若是單純使用指向 `list_head` 的指標的話,則需要在 `NEW_NODE` 後重新將 `s` 指向新的節點,否則第 18 行的 `list_entry` 相當於對串列的 head 使用 `list_entry`,造成存取到預期外位址。
因此使用 indirect pointer `*s` 來省略將 `s` 指向新的尾端節點的步驟。
:::
```c
#define MAX_DIGITS 18
#define BOUND64 1000000000000000000UL
#define FULL_ADDER_64(a, b, carry) \
({ \
uint64_t res = (a) + (b) + (carry); \
uint64_t overflow = -!!(res >= BOUND64); \
(b) = res - (BOUND64 & overflow); \
overflow; \
})
```
由於 64 位元無號整數的上限為 20 位數的 $18,446,744,073,709,551,615$,而為了減少解碼的成本,透過限制每個節點的上限值為 10 的冪,每當超過上限時就新增一節點儲存進位,而解碼時只要從最高位依序將每個節點以 10 進位的形式印出即可完成解碼。
:::success
為了盡可能的減少節點數量,原先是以 `UINT64_MAX` 作為上限,但發現在解碼時仍須進行大數運算,因此改以 10 的冪($10^{18} = 1000000000000000000$)作為上限值,來減少解碼成本。
而不選擇以 $10^{19}$ 作為上限的原因則是考慮到兩個 $10^{19}-1$ 相加時會超過 `UINT64_MAX`,造成需要額外判斷是否有 overflow 的發生,因此選擇少一位數的 $10^{18}$ 作為上限配合 $\geq$ 進行簡單的判斷。
:::
##### 測試結果(最簡單實作的費氏數列運算)
```c
int main(int argc, char const *argv[])
{
struct list_head *h1 = bignum_new(1);
struct list_head *h2 = bignum_new(0);
for (int i = 0; i < atoi(argv[1]); ++i) {
bignum_add_to_smaller(h1, h2);
swap(h1, h2);
}
bignum_to_string(h2, NULL, 0);
bignum_free(h1);
bignum_free(h2);
return 0;
}
```
由於目前只有實作加法的功能,因此先以最基本的遞迴呼叫測試:
```shell
$ taskset -cp 1
pid 1's current affinity list: 0-9
```
```shell
$ time taskset -c 10 ./a.out 100000 | wc
0 1 20899
taskset -c 10 ./a.out 100000 0.25s user 0.00s system 99% cpu 0.257 total
wc 0.00s user 0.00s system 0% cpu 0.256 total
```
$fib(100000)$ 大約在 $\frac{1}{4}$ 秒左右計算完成,且與 [WolframAlpha 上的結果](https://www.wolframalpha.com/input?i=fib%28100000%29)相同。
```shell
$ time taskset -c 10 ./a.out 1000000 | wc
0 1 208988
taskset -c 10 ./a.out 1000000 29.70s user 0.00s system 99% cpu 29.707 total
wc 0.00s user 0.00s system 0% cpu 29.707 total
```
而 $fib(1000000)$ 大約在 30 秒內計算完成,但因位數過多無法與 WolframAlpha 上的結果相比。
##### 實作 Fast Doubling 版本
:::warning
TODO : 需要實作 in-place 乘法運算
:::
#### 以 Kernel API 改寫
##### 動態記憶體配置
而由於 Linux Kernel 中不應使用 VLA,所以需要使用動態記憶體配置:
> 參考資料:[LWN - Variable-length arrays and the max() mess](https://lwn.net/Articles/749064/)
但若使用熟悉的 `malloc` 的話,會發現連熟悉的函式庫都找不到,因此 `make all` 時就會發現編譯失敗:
```text
/home/freshliver/Dropbox/Notes/_jserv/linux/labs/lab3-fibdrv/fibdrv.c:11:10: fatal error: stdlib.h: 沒有此一檔案或目錄
11 | #include <stdlib.h>
```
> Kernel Module 不能使用標準函式庫的原因參考了 [Stackoverflow 的說明](https://stackoverflow.com/questions/26455140/how-does-the-kernel-stop-you-using-malloc),但還需要查證其他第一手資料。
因此需要從 Linux Kernel 的 Header 中找出動態記憶體配置的函式,以及需要引入的標頭檔。而從 [Linux Kernel 文件的 Memory Allocation Guide](https://www.kernel.org/doc/html/latest/core-api/memory-allocation.html) 中有寫到:
> Linux provides a variety of APIs for memory allocation. You can allocate small chunks using `kmalloc` or `kmem_cache_alloc` families, large virtually contiguous areas using `vmalloc` and its derivatives, or you can directly request pages from the page allocator with `alloc_pages`. It is also possible to use more specialized allocators, for instance `cma_alloc` or `zs_malloc`.
這段首先提示了幾個可用來動態記憶體配置的函式,而這些函式需要引入以下幾個標頭檔:
```c
#include <linux/mm.h>
#include <linux/slab.h>
#include <linux/vmalloc.h>
```
接下來則是要從[一堆看起來很像的記憶體配置函式](https://www.kernel.org/doc/html/latest/core-api/mm-api.html)中尋找符合須由的函式,而在下方的 [Selecting memory allocator](https://www.kernel.org/doc/html/latest/core-api/memory-allocation.html#selecting-memory-allocator) 部份則有提到選擇 Allocator 的建議以及限制:
> The most straightforward way to allocate memory is to use a function from the kmalloc() family. And, to be on the safe side it’s best to use routines that set memory to zero, like kzalloc().
> The maximal size of a chunk that can be allocated with kmalloc is limited. The actual limit depends on the hardware and the kernel configuration, but it is a **good practice to use kmalloc for objects smaller than page size**.
> For **large** allocations you can use vmalloc() and vzalloc(),(後略)
其中提到了 `kmalloc` 的限制,可以從 [Linux Manual Page 中 `sysconf` 的 POSIX.1 variables 部份](https://www.man7.org/linux/man-pages/man3/sysconf.3.html) 中找到 `PAGESIZE` 這個系統變數,而系統變數則可以透過 [`getconf`](https://www.man7.org/linux/man-pages/man1/getconf.1p.html) 取得(單位為位元組):
```shell
$ getconf PAGESIZE
4096
```
由於在我電腦上的 `PAGESIZE` 設定是 4 KB,因此在這次實作中,只有能夠確保記憶體佔用小於 4 KB 的物件才使用 `kmalloc` 進行配置,例如:`list_head` 以及 `bignum_head` 等簡單的結構體物件,並使用 `kfree` 釋放對應的記憶體哭間。
而無法確保大小的物件,例如 $fib(k)$ 的結果字串,則使用 `vmalloc` 相關的函式進行配置,並搭配 `vfree` 釋放記憶體空間。
##### 改寫記憶體配置的部份
在建立節點以及刪除串列的部份比較單純,直接將 `malloc` 以及 `free` 分別改成 `kmalloc` 以及 `kfree` 即可:
:::warning
GFP
:::
```c
#define NEW_NODE(head, val) \
({ \
bignum_node *node = kmalloc(sizeof(bignum_node), GFP_KERNEL); \
if (node) { \
node->value = val; \
list_entry(head, bignum_head, link)->len++; \
list_add_tail(&node->link, head); \
} \
})
static inline void bignum_free(struct list_head *head)
{
bignum_node *ptr, *next;
list_for_each_entry_safe (ptr, next, head, link)
kfree(ptr);
kfree(list_entry(head, bignum_head, link));
}
```
而解碼的部份則是使用 `vzalloc` 來省去 `memset` 的步驟:
```c
static inline char *bignum_to_string(struct list_head *head)
{
// UINT64 < BOUND64 (10^18)
size_t digits = list_entry(head, bignum_head, link)->len * MAX_DIGITS + 1;
// DMA for result string
char *res = vzalloc(sizeof(char) * digits), *pres = res;
if (!res)
return NULL;
// decode from Most Significant Node
if (head == head->next) {
vfree(res);
return NULL;
}
uint64_t node_result = 0;
struct list_head *p = head->prev;
// Most Significant Node
node_result = list_entry(p, bignum_node, link)->value;
snprintf(pres, MAX_DIGITS + 1, "%" U64_FMT, node_result);
// other nodes
for (p = p->prev; p != head; p = p->prev) {
size_t pos = strlen(res);
node_result = list_entry(p, bignum_node, link)->value;
snprintf(&res[pos], MAX_DIGITS + 1, "%018" U64_FMT, node_result);
}
return res;
}
```
##### 改寫 `fibdrv.c`
```c
static long long fib_sequence_big(uint64_t target, char *buf, size_t size)
{
struct list_head *lgr = bignum_new(1), *slr = bignum_new(0);
for (uint64_t i = 0; i < target; ++i) {
bignum_add_to_smaller(lgr, slr);
swap(lgr, slr);
}
char *result = bignum_to_string(slr);
size_t failed = copy_to_user(buf, result, size);
vfree(result);
bignum_free(lgr);
bignum_free(slr);
return size - failed;
}
```
在 `client.c` 中新增一個函式來進行最基本的 `fibonacci` 運算,並將運算結果透過 `copy_to_user` 複製到在 user space 中透過 `malloc` 分配的記憶體,最後再釋放記憶體空間。
```c
static ssize_t fib_read(struct file *file,
char *buf,
size_t size,
loff_t *offset)
{
return (ssize_t) fib_sequence_big(*offset, buf, size);
}
```
接著則須改寫 `fib_read` 的部份,傳入 `buf` 以及 `size` 讓 `client` 能夠取得運算結果。
##### 改寫 `client.c`
```c
// fib(n) length ~= 0.2090n
size_t length_pred = offset / 4 + 1;
char *result = calloc(length_pred, 1);
for (int i = 0; i <= offset; i++) {
lseek(fd, i, SEEK_SET);
sz = read(fd, result, length_pred);
printf("Reading from " FIB_DEV
" at offset %d, returned the sequence "
"%s.\n",
i, result);
}
for (int i = offset; i >= 0; i--) {
lseek(fd, i, SEEK_SET);
sz = read(fd, result, length_pred);
printf("Reading from " FIB_DEV
" at offset %d, returned the sequence "
"%s.\n",
i, result);
}
free(result);
```
而 `client.c` 則須修改兩個包含 `read` 的 `for` 迴圈的內容,為了確保答案能夠全部存放到 `buffer` 中,這邊使用了大於 [$fib(n)$ 位數預測值 $n \times 0.2090$](https://en.wikipedia.org/wiki/Fibonacci_number#Magnitude) 的 $n \times 0.25$ 作為 `buffer` 大小,並使用此 `buffer` 作為 `read` 的參數儲存結果,最後再將 `printf` 改用 `result` 輸出答案。
##### 移除 `MAX_LENGTH` 限制
在完成上面的步驟後,如果直接執行 `make check` 會發現 `fibdrv` 的輸出在 $fib(92)$ 後與預期的數值不同,這是因為 `fibdrv.c` 原先用來回傳答案的型別 `sszie_t` 無法表示 $fib(93)$ 以及之後的數值,因此有在 `seek` 的部份另外進行限制:
```c
#define MAX_LENGTH 92
static loff_t fib_device_lseek(struct file *file, loff_t offset, int orig)
{
loff_t new_pos = 0;
switch (orig) {
case 0: /* SEEK_SET: */
new_pos = offset;
break;
case 1: /* SEEK_CUR: */
new_pos = file->f_pos + offset;
break;
case 2: /* SEEK_END: */
new_pos = MAX_LENGTH - offset;
break;
}
if (new_pos > MAX_LENGTH)
new_pos = MAX_LENGTH; // max case
if (new_pos < 0)
new_pos = 0; // min case
file->f_pos = new_pos; // This is what we'll use now
return new_pos;
}
```
這會讓 `seek` 無法設定成大於 92 的數,因此 `fib_read` 參數的 `offset` 最大只會是 92,造成在 `client.c` 中呼叫的 $fib(k), \forall k \geq 93$ 都以 $fib(92)$ 替代,使得 `fibdrv` 在 $fib(92)$ 後的輸出皆相同,因此只要將 `MAX_LENGTH` 改成 `100` 即可。
##### 修改正確答案
```diff
Reading from /dev/fibonacci at offset 90, returned the sequence 2880067194370816120.
Reading from /dev/fibonacci at offset 91, returned the sequence 4660046610375530309.
Reading from /dev/fibonacci at offset 92, returned the sequence 7540113804746346429.
-Reading from /dev/fibonacci at offset 93, returned the sequence 7540113804746346429.
-Reading from /dev/fibonacci at offset 94, returned the sequence 7540113804746346429.
-Reading from /dev/fibonacci at offset 95, returned the sequence 7540113804746346429.
-Reading from /dev/fibonacci at offset 96, returned the sequence 7540113804746346429.
-Reading from /dev/fibonacci at offset 97, returned the sequence 7540113804746346429.
-Reading from /dev/fibonacci at offset 98, returned the sequence 7540113804746346429.
-Reading from /dev/fibonacci at offset 99, returned the sequence 7540113804746346429.
-Reading from /dev/fibonacci at offset 100, returned the sequence 7540113804746346429.
-Reading from /dev/fibonacci at offset 100, returned the sequence 7540113804746346429.
-Reading from /dev/fibonacci at offset 99, returned the sequence 7540113804746346429.
-Reading from /dev/fibonacci at offset 98, returned the sequence 7540113804746346429.
-Reading from /dev/fibonacci at offset 97, returned the sequence 7540113804746346429.
-Reading from /dev/fibonacci at offset 96, returned the sequence 7540113804746346429.
-Reading from /dev/fibonacci at offset 95, returned the sequence 7540113804746346429.
-Reading from /dev/fibonacci at offset 94, returned the sequence 7540113804746346429.
-Reading from /dev/fibonacci at offset 93, 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.
+Reading from /dev/fibonacci at offset 100, returned the sequence 354224848179261915075.
+Reading from /dev/fibonacci at offset 99, returned the sequence 218922995834555169026.
+Reading from /dev/fibonacci at offset 98, returned the sequence 135301852344706746049.
+Reading from /dev/fibonacci at offset 97, returned the sequence 83621143489848422977.
+Reading from /dev/fibonacci at offset 96, returned the sequence 51680708854858323072.
+Reading from /dev/fibonacci at offset 95, returned the sequence 31940434634990099905.
+Reading from /dev/fibonacci at offset 94, returned the sequence 19740274219868223167.
+Reading from /dev/fibonacci at offset 93, returned the sequence 12200160415121876738.
Reading from /dev/fibonacci at offset 92, returned the sequence 7540113804746346429.
Reading from /dev/fibonacci at offset 91, returned the sequence 4660046610375530309.
Reading from /dev/fibonacci at offset 90, returned the sequence 2880067194370816120.
```
### 透過寫入使用不同的實作求值
```c
static enum FIB_MODES {
FIB_MODE_BASIC_64 = 0,
FIB_MODE_BASIC_BIG = 1,
FIB_MODE_FAST_DOUBLING_64 = 2,
FIB_MODE_FAST_DOUBLING_BIG = -1,
} mode = FIB_MODE_BASIC_64;
```
透過建立一個包含數種費氏數列計算實作的 `enum`,並修改 `fib_write` 讓程式能夠透過對 `/dev/fibonacci` 進行 `write` 來修改模式:
```c
static ssize_t fib_write(struct file *file,
const char *buf,
size_t size,
loff_t *offset)
{
int val, err = kstrtoint_from_user(buf, size, 10U, &val);
if (err == ERANGE || err == EINVAL)
pr_err("OVERFLOW OR NOT A NUMBER STRING.\n");
else
switch (val) {
case FIB_MODE_BASIC_64:
pr_info("SET MODE : BASIC_64.\n");
mode = FIB_MODE_BASIC_64;
return FIB_MODE_BASIC_64;
case FIB_MODE_FAST_DOUBLING_64:
pr_info("SET MODE : FAST_64.\n");
mode = FIB_MODE_FAST_DOUBLING_64;
return FIB_MODE_FAST_DOUBLING_64;
case FIB_MODE_BASIC_BIG:
pr_info("SET MODE : BASIC_BIG.\n");
mode = FIB_MODE_BASIC_BIG;
return FIB_MODE_BASIC_BIG;
case FIB_MODE_FAST_DOUBLING_BIG:
default:
pr_warn("TO BE IMPLEMENTED.\n");
break;
}
return 1;
}
```
這邊是選擇使用整數作為各種實作模式的編號,在 `fib_write` 中會先透過 `kstrtoint_from_user` 嘗試將 `buf` 中的字串轉為整數,接著再根據結果設定模式。
:::warning
`printk` 或是 `pr_info` 等 [Message Logging Functions](https://www.kernel.org/doc/html/latest/core-api/printk-basics.html) 都會輸出到 `/var/log/syslog` 中,可以透過 `tail` 或是 `cat` 等命令檢查程式是否有照預期運作。
```shell
Mar 16 00:02:24 freshliver-K21 kernel: [78008.798135] MODE = BASIC_64.
Mar 16 00:02:27 freshliver-K21 kernel: [78011.276084] UNKNOWN MODE.
Mar 16 00:02:27 freshliver-K21 kernel: [78011.276092] MODE = BASIC_64.
Mar 16 00:02:31 freshliver-K21 kernel: [78015.190769] MODE = BASIC_64.
Mar 16 00:02:33 freshliver-K21 kernel: [78017.419772] MODE = BASIC_64.
Mar 16 00:02:36 freshliver-K21 kernel: [78019.993990] SET MODE : BASIC_BIG.
Mar 16 00:02:36 freshliver-K21 kernel: [78019.993994] MODE = BASIC_BIG.
Mar 16 00:02:45 freshliver-K21 kernel: [78028.973371] SET MODE : BASIC_BIG.
Mar 16 00:02:45 freshliver-K21 kernel: [78028.973374] MODE = BASIC_BIG.
```
:::
而 `fib_read` 中則須根據選擇的模式呼叫對應的實作函式來進行計算:
```c
static ssize_t fib_read(struct file *file,
char *buf,
size_t size,
loff_t *offset)
{
switch (mode) {
case FIB_MODE_BASIC_64:
printk(KERN_INFO "MODE = BASIC_64.\n");
return (ssize_t) fib_basic_64(*offset, buf, size);
case FIB_MODE_FAST_DOUBLING_64:
printk(KERN_INFO "MODE = FAST_DOUBLING_64.\n");
return (ssize_t) fib_fast_64(*offset, buf, size);
case FIB_MODE_BASIC_BIG:
printk(KERN_INFO "MODE = BASIC_BIG.\n");
return (ssize_t) fib_basic_big(*offset, buf, size);
case FIB_MODE_FAST_DOUBLING_BIG:
default:
return copy_to_user(buf, "TO BE IMPLEMENTED.", size);
}
}
```
## 評測及分析
在評測及分析的部份參考了 [KYG-yaya573142 的報告](https://hackmd.io/@KYWeng/rkGdultSU)中所使用到的技巧以及觀念。
### 準備
#### 減少干擾因素
為了進可能減少干擾因素,在測試時做了以下處理:
- 關閉其他可能影響測量的程式
- 修改 GRUB 參數讓部份 CPU 不被使用並使用 `taskset` 設定 Affinity
```shell
$ cat /etc/default/grub | grep "isolcpu"
GRUB_CMDLINE_LINUX_DEFAULT="isolcpus=10,11"
```
- 關閉 Intel Turbo
`sudo sh -c "echo 1 > /sys/devices/system/cpu/intel_pstate/no_turbo"`
- 關閉 ASLR
`sudo sh -c "echo 0 > /proc/sys/kernel/randomize_va_space"`
- 設定 scaling_governor 為 performance
`sudo sh -c "echo performance > /sys/devices/system/cpu/cpu$CPU_ID/cpufreq/scaling_governor"`
- 重複測量並排除 95% 信賴區間外的數據
- 小範圍 ($fib(0)$ 到 $fib(92)$) 的測試重複 1000 次
- 大範圍 ($fib(0)$ 到 $fib(2000)$) 的測試則重複 100 次
:::danger
```bash
ISOLATED_CPU_ID=11
ISOLATED_CPU_AFFINITY_MASK=7ff
for irq in $(find /proc/irq/ -name "smp_affinity"); do
AND_MSK="$(( 0x$(cat $irq) & 0x$ISOLATED_CPU_AFFINITY_MASK ))"
AND_MSK="$(printf \"%03x\" $AND_MSK )"
sudo sh -c "echo $AND_MSK > $irq";
done
sudo sh -c "echo $ISOLATED_CPU_AFFINITY_MASK > /proc/irq/default_smp_affinity"
```
修改部份 IRQ 的 `smp_affinity` 時會出現 `sh: 1: echo: echo: I/O error` 的錯誤訊息:
```shell
$ ISOLATED_CPU_AFFINITY_MASK=7ff
$ for irq in $(find /proc/irq/ -name "smp_affinity"); do
AND_MSK="$(( 0x$(cat $irq) & 0x$ISOLATED_CPU_AFFINITY_MASK ))"
AND_MSK="$(printf \"%03x\" $AND_MSK )"
sudo sh -c "echo $AND_MSK > $irq";
done
sh: 1: echo: echo: I/O error
sh: 1: echo: echo: I/O error
sh: 1: echo: echo: I/O error
...
```
:::
#### 透過 Shell Script 進行自動測試以及格式化輸出
為了方便重複測量執行時間以及處理數據,另外[寫了一個 Shell Script](https://github.com/freshLiver/fibdrv/commit/4e1e729c243c4c1c23502a9f06a402c6d4ee1190) 來處理前一部份提到的處理以及重複測量相關的,這會將 [experiments/read_perf.c](https://github.com/freshLiver/fibdrv/commit/35dc8cbc008814fe61a7fc174a549e12b2ce40fc) 的結果以 JSON 的格式輸出,最後再交給 [Python Script](https://github.com/freshLiver/fibdrv/blob/4e1e729c243c4c1c23502a9f06a402c6d4ee1190/experiments/plot_result.ipynb) 進行信賴區間的處理以及繪製圖表。
### 64 位元整數版本
#### VLA (原始版本)
#### 無 VLA

:::warning
原先即使關閉了 Turbo、ASLR 也將 cpufreq 設定成 PERFORMANCE 模式,並透過 taskset 將測試程式指定在其中一個已設定 isolcpu 的 cpu 上測試了,但似乎仍會受到其他因素影響、畫出像下方一樣有明顯抖動的結果。

結果透過 htop 發現 dropbox 有明顯佔用其他 cpu,而將 dropbox 關閉後就沒有明顯的抖動了。
:::
#### Fast Doubling
##### Top-down (Recursive)
##### Bottom-up (Iterative)

##### `ctz`/`clz` 類的指令對費氏數列的效益
在 Bottom-up 的策略中可以看到 `__builtin_clz` 的使用,以便快速的找到最高位的 1
:::warning
TODO : 與自定義的 `clz` 進行比較
:::
### 大數運算版本 - `bignum`


####
---
//
#### 加速大數運算與縮減記憶體操作成本的措施
- [ ] [KYG-yaya573142 的報告](https://hackmd.io/@KYWeng/rkGdultSU)
### `fibdrv.c` 中的 MUTEX
> 注意到 `fibdrv.c` 存在著 `DEFINE_MUTEX`, `mutex_trylock`, `mutex_init`, `mutex_unlock`, `mutex_destroy` 等字樣,什麼場景中會需要呢?撰寫多執行緒的 userspace 程式來測試,觀察 Linux 核心模組若沒用到 mutex,到底會發生什麼問題。嘗試撰寫使用 POSIX Thread 的程式碼來確認。
## 其他
### 為何不希望在虛擬機器中進行實驗