---
tags: linux
---
# 2023q1 Homework3 (fibdrv)
contributed by < `kata1219` >
## 開發環境
```bash!
$ gcc --version
gcc version 11.3.0 (Ubuntu 11.3.0-1ubuntu1~22.04)
$ 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
CPU max MHz: 5600.0000
CPU min MHz: 800.0000
BogoMIPS: 4992.00
Vendor ID: GenuineIntel
CPU family: 6
Model: 151
Model name: 12th Gen Intel(R) Core(TM) i5-12400
Stepping: 2
BogoMIPS: 4991.99
Virtualization: VT-x
L1d cache: 288 KiB
L1i cache: 192 KiB
L2 cache: 7.5 MiB
L3 cache: 18 MiB
```
## Fibonacci 運算
費氏數列以義大利數學家費波那契(Leonardo Fibonacci)命名,他在他的著作中描述兔子生長的數目時用上了這數列,有以下規則:
* 第一個月初有一對剛誕生的兔子
* 第二個月之後(第三個月初)牠們可以生育
* 月每對可生育的兔子會誕生下一對新兔子
* 兔子永不死去
將規則整理後可得到以下的方程式
$${a}_{n+1} = {a}_n + {b}_n\\{b}_{n+1} = {a}_n$$
方程式整理為矩陣的形式
$$\left(\begin{array}{c}
{a}_{n+1} \\
{b}_{n+1} \\
\end{array}\right)=\left(\begin{array}{cc}
1 & 1 \\
1 & 0 \\
\end{array}\right)\left(\begin{array}{c}
{a}_{n} \\
{b}_{n} \\
\end{array}\right)
$$
可進一步整理
$$Q=\left(\begin{array}{cc}
1 & 1 \\
1 & 0 \\
\end{array}\right)=\left(\begin{array}{cc}
F_2 & F_1 \\
F_1 & F_0 \\
\end{array}\right) \\
Q^n=\left(\begin{array}{cc}
F_{n+1} & F_n \\
F_n & F_{n-1} \\
\end{array}\right)
$$
### Fast Doubling
從 $2n+1$ 和 $2n$ 的矩陣出發
$$\begin{split}\left[\begin{array}{c}
F(2n+1) \\
F(2n) \\
\end{array}\right]&=\left[\begin{array}{cc}
1 & 1 \\
1 & 0 \\
\end{array}\right]^{2n}\left[\begin{array}{cc}
F(1) \\
F(0) \\
\end{array}\right] \\
&= \left[\begin{array}{cc}
1 & 1 \\
1 & 0 \\
\end{array}\right]^{n}\left[\begin{array}{cc}
1 & 1 \\
1 & 0 \\
\end{array}\right]^{n}\left[\begin{array}{cc}
F(1) \\
F(0) \\
\end{array}\right] \\
&=\left[\begin{array}{cc}
F(n+1) & F(n) \\
F(n) & F(n-1) \\
\end{array}\right]
\left[\begin{array}{cc}
F(n+1) & F(n) \\
F(n) & F(n-1) \\
\end{array}\right]
\left[\begin{array}{c}
1 \\
0 \\
\end{array}\right] \\
&=\left[\begin{array}{c}
F(n+1)^2+F(n)^2 \\
F(n)F(n+1)+F(n-1)F(n) \\
\end{array}\right]
\end{split}
$$
可得到
$$
F(2k)=F(k)[2F(k+1)-F(k)]\\
F(2k+1) = F(k+1)^2+F(k)
$$
時間複雜度由原本的 $O(n)$ 降至 $O(log\ n)$。
#### clz 應用於 Fast Doubling
由於二進制的特性,我們可以從左至右計算每一個 bit,當讀到 0 代表數值乘二,讀到 1 代表將數值乘二加一,將以下 pseudocode 應用於 fast doubling。
```
while(bit)
{
if(bit == 0)
fib(n) = fib(2n)
else if(bit == 1)
fib(n) = fib(2n+1)
bit = next_bit
}
```
在本次作業中,輸入的數值為 long long,除非數字非常大,否則 n 轉換為二進制時,前面會多很多不需要計算的 0,透過 clz / clzll 可以跳過那些 0。
## 大數運算
使用大數運算計算 fibonacci 數,結構如下圖,bignum_head 紀錄 list 長度,bignum_node 紀錄 $10^{18}$ 以內的正整數,若 value 超過 $10^{18}$ 則新增一個 bignum_node。
```graphviz
digraph struct {
rankdir=LR;
node[shape=record, style=bold];
subgraph cluster_0 {
node [shape=record];
head_link [label="{link}"];
len [label="{len}"];
style=bold;
label=bignum_head
}
subgraph cluster_1{
node [shape=record];
node1_link [label="{link}"];
value1 [label="{value}"];
style=bold;
label=bignum_node;
}
subgraph cluster_2{
node [shape=record];
node2_link [label="{link}"];
value2 [label="{value}"];
style=bold;
label=bignum_node;
}
head_link->node1_link;
node1_link->node2_link;
node2_link->link;
}
```
計算 fib(100000) 的時間為 0.437s,結果與 [wolframalpha](https://www.wolframalpha.com/input?i=fib%2810000%29) 上相同。
### 加速 Fast Doubling
#### 第一版
執行時間幾乎與 n 等比例成長,有**很大**的進步空間。
| n | bignum_fib | bignum_fast_doubling |
| -------- | -------- | -------- |
| 100000 | 0.437s | 0.288s |
| 1000000 | 42.579s | 26.524s |
使用 perf 觀察到執行時間幾乎都花費在大數乘法運算上
```
$ sudo perf record -g --call-graph dwarf ./bignum
$ sudo perf report --stdio -g graph,0.5,caller
# Children Self Command Shared Object Symbol
# ........ ........ ....... .................... ...........................
#
99.80% 0.00% bignum bignum [.] _start
|
---_start
__libc_start_main_impl (inlined)
__libc_start_call_main (inlined)
main
bn_fast_doubling
|
--99.70%--bignum_mul
```
## 實作 fibdrv.ko
### 回收記憶體
kernel module 與一般程式不同,在結束後不會[自動刪除記憶體](https://stackoverflow.com/questions/11657387/is-memory-allocated-by-kmalloc-ever-automatically-freed),因此在使用 kernel 配置記憶體時需要特別小心。
* 使用 valgrind 檢查是否有未收回的記憶體,確認都完整收回後再開發 kernel module。
```
$ valgrind --tool=memcheck ./bignum --leak-check=full
gcc bignum.c -o bignum
==37880== Memcheck, a memory error detector
==37880== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==37880== Using Valgrind-3.18.1 and LibVEX; rerun with -h for copyright info
==37880== Command: ./bignum --leak-check=full
==37880==
fib 100: 354224848179261915075
==37880==
==37880== HEAP SUMMARY:
==37880== in use at exit: 0 bytes in 0 blocks
==37880== total heap usage: 82 allocs, 82 frees, 2,966 bytes allocated
==37880==
==37880== All heap blocks were freed -- no leaks are possible
==37880==
==37880== For lists of detected and suppressed errors, rerun with: -s
==37880== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
```
### 驗證程式碼
* 將大數運算搬進 kernel 後,可以使用 `printk` 查看是否運算正確。
```
6,2372,9769804943,-;fib 93: 12200160415121876738
```
> 可以使用 `sudo cat /dev/kmsg` 或 `sudo cat /proc/kmsg` 查看,實測後發現後者不能穩定輸出,常會有送出多次只收到一次的情形,根據 [difference between /proc/kmsg and /dev/kmsg](https://unix.stackexchange.com/questions/585919/what-is-the-difference-between-proc-kmsg-and-dev-kmsg) 推測可能是同時有多個程式在查看導致被 lock 住。
**更新:**
* printk 執行時間容易受影響,改為使用 sysfs 回傳 log。
### 回傳結果 & 驗證
由於原本的方法是用 ssize_t 來回傳結果,但超過 fib(92) 的數都會超過 ssize_t 的大小,使用 `<linux/uaccess.h>` 中的 `copy_to_user` 將結果搬到 user space 中並回傳 copy_to_user 的大小,用來檢查是否成功回傳至 user space。
> 在使用 `copy_to_user(buf, output, len)` 時,原本是直接把 `read()` 函式中的 `size_t size` 當作 `copy_to_user` 的長度,結果發現它雖然可以正常印出 buffer 的大小,但無法轉換為 unsigned long,導致分配給 `copy_to_user` 的長度為 0 並引發以下錯誤 - Kernel memory exposure attempt detected from SLUB object 'kmalloc-8' (offset 0, size 8192)!。
原本用來驗證的檔案超過 fib(92) 的數都固定為 fib(92),將 `expected.txt` 及 `verify.py` 修改成最高計算到 fib(500) 並測試通過。
:::success
Passed [-]
:::
:::warning
善用作業說明提到的 `sysfs`,這樣可更容易解析 fibdrv 的運算結果。
:notes: jserv
> 好的
:::
### 時間測量與效能分析
#### 改寫 fib_write