---
tags: Linux核心設計
---
# 2023q1 Homework3 (L04: fibdrv)
contributed by < [Tonr01](https://github.com/Tonr01/fibdrv) >
## 開發環境
```
gcc (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0
架構: x86_64
CPU 作業模式: 32-bit, 64-bit
Address sizes: 39 bits physical, 48 bits virtual
Byte Order: Little Endian
CPU(s): 16
On-line CPU(s) list: 0-15
供應商識別號: GenuineIntel
Model name: 11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz
CPU 家族: 6
型號: 141
每核心執行緒數: 2
每通訊端核心數: 8
Socket(s): 1
製程: 1
CPU max MHz: 4600.0000
CPU min MHz: 800.0000
BogoMIPS: 4608.00
```
## 開發環境準備
自從 Linux 核心 4.4 版以來,Ubuntu Linux 預設開啟 EFI_SECURE_BOOT_SIG_ENFORCE,這使得核心模組需要適度的簽章才可掛載進入 Linux 核心,為了後續測試的便利,我們需要將 UEFI Secure Boot 的功能關閉,請見 [Why do I get “Required key not available” when install 3rd party kernel modules or after a kernel upgrade?](https://askubuntu.com/questions/762254/why-do-i-get-required-key-not-available-when-install-3rd-party-kernel-modules)
首先先關閉 UEFI Secure Boot
```
$ sudo apt install mokutil
$ sudo mokutil --disable-validation
```
檢查 Linux 核心版本
```
$ uname -r
5.19.0-35-generic
```
安裝 `linux-headers` 套件
```
$ sudo apt install linux-headers-`uname -r`
```
確認 `linux-headers` 套件已正確安裝於開發環境,預期得到以下輸出:
```
$ dpkg -L linux-headers-5.4.0-66-generic | grep "/lib/modules"
/lib/modules
/lib/modules/5.19.0-35-generic
/lib/modules/5.19.0-35-generic/build
```
檢驗目前的使用者身份
```
$ whoami
tonr01
```
檢驗測試過程所需 sudo
```
$ sudo whoami
root
```
編譯並測試
```
$ make check
```
預期會看到綠色的 `Passed [-]` 字樣,隨後是
```
f(93) fail
input: 7540113804746346429
expected: 12200160415121876738
```
### 在 vscode 中 `fibdrv.c` 的標頭檔錯誤
直接將檔案從 clone 後,用 vscode 開啟,會有標頭檔錯誤,這裡參考 [Shiritai](https://hackmd.io/@Eroiko/fibdrv-impl#%E4%BB%A5-VSCode-%E9%96%8B%E7%99%BC-Linux-Kernel-Module-%E7%9A%84%E6%BA%96%E5%82%99) 的方法。
![](https://i.imgur.com/XnBHib5.png)
首先先確認 Linux 核心版本。
```
$ uname -r
5.19.0-35-generic
```
再確認 Linux kernel header,確認其安裝的路徑,可移動至 /usr/src 下查看。
```
$ ls /usr/src | grep linux-headers
linux-headers-5.19.0-32-generic
linux-headers-5.19.0-35-generic
```
再確認 gcc version
```
$ ls /usr/lib/gcc/x86_64-linux-gnu/
11
```
最後 IntelliSense 需要調整 [C/C++ Extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode.cpptools) 設定檔,在 vscode 界面按下 f1,輸入 Edit configuration (UI),就能編輯 IntelliSense 組態,詳細改動放在 [commit](https://github.com/sysprog21/fibdrv/commit/f64f76174e9e0bdd8231f9c9bec8d8bc4b2417f0)。
改動完, 就無 error message 了
![](https://i.imgur.com/bLSi60k.png)
#### 參考相關資料
[How to develop Linux Kernel Module with vscode without incorrect error detection](https://stackoverflow.com/questions/58386640/how-to-develop-linux-kernel-module-with-vscode-without-incorrect-error-detection)
[An IntelliSense Bug When Coding Linux Kernel Module](https://github.com/microsoft/vscode-cpptools/issues/5588)
## 研讀費氏數列相關材料
[費氏數列分析](https://hackmd.io/@sysprog/fibonacci-number)
[你所不知道的C語言:遞迴呼叫篇 - Fibonacci sequence](https://hackmd.io/@sysprog/c-prog/%2Fs%2FrJ8BOjGGl#Fibonacci-sequence)
### 費氏數列一般式
![](https://i.imgur.com/GBfoEXL.png)
### 費氏數列公式解
這個「公式解」一般認為由 [Jacques P. M. Binet](https://en.wikipedia.org/wiki/Jacques_Philippe_Marie_Binet) 在 1843 年發現,仍可追溯得更早。習慣上我們仍稱之為 Binet 等式 (Binet’s Formula)
![](https://i.imgur.com/mwkxwDp.png)
大多數的迷思認為這是最快的解法,為 $O(1)$ 的公式解,但對於電腦的離散本質會有幾個問題
* 根號運算表達困難,計算完 `sqrt(5)` 之後,只能用一個近似的值來表達結果,其誤差會在 n 值變大時慢慢放大。
* 根號、除法、次方對於程式來說是高成本的
可運用 [GMP](https://gmplib.org/) 或 [MPFR](https://www.mpfr.org/) 這這兩個 GNU 函式庫是所謂的「無限」位數的整數跟「無限」精確度的浮點數,可參考 [Fibonacci 快速解程式碼與公式解程式碼](https://gist.github.com/yodalee/4e221b081be4b367e9c7ef328ada7db5) 實作
### Q-matrix
主要透過矩陣轉換之方法,我們把原本之遞迴式轉換到矩陣表示,並透過矩陣次方之Divide and Conquer Method 來做加速,解說短片: [The Fibonacci Q-matrix](https://www.youtube.com/watch?v=lTHVwsHJrG0)。
![](https://i.imgur.com/CvJJica.png)
### Fast-Doubling
[Fast Doubling](https://chunminchang.github.io/blog/post/calculating-fibonacci-numbers-by-fast-doubling) 為 Q-matrix 的改良版,主要再細分成兩種情況, n 為奇數或偶數的情況,可以降低遞迴次數,
![](https://i.imgur.com/0I2pOe4.png)
## Fast Doubling 實作
### 作法
這裡參考 [Fast Doubling](https://chunminchang.github.io/blog/post/calculating-fibonacci-numbers-by-fast-doubling) 的手法,先用 `63 - __builtin_clzl(k)` 去找出不包含 leading 0s 的真正數字,再用 bit-mask 的方式取檢驗目前 bit 為0或1。
以 `k = 50` 為例,其二進位表示為 $110010_{2}$
```
00.....00 110010
^
6
count = 63 - __builtin_clzl(k) = 63 - 58 = 5
mask = 1 << count = 100000
```
|round|k|mask|k & mask|odd/even|
|-|-|-|-|-|
|1|110010|100000|100000|odd|
|2|110010|010000|010000|odd|
|3|110010|001000|000000|even|
|4|110010|000100|000000|even|
|5|110010|000010|000010|odd|
|6|110010|000001|000000|even|
### 程式碼
```c
/* Fast Doubling with clz */
static uint64_t fib_sequence(uint64_t k)
{
uint64_t f[2] = {0, 1};
uint64_t count = 63 - __builtin_clz(k);
for (unsigned int mask = 1 << count; mask; mask >>= 1) {
uint64_t a = f[0] * (2 * f[1] - f[0]); //F(2k+1) = F(k+1)^2+F(k)^2
uint64_t b = f[0] * f[0] + f[1] * f[1]; //F(2k) = F(k)[2F(k+1)−F(k)]
if ((mask & k)) {
f[0] = b;
f[1] = a + b;
} else {
f[0] = a;
f[1] = b;
}
}
return f[0];
}
```
### 測試結果
可以輸出正確到 Fib(92)
```
Reading from /dev/fibonacci at offset 89, returned the sequence 1779979416004714189.
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.
```
### 測試效能差距
這裡引入 [ktime_t](https://www.kernel.org/doc/html/latest/core-api/timekeeping.html) 來測量輸出上一次 fib 的執行時間,先使用老師的參考範例來測試,因為 `fib_write` 未使用,所以使用這個函式計算時間。
```c
/* ktime_t to calculate fib time */
static ktime_t kt;
static uint64_t fib_time_proxy(uint64_t k, int n)
{
kt = ktime_get();
uint64_t result = fib_sequence(k, n);
kt = ktime_sub(ktime_get(), kt);
return result;
}
/* Calculate the runtime */
static ssize_t fib_write(struct file *file,
const char *buf,
size_t size,
loff_t *offset)
{
fib_time_proxy(*offset, n);
return ktime_to_ns(kt);
}
```
再不影響原本的 `client.c` ,這裡多加入了一個新的使用者模式,叫做 `runtime.c` ,主要用來輸出時間的資料,並修改 `Makefile` 來執行程式,詳細改動在 [commit](https://github.com/sysprog21/fibdrv/commit/dfdc69f1d6d0f1aeecaba4cb725718f8d1d2951d),將輸出的時間資料用 [gnuplot](https://hackmd.io/@sysprog/gnu-linux-dev/https%3A%2F%2Fhackmd.io%2Fs%2FSkwp-alOg) 去繪製圖片。
![](https://i.imgur.com/pAgmMTl.png)
### 後續改善
```c
static uint64_t fib_sequence(uint64_t k, int n)
{
switch (n) {
case 0:
return fib_sequence_dp(k);
break;
default:
return fib_sequence_fast_doubling(k);
break;
}
}
```
將 `fib_sequence` 改善成可以切換不同的實作。
```c
static ssize_t fib_write(struct file *file,
const char *buf,
size_t size,
loff_t *offset)
{
switch (size) {
case 0:
fib_time_proxy(*offset, 0); // Fib_sequence with dynamic programming
break;
default:
fib_time_proxy(*offset, 1); // Fast Doubling with clz
break;
}
return ktime_to_ns(kt);
}
```
並將 `fib_write` 改善成可以同時計算及輸出其 runtime。
```c
/* Output the runtime */
for (int i = 0; i <= offset; i++) {
lseek(fd, i, SEEK_SET);
long long sz1 = write(fd, write_buf, 0);
long long sz2 = write(fd, write_buf, 1);
printf("%d %lld %lld\n", i, sz1, sz2);
}
```
在 `runtime.c` 中將兩種實作的 runtime 輸出到 txt 檔,方便圖的繪製,詳細改動在 [commit](https://github.com/sysprog21/fibdrv/commit/3f84990c83bf6694359f427993378e8f8f9dcf58)。
## 引入大數運算並研究
參考[初步支援大數運算](https://hackmd.io/@sysprog/linux2023-fibdrv/%2F%40sysprog%2Flinux2023-fibdrv-d#%E5%88%9D%E6%AD%A5%E6%94%AF%E6%8F%B4%E5%A4%A7%E6%95%B8%E9%81%8B%E7%AE%97),這邊引入 [KYG-yaya573142](https://github.com/KYG-yaya573142/fibdrv/blob/master/bn_kernel.c) 的實作來研究。
### 結構體 `bn`
```c
/* number[size - 1] = msb, number[0] = lsb */
typedef struct _bn {
unsigned int *number;
unsigned int size;
int sign;
} bn;
```
首先是結構體的部份,`number` 指向數值的部份,在後續使用會以陣列的方式存放數值,`size` 則是大數的記憶體大小,也就是陣列的大小,陣列的大小為 `number[size]` ,`sign` 存放的是數值的正負。
### `bn_clz`
```c
static int bn_clz(const bn *src)
{
int cnt = 0;
for (int i = src->size - 1; i >= 0; i--) {
if (src->number[i]) {
// prevent undefined behavior when src = 0
cnt += __builtin_clz(src->number[i]);
return cnt;
} else {
cnt += 32;
}
}
return cnt;
}
```
大數版本的 `clz` ,主要用來計算 leading zero 的個數。
### `bn_to_string`
```c
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;
}
```
因大數無法以數值的形式表示,故需要將其轉成字串,`len` 存放大數的 bit 個數,迴圈將二進位字串轉換成十進位字串。
### `bn_add`, `bn_sub`
```c
void bn_add(const bn *a, const bn *b, bn *c)
{
if (a->sign == b->sign) { // both positive or negative
bn_do_add(a, b, c);
c->sign = a->sign;
} else { // different sign
if (a->sign) // let a > 0, b < 0
SWAP(a, b);
int cmp = bn_cmp(a, b);
if (cmp > 0) {
/* |a| > |b| and b < 0, hence c = a - |b| */
bn_do_sub(a, b, c);
c->sign = 0;
} else if (cmp < 0) {
/* |a| < |b| and b < 0, hence c = -(|b| - |a|) */
bn_do_sub(b, a, c);
c->sign = 1;
} else {
/* |a| == |b| */
bn_resize(c, 1);
c->number[0] = 0;
c->sign = 0;
}
}
}
```
加法的部份,首先要先判斷兩個大數的正負是否相同,相同的話直接加起來即可,若正負不同時,因為大數的結構體是將數值跟正負號的部份分開存放,所以要先判斷兩個數值的大小,做相減的動作,其結果在加上正負號。
```c
static void bn_do_add(const bn *a, const bn *b, bn *c)
{
// max digits = max(sizeof(a) + sizeof(b)) + 1
int d = MAX(bn_msb(a), bn_msb(b)) + 1;
d = DIV_ROUNDUP(d, 32) + !d;
bn_resize(c, d); // round up, min size = 1
unsigned long long int carry = 0;
for (int i = 0; i < c->size; i++) {
unsigned int tmp1 = (i < a->size) ? a->number[i] : 0;
unsigned int tmp2 = (i < b->size) ? b->number[i] : 0;
carry += (unsigned long long int) tmp1 + tmp2;
c->number[i] = carry;
carry >>= 32;
}
if (!c->number[c->size - 1] && c->size > 1)
bn_resize(c, c->size - 1);
}
```
這裡用 `DIV_ROUNDUP` 來計算 `c` 的所需大小,其大小會滿足所需大小而不過於太多,使用 8 bytes 大小的 `carry` 來實行兩個 4 bytes 項目的加法來避免 overflow,因為每個 array 大小為 4 bytes,所以當相加完,`carry` 扣除右邊 4 bytes ,其餘都是進位的值,所以每回合 `carry>>32` 。
```c
static void bn_do_sub(const bn *a, const bn *b, bn *c)
{
// max digits = max(sizeof(a) + sizeof(b))
int d = MAX(a->size, b->size);
bn_resize(c, d);
long long int carry = 0;
for (int i = 0; i < c->size; i++) {
unsigned int tmp1 = (i < a->size) ? a->number[i] : 0;
unsigned int tmp2 = (i < b->size) ? b->number[i] : 0;
carry = (long long int) tmp1 - tmp2 - carry;
if (carry < 0) {
c->number[i] = carry + (1LL << 32);
carry = 1;
} else {
c->number[i] = carry;
carry = 0;
}
}
d = bn_clz(c) / 32;
if (d == c->size)
--d;
bn_resize(c, c->size - d);
}
```
因為減法不會有 overflow 的問題,所以不須用 `DIV_ROUNDUP` ,這裡是做無號數減法,`carry` 與加法的不同,這裡是作為向前一位的借位,當相減的值小於 0 ,才須向前一位借位,最後在調整 `c` 的所需大小,將不需要的 0 去掉。
```c
/* c = a - b
* Note: work for c == a or c == b
*/
void bn_sub(const bn *a, const bn *b, bn *c)
{
/* xor the sign bit of b and let bn_add handle it */
bn tmp = *b;
tmp.sign ^= 1; // a - b = a + (-b)
bn_add(a, &tmp, c);
}
```
再來是減法的部份,因為 $a - b = a + (-b)$ ,所以先將負號給 `b` ,但是不能改變 `b` 的值,所以宣告 `tmp` 給予改變後的 `b` 值,最後將 `a` 與 `tmp` 做加法即可。
### `bn_mult`
```c
void bn_mult(const bn *a, const bn *b, bn *c)
{
// max digits = sizeof(a) + sizeof(b))
int d = bn_msb(a) + bn_msb(b);
d = DIV_ROUNDUP(d, 32) + !d; // round up, min size = 1
bn *tmp;
/* make it work properly when c == a or c == b */
if (c == a || c == b) {
tmp = c; // save c
c = bn_alloc(d);
} else {
tmp = NULL;
bn_resize(c, d);
}
for (int i = 0; i < a->size; i++) {
for (int j = 0; j < b->size; j++) {
unsigned long long int carry = 0;
carry = (unsigned long long int) a->number[i] * b->number[j];
bn_mult_add(c, i + j, carry);
}
}
c->sign = a->sign ^ b->sign;
if (tmp) {
bn_cpy(tmp, c); // restore c
bn_free(c);
}
}
```
乘法採用簡單的 long multiplication ,若 `c == a || c == b`,就必須配置記憶體來儲存計算結果,避免 `a` 與 `b` 在計算途中就被改變。
```c
static void bn_mult_add(bn *c, int offset, unsigned long long int x)
{
unsigned long long int carry = 0;
for (int i = offset; i < c->size; i++) {
carry += c->number[i] + (x & 0xFFFFFFFF);
c->number[i] = carry;
carry >>= 32;
x >>= 32;
if (!x && !carry) // done
return;
}
}
```
`bn_mult_add` 主要處理乘法運算時,每回合的計算結果累加。
### `bn_lshift`, `bn_rshift`
```c
void bn_lshift(const bn *src, size_t shift, bn *dest)
{
size_t z = bn_clz(src);
shift %= 32; // only handle shift within 32 bits atm
if (!shift)
return;
if (shift > z) {
bn_resize(dest, src->size + 1);
} else {
bn_resize(dest, src->size);
}
/* bit shift */
for (int i = src->size - 1; i > 0; i--)
dest->number[i] =
src->number[i] << shift | src->number[i - 1] >> (32 - shift);
dest->number[0] = src->number[0] << shift;
}
```
主要做大數左移,這裡只考慮最多位移 31 bits 的情形,當 `shift` 大於 leading zero 個數時,會超出原本的大小限制,所以須將 `size + 1`,在重新配置記憶體大小。
`dest->number[0] = src->number[0] << shift;` 這行就是因為當超出原本大小時,可能會有 overflow 的問題,所以須將多出來的部份在放到最一開始新增的位置。
```c
void bn_rshift(bn *src, size_t shift)
{
size_t z = 32 - bn_clz(src);
shift %= 32; // only handle shift within 32 bits atm
if (!shift)
return;
/* bit shift */
for (int i = 0; i < (src->size - 1); i++)
src->number[i] = src->number[i] >> shift | src->number[i + 1]
<< (32 - shift);
src->number[src->size - 1] >>= shift;
if (shift >= z && src->size > 1)
bn_resize(src, src->size - 1);
}
```
與左移類似,只考慮最多位移 31 bits 的情形,當 `shift` 大於 leading zero 個數時,則表示原本的 `number[0]` 的數值以全部右移到下一個陣列,故須重新調整大小,扣除不需要的空間。
### 引入程式
Todo