---
tags: linux kernel class
---
# 2023q1 Homework3 (fibdrv)
contributed by < `willwillhi1` >
## 實驗環境
```shell
$ gcc --version
gcc (Ubuntu 9.4.0-1ubuntu1~20.04.1) 9.4.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): 8
On-line CPU(s) list: 0-7
Thread(s) per core: 2
Core(s) per socket: 4
Socket(s): 1
NUMA node(s): 1
Vendor ID: GenuineIntel
CPU family: 6
Model: 142
Model name: Intel(R) Core(TM) i5-10210U CPU @ 1.60GHz
Stepping: 12
CPU MHz: 2100.000
CPU max MHz: 4200.0000
CPU min MHz: 400.0000
BogoMIPS: 4199.88
Virtualization: VT-x
L1d cache: 128 KiB
L1i cache: 128 KiB
L2 cache: 1 MiB
L3 cache: 6 MiB
NUMA node0 CPU(s): 0-7
```
## 開發紀錄
### 執行 `make check` 遇到的問題
```
$ make check
cc -o client client.c
make -C /lib/modules/5.15.0-52-generic/build M=fibdrv modules
make[1]: Entering directory '/usr/src/linux-headers-5.15.0-52-generic'
CC [M] fibdrv/fibdrv.o
fibdrv/fibdrv.c: In function ‘fib_sequence’:
fibdrv/fibdrv.c:30:5: warning: ISO C90 forbids variable length array ‘f’ [-Wvla]
30 | long long f[k + 2];
| ^~~~
Building modules, stage 2.
MODPOST 1 modules
CC [M] fibdrv/fibdrv.mod.o
LD [M] fibdrv/fibdrv.ko
make[1]: Leaving directory '/usr/src/linux-headers-5.15.0-52-generic'
make unload
make[1]: Entering directory 'fibdrv'
sudo rmmod fibdrv || true >/dev/null
rmmod: ERROR: Module fibdrv is not currently loaded
make[1]: Leaving directory 'fibdrv'
make load
make[1]: Entering directory 'fibdrv'
sudo insmod fibdrv.ko
insmod: ERROR: could not insert module fibdrv.ko: Operation not permitted
make[1]: *** [Makefile:23: load] Error 1
make[1]: Leaving directory 'fibdrv'
make: *** [Makefile:37: check] Error 2
```
* 關閉 Secure Boot:
```shell
$ sudo mokutil --disable-validation
```
* 重新開機,選擇 ```Change Secure Boot State```
* 之後就可以正常執行
```
$ make check
make -C /lib/modules/5.15.0-52-generic/build M=/home/will/fibdrv modules
make[1]: Entering directory '/usr/src/linux-headers-5.15.0-52-generic'
make[1]: Leaving directory '/usr/src/linux-headers-5.15.0-52-generic'
make unload
make[1]: Entering directory '/home/will/fibdrv'
sudo rmmod fibdrv || true >/dev/null
[sudo] password for will:
rmmod: ERROR: Module fibdrv is not currently loaded
make[1]: Leaving directory '/home/will/fibdrv'
make load
make[1]: Entering directory '/home/will/fibdrv'
sudo insmod fibdrv.ko
make[1]: Leaving directory '/home/will/fibdrv'
sudo ./client > out
make unload
make[1]: Entering directory '/home/will/fibdrv'
sudo rmmod fibdrv || true >/dev/null
make[1]: Leaving directory '/home/will/fibdrv'
Passed [-]
f(93) fail
input: 7540113804746346429
expected: 12200160415121876738
```
### 實驗環境的設定
* 撰寫 `perfomance.sh`,讓 cpu 只注重效率,將頻率固定在其支持的最高運行頻率上。
```shell
for i in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor
do
echo performance > ${i}
done
```
* 抑制 `address space layout randomization (ASLR)`,`ASLR` 全名為 `Address Space Layout Randomization` (地址空間佈局隨機化),它可以將 process 的某些內存空間地址進行隨機化來增加入侵者預測目的地址的難度,從而降低被入侵的風險。
```shell
$ sudo sh -c "echo 0 > /proc/sys/kernel/randomize_va_space"
```
* 限定 CPU 給特定的程式使用,修改 ```/etc/default/grub``` 內的 ```GRUB_CMDLINE_LINUX_DEFAULT``` 為 ```
GRUB_CMDLINE_LINUX_DEFAULT="quiet splash isolcpus=7"
```,修改完成後需 ```sudo update-grub``` 來更新設定,重開機後即生效
* 可用 ```cat /sys/devices/system/cpu/isolated```
* 或用 ```taskset -cp 1```,來確認是否生效
```shell
$ taskset -cp 1
pid 1's current affinity list: 0-6
```
* 之後用 `taskset -c 7 command` 執行測試程式
## 開發過程
### 前期準備
```shell
cat /sys/class/fibonacci/fibonacci/dev
```
輸出結果代表註冊的 `fibdrv` 的 [Major and Minor Numbers](http://www.makelinux.net/ldd3/chp-3-sect-2.shtml)
```shell
$ cat /sys/class/fibonacci/fibonacci/dev
509:0
```
`509` 代表動態申請的主設備號
也可以用 `ls -l /dev | grep fib` 找到
```
crw------- 1 root root 509, 0 3月 2 15:57 fibonacci
```
* 編譯 `linux kernel moudle`
* 修改完 `fibdrv.c` 後,用 `make all` 來編譯
* 之後先用 `make unload` 移除之前的模組
* 再用 `make load` 載入新編譯後的模組
### fast-doubling 加速 Fibonacci 運算
以下為原始版本的演算法:
```c
static long long easy_fib(int k)
{
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];
}
```
用作業提示的 `fast-doubling` 實作 `fibonacci`,參考作業說明和 [Calculating Fibonacci Numbers by Fast Doubling](https://chunminchang.github.io/blog/post/calculating-fibonacci-numbers-by-fast-doubling) 的解說。
公式列在下方:
$$
\begin{align}
&F(2k) = 2F(k)\times F(k+1) - F(k)^2 \\
&F(2k+1) = F(k)^2 + F(k+1)^2
\end{align}
$$
修改過後的程式碼:
以 `n = 0b101` 為例,
當迴圈走到最左邊的 1 時(也就是目前總共是 `0b1`),會先計算 `F(2K)` 和 `F(2K+1)`,之後在用這兩個結果算出 `a = F(2K+1)` 和 `b = F(2K+2) = F(2K) + F(2K+1)`。
接著往右移一位遇到 0,相當於把先前的結果乘二即可(`0b1` -> `0b10`),也就是 6、7 行。
最後遇到 1,就是將上一步的結果乘二 (`0b10` -> `0b100`),再加 1 (9 ~ 11 行) 得到 `0b101`。
```c=
static long long double_fib(int n)
{
long long a = 0;
long long b = 1;
for (int i = 31; i >= 0; --i) {
long long c = a * (2 * b - a);
long long d = a * a + b * b;
if ((n >> i) & 1) {
a = d;
b = c + d;
} else {
a = c;
b = d;
}
}
return a;
}
```
其中
```c
for (int i = 31; i >= 0; --i)
```
可以用 `__builtin_clz` 加速
```c
int i = 31 - __builtin_clz(n);
```
這麼做可以不需要每次迴圈都從 `31` 開始,透過 `clz` 可以直接從最高位 `1` 開始做,接下來用 `gunplot` 來製圖
![](https://i.imgur.com/Zcnr3hm.png)
`if-else` 的部份
```c
if ((n >> i) & 1) {
a = d;
b = c + d;
} else {
a = c;
b = d;
}
```
可以用以下程式代替
```c
mask = -!!(n & (1UL << i));
a = (c & ~mask) + (d & mask);
b = (c & mask) + d;
```
* `n & (1UL << i)` 表示只保留 `n` 的第 `i` 個 bit,其餘 `bits` 都是零,而 `-!!` 這個操作可以讓 `mask`:
1. `mask = -1`,如果 n 的第 `i` 個 `bit` 為 `1`
2. `mask = 0`,如果 n 的第 `i` 個 `bit` 為 `0`
最後的程式會是:
```c
static long long fib_sequence(int n)
{
long long a = 0;
long long b = 1;
long long mask;
for (int i = 31 - __builtin_clz(n); i >= 0 ; --i) {
long long c = a * (2 * b - a);
long long d = a * a + b * b;
mask = -!!(n & (1UL << i));
a = (c & ~mask) + (d & mask);
b = (c & mask) + d;
}
return a;
}
```
接下來測試不同實作的時間分佈
`write` 實作 `naive` 版本
`read` 實作 `fast doubling` 版本
```c
static inline long elapse(struct timespec start, struct timespec end)
{
return ((long) 1.0e+9 * end.tv_sec + end.tv_nsec) -
((long) 1.0e+9 * start.tv_sec + start.tv_nsec);
}
for (int i = 0; i <= offset; i++) {
lseek(fd, i, SEEK_SET);
clock_gettime(CLOCK_REALTIME, &t1);
write(fd, write_buf, strlen(write_buf));
clock_gettime(CLOCK_REALTIME, &t2);
printf("%d ", i);
printf("%ld ", elapse(t1, t2));
clock_gettime(CLOCK_REALTIME, &t1);
read(fd, buf, 1);
clock_gettime(CLOCK_REALTIME, &t2);
printf("%ld\n", elapse(t1, t2));
}
```
然後用 `gunplot` 製圖
![](https://i.imgur.com/ofqwEWP.png)
> 圖例應為 naive vs. fast doubling
> :notes: jserv
### kernel, user space & system call time
使用作業說明提及的 `ktime` 在核心模組中測量執行時間,實作在 `write` 函式裡,並回傳 `kernel space` 執行時間。
```c
static ssize_t fib_write(struct file *file,
const char *buf,
size_t size,
loff_t *offset)
{
ktime_t k1, k2;
k1 = ktime_get();
long long result = double_fib(*offset);
k2 = ktime_sub(ktime_get(), k1);
return ktime_to_ns(k2);
}
```
`user space` 在呼叫 `write` 前後用 `clock_gettime` 計算 `user space` 的時間,最後可以把 `kernel time`, `user time` 相減得到 `system call` 的時間。
```c
for (int i = 0; i <= offset; i++) {
lseek(fd, i, SEEK_SET);
clock_gettime(CLOCK_REALTIME, &t1);
kernel_time = write(fd, buf, 1);
clock_gettime(CLOCK_REALTIME, &t2);
printf("%d ", i);
/* user space execute time */
printf("%ld ", elapse(t1, t2));
/* kernel space execute time */
printf("%ld ", kernel_time);
/* system call overhead between kernel and user space */
printf("%ld\n", elapse(t1, t2)-kernel_time);
}
```
![](https://i.imgur.com/7AaJ9W2.png)
### 大數運算
先看懂 [作業說明的大數運算部份](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)
#### struct bn
使用以下結構體來進行大數運算
* `number` 是儲存整個數值的陣列,以 `unsigned int` 為單位也就是 `32 bits`,由低位元依序存到高位元。
* `size` 代表該陣列的長度。
* `sign` 代表該數為正或負。
```c=
typedef struct _bn {
u8 *number;
unsigned int size;
int sign;
} bn;
```
#### bn_clz
計算 `src` 的陣列的 `leading zero`。
```c
/* count leading zeros of src*/
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;
}
```
#### bn_to_string
`for` 迴圈要是主要處理轉成二進位轉成十進位字串的地方,每 `32 bits` 處理一次。
* 由 `src->number` 的最高位(msb)逐一放入字串的最高位(msb)並處理。
* 用 `carry` 來代表是否進位,初始化為 `!!(d & src.number[i])`,代表該 `bit` 是否為 `0` 或 `1`。
* `for (int j = len - 2; j >= 0; j--)`,每次累加一個 `bit` 時都要跑整個字串的長度的迴圈。
* 累加的方式為 `s[j] += s[j] - '0' + carry`,原本的數值乘以二再加上 `carry`,最後還要處理進位問題。
```c
/*
* output bn to decimal string
* Note: the returned string should be freed with kfree()
*/
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;
}
```
#### bn_add
`bn_add` 是先用判斷正負號後再依照案例使用 `bn_do_add` 和 `bn_do_sub` 進行無號整數的計算。
```c
/* c = a + b
* Note: work for c == a or c == b
*/
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;
}
}
}
```
#### bn_do_add
進行無號整數加法 (c = |a| + |b|)
* `DIV_ROUNDUP(x, len)` 的定義為 `(((x) + (len) -1) / (len))`,簡單來說就是除法無條件進位。
* `!d` 應該不用加,因為 `d` 不可能是 `0`。
* `for` 迴圈部份則是執行簡單的加法。
```c=
/* |c| = |a| + |b| */
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);
}
```
#### bn_do_sub
* `|a| > |b|` 條件一定要成立。
* 由於 `carry` 計算時可能會需要與 `1LL << 32` 相加,超過 `unsigned int` 大小,所以 `carry` 型別要宣告為 `long long int`。
* `(long long int) tmp1 - tmp2 - carry` 結果若小於 `0` 就需要借位。
* 最後 `bn_resize(c, c->size - d)`,把多餘的零去掉。
```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);
}
```
#### bn_mult
邏輯就像一般的乘法一樣,慢慢累加上去
另外處理 `c` 有可能是 `a` 或 `b` 的情況,會先用 `tmp` 來存 `c` 原本的位址避免 `a` 或 `b` 的值被改變,等最後做完乘法再 `swap` 回來。
```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;
for (int i = 0; i < c->size; i++)
c->number[i] = 0;
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_swap(tmp, c); // restore c
bn_free(c);
}
}
```
#### bn_lshift
原本的版本程式碼如下,會限制 `shift` 在 `31` 以下,然後根據 `shift` 是否大於 `leading zero` 來判斷重新 `resize dest` 的大小。
```c
/* left bit shift on bn (maximun shift 31) */
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;
}
```
後來我在進行 `10110001000110010010010011100001`(剛好 32 bits) 向左 `shift 1` 時會出錯輸出 `01100010001100100100100111000010`,很明顯是最高位的 `1` 被吃掉了,後來我發現如果 `dest` 被 `resize` 成大小為 `src->size + 1`,需要額外處理 `dest` 陣列的最後一個 `unsigned int` (第三行)。
```c=
if (shift > z) {
bn_resize(dest, src->size + 1);
dest->number[src->size] = src->number[src->size - 1] >> (32 - shift);
} else {
bn_resize(dest, src->size);
}
```
這麼一來 `fast doubling` 需要用到的函式幾乎都有了,接下來就是拿作業說明提供的 `bn_fib_fdoubling` 函式來跑即可。
#### bn_fib_fdoubling 改進
原始版本的 `bn_fib_fdoubling` 為以下:
```c
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);
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, k1);
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);
}
}
bn_free(f2);
bn_free(k1);
bn_free(k2);
}
```
後來利用 `__builtin_clz` 硬體指令可以直接從最高位的 `1` 開始計算,減少迴圈數。
接著為了避免執行 `bn_mult` 的 `(c == a || c == b)` 會用 `tmp` 來儲存 `c` 的情況所以用以下邏輯來避免這個情況。
```c
/* f1 = F(k) */
/* f2 = F(k+1) */
for (unsigned int i = 1U << (31 - __builtin_clz(n)); i; i >>= 1) {
/* F(2k) = F(k) * [ 2 * F(k+1) – F(k) ] */
bn_lshift(f2, 1, k1); // k1 = 2 * F(k+1)
bn_mult(k1, f1, k2); // k2 = 2 * F(k+1) * F(k)
bn_mult(f1, f1, k1); // k1 = F(k) * F(k)
bn_sub(k2, k1, f1); // f1 = F(2k) = 2 * F(k+1) * F(k) - F(k) * F(k)
/* F(2k+1) = F(k)^2 + F(k+1)^2 */
bn_mult(f2, f2, k2); // k2 = F(k+1) * F(k+1)
bn_add(k1, k2, f2); // f2 = F(2k+1) = F(k)^2 + F(k+1)^2
if (n & i) {
bn_swap(f1, f2); // f1 = F(2k+1)
bn_add(f1, f2, f2); // f2 = F(2k+2)
}
}
```
`fib v0` 代表修改前,`fib v1` 代表修改後,由此可見 `bn_alloc()` 的成本是很高的。
![](https://i.imgur.com/wpw7lKl.png)
#### 減少 `realloc` 次數
為了避免每次呼叫 `bn_resize` 時都會執行 `realloc` 造成對記憶體頻繁的修剪。
在 `bn` 結構體多加入了一個變數 `capacity`,用來事先配置額外的記憶體,所以在每次呼叫 `resize` 時都會先判斷是否還有空間可以使用 `(capacity > resize size)`,若是則可以直接回傳,避免再次執行 `realloc`。
```c=
static int bn_resize_v1(bn *src, size_t size)
{
...
if (src->capacity < size) {
src->capacity = (size+(CHUNK_SIZE-1)) & ~(CHUNK_SIZE-1);
src->number = realloc(src->number, sizeof(unsigned int) * src->capacity);
}
...
}
```
用 `perf` 比較前後結果,計算 `fib(1) ~ fib(10000)`,可以發現 `cache-references` 確實有明顯的下降。
```c=
BRFORE
Performance counter stats for './experiment/exp4' (5 runs):
48,912 cache-misses # 4.071 % of all cache refs ( +- 21.01% )
1,665,299 cache-references ( +- 18.85% )
19,037,748,191 instructions # 2.05 insn per cycle ( +- 0.00% )
9,267,320,914 cycles ( +- 0.07% )
7.06808 +- 0.0124 seconds time elapsed ( +- 0.17% )
---------------------------
AFTER
Performance counter stats for './experiment/exp4' (5 runs):
49,883 cache-misses # 4.824 % of all cache refs ( +- 30.55% )
365,370 cache-references ( +- 98.70% )
19,034,628,079 instructions # 2.05 insn per cycle ( +- 0.00% )
9,255,791,613 cycles ( +- 0.15% )
5.81458 +- 0.00717 seconds time elapsed ( +- 0.12% )
```
#### 用 `verify.py` 驗證
`verify.py` 會產生 `fib(0) ~ fib(想要測量到的範圍)` 並存放在 `expect` 裡,隨後讀取 `out` 並存在 `result`,經過處理後將答案存在 `dice`,
```c
#!/usr/bin/env python3
expect = [0, 1]
result = []
result_split = []
dics = []
for i in range(2, 1001):
expect.append(expect[i - 1] + expect[i - 2])
with open('out', 'r') as f:
tmp = f.readline()
while (tmp):
result.append(tmp)
tmp = f.readline()
f.close()
for r in result:
if (r.find('Reading') != -1):
result_split.append(r.split(' '))
k = int(result_split[-1][5].split(',')[0])
f0 = int(result_split[-1][9].split('.')[0])
dics.append((k, f0))
for i in dics:
fib = i[1]
if (expect[i[0]] != fib):
print('f(%s) fail' % str(i[0]))
print('input: %s' %(fib))
print('expected: %s' %(expect[i[0]]))
exit(1)
```
最後去跟 `expect` 做比較,如果有錯就會輸出類似以下的訊息:
```
f(93) fail
input: -6246583658587674878
expected: 12200160415121876738
```
目前測試到 `1000` 為止答案都是正確的。
```
Reading from /dev/fibonacci at offset 1000, returned the sequence 43466557686937456435688527675040625802564660517371780402481729089536555417949051890403879840079255169295922593080322634775209689623239873322471161642996440906533187938298969649928516003704476137795166849228875.
```
觀察 `make check` 時會執行的命令,
發現 `expected.txt` 不會變更到,所以
```shell
@diff -u out scripts/expected.txt && $(call pass)
```
就用 `verify.py` 來驗證,改為以下:
```shell
@scripts/verify.py && $(call pass)
```
且 `verify.py` 裡的 `exit()` 改為 `exit(1)`,代表執行期間有錯誤發生 (與預期答案不符)。
### 測量 user space 和 kernel space 的傳遞時間
原本的大數運算方式是直接在 `kernel space` 執行 `bn_to_string` 將 `bn->number` 轉成字串後通過 `copy_to_user` 將答案傳回 `user space`。
但是實作的大數結構是用一個 `unsigned int` 陣列來表示
* 以 `0xffffffff` 為例,其所需要的空間是 `4 Byte`。
* 轉為十進位是 `4294967295`,因為這邊是用字元表示所以所需空間是 `10 byte`,大於二進位表示時所需的空間。
所以我將傳送的方式改成傳送 `bn->number` 到 `user space` 而不是空間佔用較大的字串,
直接將計算結果透過 `copy_to_user` 傳送。
```c
static ssize_t fib_read(struct file *file,
char *buf,
size_t size,
loff_t *offset)
{
bn *fib = bn_alloc(1);
bn_fib_fdoubling(fib, *offset);
size = fib->size;
copy_to_user(buf, fib->number, sizeof(unsigned int)*size);
bn_free(fib);
return size;
}
```
接著由 `client` 接收,並用修改過後的 `bn_to_str` 將其轉成字串。
```c
char buf[1000];
int offset = 1000; /* TODO: try test something bigger than the limit */
int fd = open(FIB_DEV, O_RDWR);
if (fd < 0) {
perror("Failed to open character device");
exit(1);
}
for (int i = 0; i <= offset; i++) {
lseek(fd, i, SEEK_SET);
unsigned int size = read(fd, buf, 1000);
char *p = bn_to_str(buf, size, 0);
printf("Reading from " FIB_DEV
" at offset %d, returned the sequence "
"%s.\n",
i, p);
}
```
最後對 `copy_to_user` 的執行時間做比較,可以發現傳送所花費的時間確實有差,而且數字越大越明顯。
![](https://i.imgur.com/xg7bKav.png)
但是這麼做會造成將 `bn` 轉成十進位需要在 `user space` 做相對於在 `kernel space` 還要花時間,~~原因推測是因為 `bn_to_string` 會呼叫到 `malloc` 需要切換到 `Supervisor mode`,導致時間花費會比較高。~~
`malloc` 是 `library function`,底下實際的實作是透過 `sbrk` 或 `mmap`。
:::warning
`malloc` 不是系統呼叫!請善用 perf 一類的工具分析執行成本。
:notes: jserv
:::
後來發現編譯時用 `-O3`,之前沒用的時候導致修改後的 `client` 的 `instructions` 數過多導致執行時間變長。
接著用 `perf` 做比較可以發現兩個方法所花費的時間基本上差不多,但是 `pass_number_array` 相對減少了 `copy_to_user` 的時間,也就是傳送 `bn->number` 到 `user space`,然後在 `user space` 執行 `bn_to_string`。
* pass_number_array
```c=
Performance counter stats for './client' (10 runs):
156.76 msec task-clock # 0.997 CPUs utilized ( +- 0.08% )
0 context-switches # 0.000 /sec
0 cpu-migrations # 0.000 /sec
83 page-faults # 529.003 /sec ( +- 0.36% )
250,231,871 cycles # 1.595 GHz ( +- 0.08% )
774,286,066 instructions # 3.09 insn per cycle ( +- 0.00% )
61,917,107 branches # 394.631 M/sec ( +- 0.01% )
400,112 branch-misses # 0.65% of all branches ( +- 0.15% )
0.157247 +- 0.000113 seconds time elapsed ( +- 0.07% )
```
* pass_string
```c=
Performance counter stats for './client' (10 runs):
155.82 msec task-clock # 0.946 CPUs utilized ( +- 1.48% )
0 context-switches # 0.000 /sec
0 cpu-migrations # 0.000 /sec
49 page-faults # 299.439 /sec ( +- 0.68% )
248,725,566 cycles # 1.520 GHz ( +- 1.43% )
832,882,929 instructions # 3.22 insn per cycle ( +- 0.04% )
62,446,804 branches # 381.613 M/sec ( +- 0.11% )
400,226 branch-misses # 0.64% of all branches ( +- 0.68% )
0.16468 +- 0.00242 seconds time elapsed ( +- 1.47% )
```
原本以為會有明顯差距,但實際上沒有,
從以上結果可以看到 `malloc` 再執行時沒有呼叫 `system call` 因為沒有做 `context switch`,
所以我後來去查了一些可能的因素,發現 `kmalloc` 和 `malloc` 它們實作上的不同,
`kmalloc` 配置的記憶體是 `physically contiguous` 的,通常用於配置 `128 KB` 以下的空間。
而 `malloc` 拿到的記憶體是 `virtual memory`(也就是物理上不連續的記憶體),所以當程式去修改這片記憶體時,就有可能會發生 `page fault`。
由以上兩點來看,剛好可以呼應前面的 `perf` 分析的 `page fault` 的結果。
進一步優化 `copy_to_user` 所需要複製的資料大小,目前的算法是陣列多大就複製多大,可優化的地方是最後一個元素的 `leading zero` 是不需要的,所以就只傳送最少所需要的位元組即可。
```c=
int count = 32 - __builtin_clz(fib->number[fib->size-1]);
count = (count >> 3) + !!(count & 0x7);
copy_to_user(buf, fib->number, sizeof(unsigned int) * (len-1) + count);
```
### 使用 `sysfs` 界面
為了取得比用 `printk` 更準確的時間,所以透過 `sysfs` 的界面來存取 `fibdrv` 的執行時間。
[`sysfs` 描述](https://man7.org/linux/man-pages/man5/sysfs.5.html):
> The sysfs filesystem is a pseudo-filesystem which provides an interface to kernel data structures.(More precisely, the files and directories in sysfs provide a view of the kobject structures defined internally within the kernel.)The files under sysfs provide information about devices, kernel modules, filesystems,and other kernel components.
簡單來說 `sysfs` 可以提供一個界面 (`kobject structures`) 讓 `user space` 可以存取 `kernel` 內的資料結構。
* kobject
* 定義於 [linux/kobject.h](https://elixir.bootlin.com/linux/v2.6.34/source/include/linux/kobject.h#L59),主要功能有:
* `reference count`
* 管理物件的鏈結串列(集合),`linux` 風格的 `linked list`,嵌入在別的資料結構中使用
* 將該物件的屬性導出到 `user space`(透過 `sysfs`)
* `kobject` 在 `sysfs` 會以目錄的形式呈現。
* 該物件具有的屬性(`attribute`)會以檔案的方式表示。
* attribute
* 每個 `kobject` 都會有一到多個屬性,每個屬性都代表核心內的某資訊
* 使用者可以通過 `attribute` 中的 `show` 和 `store` callback 函式來取得核心資訊
* 對 `attribute` 進行讀取操作(ex: cat)時會執行 `show`
* 對 `attribute` 進行寫入操作(ex: echo)時會執行 `store`
以下依照[kobject-example.c](https://elixir.bootlin.com/linux/latest/source/samples/kobject/kobject-example.c)的實作
#### 設定 `attribute`
* `__ATTR` 定義在 [`linux/sysfs.h`](https://github.com/torvalds/linux/blob/master/include/linux/sysfs.h) 中:
```c=
#define __ATTR(_name, _mode, _show, _store) { \
.attr = {.name = __stringify(_name), \
.mode = VERIFY_OCTAL_PERMISSIONS(_mode) }, \
.show = _show, \
.store = _store, \
}
```
* 定義 `show`, `store`
* `store`: 利用寫入的方式傳入要算第幾個費氏數列,並將執行時間紀錄在 `kt`。
* `show`: 將 `fibdrv` 內的 `kt` 轉為奈秒後輸出。
```c=
static ssize_t show(struct kobject *kobj,
struct kobj_attribute *attr,
char *buf)
{
kt_ns = ktime_to_ns(kt);
return snprintf(buf, 16, "%lld\n", kt_ns);
}
static ssize_t store(struct kobject *kobj,
struct kobj_attribute *attr,
const char *buf,
size_t count)
{
int ret, n_th;
if (ret = kstrtoint(buf, 10, &n_th)) {
return ret;
}
bn *fib = bn_alloc(1);
kt = ktime_get();
bn_fib_fdoubling(fib, n_th);
kt = ktime_sub(ktime_get(), kt);
bn_free(fib);
return count;
}
```
* 設定 `attribute`
```c=
struct kobject *fib_ktime_kobj;
static ktime_t kt;
static long long kt_ns;
// 設定一個 attribute 叫做 kt_ns,並設定權限為 rw-rw-r--
static struct kobj_attribute ktime_attr = __ATTR(kt_ns, 0664, show, store);
// 定義 kobject 的 attributes,可以有一到多個
static struct attribute *ktime_attrs[] = {
&ktime_attr.attr,
NULL,
};
// 把 attribute 封裝成 attribute_group,之後建立 sysfs 界面時使用
static struct attribute_group ktime_attr_group = {
.attrs = ktime_attrs,
};
```
#### 建立目錄
* 利用 `kobject_create_and_add` 在 `/sys/kernel/` 下建立 `fib_ktime` 目錄。
* 然後用 `sysfs_create_group` 把剛剛建立的屬性放到這個目錄下。
```c
fib_ktime_kobj = kobject_create_and_add("fib_ktime", kernel_kobj);
if (!fib_ktime_kobj)
return -ENOMEM;
int retval = sysfs_create_group(fib_ktime_kobj, &ktime_attr_group);
if (retval)
goto failed_sysfs_create;
```
最後用 `kobject_put` 減少 `reference count`。
```c=
static void __exit exit_fib_dev(void)
{
mutex_destroy(&fib_mutex);
device_destroy(fib_class, fib_dev);
class_destroy(fib_class);
cdev_del(fib_cdev);
unregister_chrdev_region(fib_dev, 1);
kobject_put(fib_ktime_kobj);
}
```
#### 簡單測試
* 測試 `fib(1000)` 的執行時間
```c=
will@will:/sys/kernel/fib_ktime$ sudo sh -c "echo 1000 > kt_ns"
will@will:/sys/kernel/fib_ktime$ sudo cat kt_ns
8838
```