---
tags: linux2022
---
# 2022q1 Homework3 (fibdrv)
contributed by < [`jj97181818`](https://github.com/jj97181818) >
## 自我檢查清單
- [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 數快速計算演算法的實作中如何減少乘法運算的成本;
- [ ] 研讀 [KYG-yaya573142 的報告](https://hackmd.io/@KYWeng/rkGdultSU),指出針對大數運算,有哪些加速運算和縮減記憶體操作成本的舉措?
- [ ] `lsmod` 的輸出結果有一欄名為 `Used by`,這是 "each module's use count and a list of referring modules",但如何實作出來呢?模組間的相依性和實際使用次數 ([reference counting](https://en.wikipedia.org/wiki/Reference_counting)) 在 Linux 核心如何追蹤呢?
> 搭配閱讀 [The Linux driver implementer’s API guide » Driver Basics](https://www.kernel.org/doc/html/latest/driver-api/basics.html)
- [ ] 注意到 `fibdrv.c` 存在著 `DEFINE_MUTEX`, `mutex_trylock`, `mutex_init`, `mutex_unlock`, `mutex_destroy` 等字樣,什麼場景中會需要呢?撰寫多執行緒的 userspace 程式來測試,觀察 Linux 核心模組若沒用到 mutex,到底會發生什麼問題。嘗試撰寫使用 [POSIX Thread](https://en.wikipedia.org/wiki/POSIX_Threads) 的程式碼來確認。
- [ ] timer interrupt, IPI
## 效能分析
### 限定 CPU 給特定的程式使用
1. 在 `/etc/default/GRUB` 中的一行 `GRUB_CMDLINE_LINUX_DEFAULT="quiet splash"` 加入 `isolcpus=3`,指定編號 3 的 CPU 核心就不會被 Linux 的排程器安排行程,只有用 `taskset` 設定的 process 可以使用該 CPU 核心:
```shell
GRUB_CMDLINE_LINUX_DEFAULT="quiet splash isolcpus=3"
```
2. 執行 `sudo update-grub` 後關機,然後透過 `cat /sys/devices/system/cpu/isolated` 可以確認是否有設定成功。這裡有成功回傳 3。
### 指定 CPU 核心來執行一個新的程式
1. 指定保留的 CPU 核心來跑 `./client`
```shell
sudo taskset 0x8 ./client
```
### 排除干擾效能分析的因素
1. 抑制 address space layout randomization (ASLR)
不讓 ASLR 利用隨機方式配置資料位址。
```shell
sudo sh -c "echo 0 > /proc/sys/kernel/randomize_va_space"
```
+ 0 = 關閉
+ 1 = 半隨機
+ 2 = 全隨機
2. 用以下 shell script,檔名為 performance.sh:
```shell
for i in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor
do
echo performance > ${i}
done
```
再用 `sudo sh performance.sh` 執行,固定 CPU 頻率。
3. 針對 Intel 處理器,關閉 turbo mode
```shell
sudo sh -c "echo 1 > /sys/devices/system/cpu/intel_pstate/no_turbo"
```
CPU 就不會自動超頻。原本是 0,改為 1。
## kernel space 傳遞到 user space
### copy_to_user
原本的 `fib_read` 從 kernel space 傳遞到 user space 時,會碰到只能回傳 `ssize_t` 大小的值,在 LP64 資料模式中僅 64 位元,會使得 Fibonacci 數只能算到 $F(92)$ = 7540113804746346429,$F(93)$ 之後的皆無法正確傳送。
```c
static ssize_t fib_read(struct file *file,
char *buf,
size_t size,
loff_t *offset)
{
return (ssize_t) fib_sequence(*offset);
}
```
因此改用 `copy_to_user` 將 Fibonacci 數從 kernel space 傳遞到 user space。
```c
unsigned long copy_to_user (void __user * to, const void * from, unsigned long n);
```
將 `from` 的資料複製到 `to`。
+ to: Destination address, in user space.
+ from: Source address, in kernel space.
+ n: Number of bytes to copy.
因為 `fib_read` 從 user space 取得的參數 `buf` 的型別是 `char*`,所以要將 Fibonacci 數 `fib` 從 `long long` 轉成 `char*`。
```c
static ssize_t fib_read(struct file *file,
char *buf,
size_t size,
loff_t *offset)
{
char fib[64];
long long n = fast_doubling(*offset);
int i = 0;
if (n == 0) {
fib[i] = '0';
i++;
}
while (n) {
fib[i] = (n % 10) + '0';
n /= 10;
i++;
}
fib[i] = '\0';
int start = 0, end = i - 1;
while (start < end) {
int temp = fib[start];
fib[start] = fib[end];
fib[end] = temp;
start++;
end--;
}
copy_to_user(buf, fib, 64);
return (ssize_t) fib_sequence(*offset);
}
```
後來實作大數運算,是將 `BigN*` 型別的 Fibonacci 數轉成 `char*`,然後同樣使用 copy_to_user。
```c
static ssize_t fib_read(struct file *file,
char *buf,
size_t size,
loff_t *offset)
{
char fib[64];
BigN *n = fib_sequence_BigN(*offset);
BigN_to_string(fib, n);
copy_to_user(buf, fib, 64);
return (ssize_t) fib_sequence(*offset);
}
```
## 計算 Fibonacci 數的方法
### 一、iterative
最基本計算 Fibonacci 數的方式:
$$
a_{n+1} = a_n + b_n \\ b_{n+1} = a_n
$$
原本提供的 `fib_sequence` 程式有 VLA 的問題:
```c
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];
f[0] = 0;
f[1] = 1;
for (int i = 2; i <= k; i++) {
f[i] = f[i - 1] + f[i - 2];
}
return f[k];
}
```
其實這裡不需要處理 VLA 的問題,因為可以用更簡單的方式來儲存計算過程:透過 3 個型別為 `long long` 的變數,取代原本的 variable-length array。
```c
static long long fib_sequence(long long k)
{
if (k == 0) return 0;
long long a, b;
a = 0;
b = 1;
for (int i = 2; i <= k; i++) {
long long c = a + b;
a = b;
b = c;
}
return b;
}
```
不傾向使用 VLA 是因為如果變數是很大的值,可能會導致 buffer overflow,放在 stack 中,會因為太大蓋到其他的資料。
### 二、fast doubling
當需要計算 2k(偶數)或 2k + 1(奇數)的 Fibonacci 數時,只要使用到 k 和 k + 1 的 Fibonacci 數,這表示不需要將 2k 之前所有數字的 Finbonacci 數都算一遍,可以減少計算量。
$$
\begin{split}
F(2k) &= F(k)[2F(k+1) - F(k)] \\
F(2k+1) &= F(k+1)^2+F(k)^2
\end{split}
$$
先將要求的 Fibonacci 數 `n` 換成二進位來看,從 MSB 開始判斷是 0 還是 1
+ 遇到 0,求 F(2k) 和 F(2k + 1)
+ 遇到 1,求 F(2k + 1) 和 F(2k + 2)
一直看到 `n` 的 LSB 為止。
參考 [K04: fibdrv](https://hackmd.io/@sysprog/linux2022-fibdrv#-%E8%B2%BB%E6%B0%8F%E6%95%B8%E5%88%97) fast doubling 的虛擬瑪,程式碼如下:
因為加法的結果都會存在資料型態為 long long 的變數中,所以只能算到第 92 個 Fibonacci 數,所以這裡的 `n` 最大到 92,92~10~ = 1011100~2~,因此最多只需要將 mask `i` 先向左位移六位即可。
```c
static long long fast_doubling(long long n)
{
long long a = 0, b = 1;
for (unsigned int i = 1U << 6; i; i >>= 1) {
long long t1 = a * (2 * b - a); // 2k
long long t2 = a * a + b * b; // 2k+1
if (n & i) {
a = t2; // 2k + 1
b = t1 + t2; // 2k + 2
} else {
a = t1; // 2k
b = t2; // 2k + 1
}
}
return a;
}
```
![](https://i.imgur.com/VbMQjBg.png)
由實驗可看出,左移 6 位 fast doubling 的速度,的確比左移 31 位的 fast doubling 來得快,因為需要跑的迴圈數比較少。
### 三、fast doubling with clz
使用 `__builtin_clz` 計算 leading zero 的個數,加速 fast doubling,減少不必要的迴圈執行。
```c
static long long fast_doubling_clz(long long n)
{
long long a = 0, b = 1;
for (unsigned int i = 1U << (31 - __builtin_clz(n)); i; i >>= 1) {
long long t1 = a * (2 * b - a); // 2k
long long t2 = a * a + b * b; // 2k+1
if (n & i) {
a = t2;
b = t1 + t2;
} else {
a = t1;
b = t2;
}
}
return a;
}
```
![](https://i.imgur.com/f0DUroM.png)
`fast doubling` 左移 6 位和 `fast doubling with clz` 看起來不會差太多。
## 時間量測
### kernel space
因為 `fib_write` 在這個 kernel module 裡面沒有做什麼事,所以拿來測量 Fibonacci 數演算法的時間,參考 [KYWeng](https://hackmd.io/@KYWeng/rkGdultSU#%E9%87%8F%E6%B8%AC%E6%99%82%E9%96%93%E7%9A%84%E6%96%B9%E5%BC%8F) 的做法,利用傳進來的 `size` 參數,選擇要測量的演算法。
+ 0: fib_sequence
+ 1: fast_doubling
+ 2: fast_doubling_clz
```c
static ktime_t kt;
/* write operation is skipped */
static ssize_t fib_write(struct file *file,
const char *buf,
size_t size,
loff_t *offset)
{
long long res = 0;
if (size == 0) {
kt = ktime_get();
res = fib_sequence(*offset);
kt = ktime_sub(ktime_get(), kt);
} else if (size == 1) {
kt = ktime_get();
res = fast_doubling(*offset);
kt = ktime_sub(ktime_get(), kt);
} else if (size == 2) {
kt = ktime_get();
res = fast_doubling_clz(*offset);
kt = ktime_sub(ktime_get(), kt);
}
return ktime_to_ns(kt);
}
```
![](https://i.imgur.com/5QAeiSq.png)
三個演算法測量出來的時間,看起來 `iterative` 最花時間,再來是 `fast doubling with clz`,最後是 `fast doubling`。但是有用到 `clz` 的 `fast doubling` 比沒有 `clz` 的執行更慢還滿奇怪的。
同樣是參考 [KYWeng](https://hackmd.io/@KYWeng/rkGdultSU#%E9%87%8F%E6%B8%AC%E6%99%82%E9%96%93%E7%9A%84%E6%96%B9%E5%BC%8F) 用 `escape` 函式來避免編譯器做最佳化時,直接不執行沒用到其結果的程式。
+ `__attribute__((always_inline))` 會確保 escape 這個 inline function 永遠會展開
+ `__asm__ volatile ("" : : "g"(p) : "memory");` 這行 assembly 的 volatile 會提醒編譯器該變數可能隨時改變,阻止編譯器做優化。
```c
__attribute__((always_inline))
static inline void escape(void *p) {
__asm__ volatile ("" : : "g"(p) : "memory");
}
```
用 escape 函式來保護 `res` 不被編譯器優化影響,並確實計算 `fast_doubling` 和 `fast_doubling_clz`。
```diff
static ktime_t kt;
/* write operation is skipped */
static ssize_t fib_write(struct file *file,
const char *buf,
size_t size,
loff_t *offset)
{
long long res = 0;
if (size == 0) {
kt = ktime_get();
res = fib_sequence(*offset);
kt = ktime_sub(ktime_get(), kt);
} else if (size == 1) {
kt = ktime_get();
res = fast_doubling(*offset);
kt = ktime_sub(ktime_get(), kt);
} else if (size == 2) {
kt = ktime_get();
res = fast_doubling_clz(*offset);
kt = ktime_sub(ktime_get(), kt);
}
+ escape(&res);
return ktime_to_ns(kt);
}
```
![](https://i.imgur.com/SA1iA8f.png)
實驗結果看起來比較合理了,`fast doubling with clz` 執行的速度比 `fast doubling` 都來得快,然後 `iterative` 隨著 Fibonacci 數愈來愈大,執行時間愈來愈長,慢慢地超過 fastdoubling 做法所花的時間。
## 大數運算
因為 `long long` 可以儲存的數字只夠存到 F(92),從 F(93) 以後的數字都會 overflow,所以要自定義一個大數的資料結構,並定義大數的相關運算。
### 結構
```c
typedef struct _BigN {
unsigned int *val;
unsigned int size;
int sign;
} BigN;
```
### to_string
```c
static void BigN_to_string(char* s, BigN *num) {
// log10(x) = log2(x) / log2(10) ~= log2(x) / 3.322
size_t len = (8 * sizeof(int) * num->size) / 3 + 2 + num->sign;
char *p = s;
memset(s, '0', len - 1);
s[len - 1] = '\0';
for (int i = num->size - 1; i >= 0; i--) {
for (unsigned int d = 1U << 31; d; d >>= 1) {
/* binary -> decimal string */
int carry = ((d & num->val[i]) > 0);
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 (num->sign)
*(--p) = '-';
memmove(s, p, strlen(p) + 1);
}
```
### add
```c
static BigN* BigN_add(const BigN *a, const BigN *b)
{
unsigned int size = ((a->size > b->size) ? a->size : b->size) + 1;
BigN *sum = BigN_new(size);
unsigned int carry = 0;
unsigned long s = 0;
for (int i = 0; i < sum->size; i++) {
unsigned int tmp1 = (a->size) > i ? a->val[i] : 0;
unsigned int tmp2 = (b->size) > i ? b->val[i] : 0;
s = (unsigned long)tmp1 + tmp2 + carry;
sum->val[i] = s & UINT_MAX;
carry = 0;
if (s > UINT_MAX) {
carry = 1;
}
}
if (sum->val[sum->size - 1] == 0) {
sum->size -= 1;
}
return sum;
}
```
### new
```c
static BigN* BigN_new(unsigned int size) {
BigN *new = kmalloc(sizeof(BigN), GFP_KERNEL);
new->size = size;
new->sign = 0;
new->val = kmalloc(sizeof(unsigned int) * size, GFP_KERNEL);
memset(new->val, 0, sizeof(unsigned int) * size);
return new;
}
```
### free
```c
static void BigN_free(BigN *num) {
kfree(num->val);
kfree(num);
}
```
### 用大數實作的 iterative
```c
static BigN* fib_sequence_BigN(long long k)
{
BigN *a = BigN_new(1); // f(0) = 0
BigN *b = BigN_new(1); // f(1) = 1
b->val[0] = 1;
BigN *sum;
if (k == 0) return a;
for (int i = 2; i <= k; i++) {
sum = BigN_add(a, b); // f[i] = f[i - 1] + f[i - 2]
BigN_free(a);
a = b;
b = sum;
}
BigN_free(a);
return b;
}
```
能算出 f(93) 以後的值了
```
Reading from /dev/fibonacci at offset 92, 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.
```