---
tags: linux2023
---
# 2023q1 Homework3 (fibdrv)
contributed by < `Ahsen-lab` >
## 實驗環境
```shell
$ gcc --version
gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0
Copyright (C) 2019 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
$ 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): 4
On-line CPU(s) list: 0-3
Thread(s) per core: 1
Core(s) per socket: 4
Socket(s): 1
NUMA node(s): 1
Vendor ID: GenuineIntel
CPU family: 6
Model: 165
Model name: Intel(R) Core(TM) i5-10400 CPU @ 2.90GHz
Stepping: 5
CPU MHz: 2904.002
BogoMIPS: 5808.00
Hypervisor vendor: KVM
Virtualization type: full
L1d cache: 128 KiB
L1i cache: 128 KiB
L2 cache: 1 MiB
L3 cache: 48 MiB
NUMA node0 CPU(s): 0-3
```
## 自我檢查清單
- [x] 研讀上述 ==Linux 效能分析的提示== 描述,在自己的實體電腦運作 GNU/Linux,做好必要的設定和準備工作 $\to$ 從中也該理解為何不希望在虛擬機器中進行實驗;
- [ ] 研讀上述費氏數列相關材料 (包含論文),摘錄關鍵手法,並思考 [clz / ctz](https://en.wikipedia.org/wiki/Find_first_set) 一類的指令對 Fibonacci 數運算的幫助。請列出關鍵程式碼並解說
- [ ] 複習 C 語言 [數值系統](https://hackmd.io/@sysprog/c-numerics) 和 [bitwise operation](https://hackmd.io/@sysprog/c-bitwise),思考 Fibonacci 數快速計算演算法的實作中如何減少乘法運算的成本;
- [ ] 學習指出針對大數運算的加速運算和縮減記憶體操作成本的舉措,紀錄你的認知和疑惑
- [ ] 注意到 `fibdrv.c` 存在著 `DEFINE_MUTEX`, `mutex_trylock`, `mutex_init`, `mutex_unlock`, `mutex_destroy` 等字樣,什麼場景中會需要呢?撰寫多執行緒的 userspace 程式來測試,觀察 Linux 核心模組若沒用到 mutex,到底會發生什麼問題。嘗試撰寫使用 [POSIX Thread](https://en.wikipedia.org/wiki/POSIX_Threads) 的程式碼來確認。 $\to$ 搭配閱讀〈[並行和多執行緒程式設計](https://hackmd.io/@sysprog/concurrency)〉
## 作業要求
> [題目](https://hackmd.io/@sysprog/linux2023-fibdrv/%2F%40sysprog%2Flinux2023-fibdrv-a)
> [作業要求](https://hackmd.io/@sysprog/linux2023-fibdrv/https%3A%2F%2Fhackmd.io%2F%40sysprog%2Flinux2023-fibdrv-g#-%E4%BD%9C%E6%A5%AD%E8%A6%81%E6%B1%82)
## 修正 variable-length array (VLA) 警告
若在 Linux module 中使用 variable-length array (VLA) 的機制,編譯過程中會產生警告。因此,為了解決此問題,改用固定大小的陣列來實作求解 Fibonacci 數列的迭代演算法。經修改後,程式碼可以正常編譯和執行。
在新的實作中,迭代演算法從第二項開始進行計算,而不再是從第一項開始。
```diff
static long long fib_sequence(long long k)
{
- /* FIXME: C99 variable-length array (VLA) is not allowed in Linux kernel. */
- long long f[k + 2];
+ if (k < 1)
+ return 0;
- f[0] = 0;
- f[1] = 1;
+ long long f[2] = {1, 1};
- for (int i = 2; i <= k; i++) {
- f[i] = f[i - 1] + f[i - 2];
+ for (int i = 2; i < k; i++) {
+ f[1] += f[0];
+ f[0] = f[1] - f[0];
}
- return f[k];
+ return f[1];
}
```
## 支援大數運算
> 參考 [KYG-yaya573142 / fibdrv](https://github.com/KYG-yaya573142/fibdrv)
引入大數運算的實作,用來計算 $fib(92)$ 以後的數值。
### `bn` 結構體
:::spoiler 完整程式碼
```c
/*
* bignum data structure
* number[0] contains least significant bits
* number[size - 1] contains most significant bits
* sign = 1 for negative number
*/
typedef struct _bn {
unsigned int *number;
unsigned int size;
int sign;
} bn;
```
:::
- `number` - 指向儲存的數值,之後會以 array 的形式來取用
- `size` - 配置的記憶體大小,單位為 `sizeof(unsigned int)`
- `sign` - 0 為正數、1 為負數
### `bn_to_string`
> [kmalloc(9)](https://manpages.debian.org/jessie/linux-manual-3.16/kmalloc.9.en.html)
:::spoiler 完整程式碼
```c
/*
* output bn to decimal string
* Note: the returned string should be freed with the kfree()
*/
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 + src->sign;
char *s = kmalloc(len, GFP_KERNEL);
char *p = s;
memset(s, '0', len - 1);
s[len - 1] = '\0';
/* src.number[0] contains least significant bits */
for (int i = src->size - 1; i >= 0; i--) {
/* walk through every bit of bn */
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;
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_fib_fdoubling`
使用 Fast Doubling 的技巧,在大數運算中計算 Fibonacci 數。
:::spoiler 完整程式碼
```c
/*
* calc n-th Fibonacci number and save into dest
* using fast doubling algorithm
*/
void bn_fib_fdoubling(bn *dest, unsigned int n)
{
bn_resize(dest, 1);
if (n < 2) { // Fib(0) = 0, Fib(1) = 1
dest->number[0] = n;
return;
}
bn *f1 = dest; /* F(k) */
bn *f2 = bn_alloc(1); /* F(k+1) */
f1->number[0] = 0;
f2->number[0] = 1;
bn *k1 = bn_alloc(1);
bn *k2 = bn_alloc(1);
/* walk through the digit of n */
for (unsigned int i = 1U << 31; i; i >>= 1) {
/* F(2k) = F(k) * [ 2 * F(k+1) – F(k) ] */
bn_cpy(k1, f2);
bn_lshift(k1, 1);
bn_sub(k1, f1, k1);
bn_mult(k1, f1, k1);
/* F(2k+1) = F(k)^2 + F(k+1)^2 */
bn_mult(f1, f1, f1);
bn_mult(f2, f2, f2);
bn_cpy(k2, f1);
bn_add(k2, f2, k2);
if (n & i) {
bn_cpy(f1, k2);
bn_cpy(f2, k1);
bn_add(f2, k2, f2);
} else {
bn_cpy(f1, k1);
bn_cpy(f2, k2);
}
}
// return f[0]
bn_free(f2);
bn_free(k1);
bn_free(k2);
}
```
:::
- `dest` - 用來儲存 $fib(n)$
- `n` - 找出第 $n$ 個 Fibonacci 數
```c
bn_resize(dest, 1);
if (n < 2) { // Fib(0) = 0, Fib(1) = 1
dest->number[0] = n;
return;
}
bn *f1 = dest; /* F(k) */
bn *f2 = bn_alloc(1); /* F(k+1) */
f1->number[0] = 0;
f2->number[0] = 1;
bn *k1 = bn_alloc(1);
bn *k2 = bn_alloc(1);
```
首先確保 `dest` 的 size 為 1,若 `n < 2` 則 $fib(n) = n$ 。
初始化 `f1` 和 `f2` 為 `0` 和 `1` ,分別對應 $fib(0)$ 和 $fib(1)$ ,再給定兩個變數 `k1` 和 `k2` 分別用來儲存 $fib(k)$ 和 $fib(k + 1)$ 。
$$
\begin{split}
fib(2k) &= fib(k)[2fib(k+1) - fib(k)] \\
fib(2k+1) &= fib(k+1)^2+fib(k)^2
\end{split}
$$
根據上述公式並配合 [Bottom-up Fast Doubling](https://hackmd.io/@sysprog/linux2023-fibdrv/%2F%40sysprog%2Flinux2023-fibdrv-d#Bottom-up-Fast-Doubling) 的做法,假設要找第 $n$ 個 Fibonacci 數,只需要從左到右依序檢查每個位元,步驟如下:
1. 計算 `k1 = fib(2k)` 、 `k2 = fib(2k+1)`
2. 檢查當前位元 `i` ,若不存在的話跳到第 3 步,否則(假設目前為 $n = k$):
- `i = 0` : $n = 2k$
- `f1 = fib(2k) = k1`
- `f2 = fib(2k+1) = k2`
- `i = 1` : $n = 2k + 1$
- `f1 = fib(2k+1) = k2`
- `f2 = fib(2k+2) = k1 + k2`
- 回到第 2 步
3. 此時 $n$ 為目標數,回傳 `f1` 也就是 $fib(n)$
對應的程式碼:
```c
/* walk through the digit of n */
for (unsigned int i = 1U << 31; i; i >>= 1) {
/* F(2k) = F(k) * [ 2 * F(k+1) – F(k) ] */
bn_cpy(k1, f2);
bn_lshift(k1, 1);
bn_sub(k1, f1, k1);
bn_mult(k1, f1, k1);
/* F(2k+1) = F(k)^2 + F(k+1)^2 */
bn_mult(f1, f1, f1);
bn_mult(f2, f2, f2);
bn_cpy(k2, f1);
bn_add(k2, f2, k2);
if (n & i) {
bn_cpy(f1, k2);
bn_cpy(f2, k1);
bn_add(f2, k2, f2);
} else {
bn_cpy(f1, k1);
bn_cpy(f2, k2);
}
}
```
## 量化執行時間
為了分析並改善 Fibonacci 數的計算效率,需要測量並量化執行時間,我們會使用 `fib_read()` 來計算 Fibonacci 數列,使用者可在 user space 透過指定不同的 offest 作為 Fibonacci 數列的 $n$ 然後透過 `read` 來輸出 $fib(n)$。
在計算的過程中,傳入 `fib_read()` 的 `size` 參數並沒有被使用到,因此我將 `size` 當作選擇演算法的參數:
- case 0 :
- 原始迭帶版本的計算方式,因有缺陷,最多只能計算到 $fib(92)$
- case 1 :
- 結合了 Fast Doubling 的大數運算函式 `bn_fib_fdoubling()`
在 `case 1` 中,首先用 `bn_alloc` 創建了一個新的大數 `fib` (用於計算 Fibonacci 數列),然後調用 `bn_fib_fdoubling` 函式來計算第 `*offset` 項的 Fibonacci 數。計算完成後,使用 `bn_to_string` 將結果轉換為字串,並用 `copy_to_user` 將其複製到 user space 的 `buf` 中,最後回傳剩餘的未複製成功的字節數。如果一切正常,該函式回傳 0。
```c
static ssize_t fib_read(struct file *file,
char *buf,
size_t size,
loff_t *offset)
{
bn *fib;
switch (size) {
case 0: /* fib_sequence */
if (*offset > 92)
*offset = 92;
return (ssize_t) fib_sequence(*offset);
case 1: /* bn_fib_fdoubling */
fib = bn_alloc(1);
kt = ktime_get();
bn_fib_fdoubling(fib, *offset);
kt = ktime_sub(ktime_get(), kt);
char *p = bn_to_string(fib);
size_t len = strlen(p) + 1;
k2u = ktime_get();
size_t left = copy_to_user(buf, p, len);
k2u = ktime_sub(ktime_get(), k2u);
bn_free(fib);
kfree(p);
return (ssize_t) left;
}
return 0;
}
```
### Fibonacci 數列計算的時間
在 Linux 核心模組中,使用 ktime 系列的 API 來測量 Fibonacci 數列計算的時間,參考 [核心模式的時間測量](https://hackmd.io/@sysprog/linux2023-fibdrv/%2F%40sysprog%2Flinux2023-fibdrv-c#%E6%A0%B8%E5%BF%83%E6%A8%A1%E5%BC%8F%E7%9A%84%E6%99%82%E9%96%93%E6%B8%AC%E9%87%8F) 的做法
```c
kt = ktime_get();
bn_fib_fdoubling(fib, *offset);
kt = ktime_sub(ktime_get(), kt);
```
`kt` 為在核心模式下執行 `bn_fib_fdoubling(fib, *offset)` 的時間,首先,在計算之前先用 `ktime_get()` 取得當前時間,在計算完成後,再次呼叫 `ktime_get()` ,用計算後的時間減去計算前的時間,即可得到在核心模式中 Fibonacci 數列計算的時間。
### 從 kernel 傳遞到 user space 的時間
Fibonacci 數列計算的結果會儲存在 `buf` 裡面,需要透過 `copy_to_user` 函式來傳回 user space , `k2u` 會儲存資料從 kernel 傳遞到 user space 的時間。
```c!
k2u = ktime_get();
size_t left = copy_to_user(buf, p, len);
k2u = ktime_sub(ktime_get(), k2u);
```
這段程式碼的目的是將 Fibonacci 數列計算的結果傳回給 user space,並測量從 kernel 傳遞到 user space 所需的時間。
`copy_to_user` 函式用於將計算結果從 kernel 的記憶體區域 `p` 複製到 user space 的緩衝區 `buf` 中。在這之前,使用 `ktime_get()` 函式獲取當前時間,在傳遞結束之後,再次呼叫 `ktime_get()` 函式,獲取傳遞後時間點,並使用 `ktime_sub()` 函式計算兩者之差,即為從 kernel 傳遞到 user space 所需的時間,儲存在 `k2u` 變數中。
### `read()` 在 user space 的執行時間
`fibdrv` 是一個 character device ,使用者可透過定義相關的函式並使用存取檔案的系統呼叫 (如 `read()`) 來與裝置互動,也就是說使用者空間 ( userspace ) 的程式可透過 `read()` 系統呼叫來得到 `fibdrv` 裝置的輸出。
```c
clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &start);
long long sz = read(fd, buf, 1);
clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &end);
```
在 user space 中,我使用 [`clock_gettime(3)`](https://linux.die.net/man/3/clock_gettime) 函式來計算 `read()` 系統呼叫的執行時間。
```c
#include <time.h>
int clock_gettime(clockid_t clk_id, struct timespec *tp);
```
`clk_id` 參數可用來指定不同的計時器
- `CLOCK_PROCESS_CPUTIME_ID` : 當前進程在 CPU 上執行的時間。
這個計時器不會包括行程被阻塞的時間,因此可以更好地測量行程的實際執行時間。
具體而言,首先獲取當前時間點,稱為 `start` ,接著執行 `read()` 系統呼叫,獲取讀取的字元數 sz,最後再次使用 `clock_gettime()` 函式獲取當前時間點,稱為 end 時間點。使用 end 減去 start 可得到 read() 系統呼叫在使用者空間中的執行時間。